互联网开发的演变历程

  • 第一阶段:

数据访问量不大,简单的架构即可搞定。就是常见的SSM架构:Spring + Spring MVC + Mybatis + MySQL
image.png

  • 第二阶段:

数据访问量大,使用缓存技术来缓解数据库的压力。不同业务访问不同的数据库。
image.png
这个cache可以看成是Redis,当A查询数据,会先从缓存中查询,如果缓存中没有,则会从数据库中查询,查询出来的数据会存到缓存中,当B、C等等用户再查询数据,就可以直接从缓存中获取不用再去查询数据库。缓存就是将数据存到内存中。

  • 第三阶段:

主从读写分离;之前的缓存确实能够缓解数据库的压力,但是写和读都集中在一个数据库上,压力又来了。一个数据库负责写,一个数据库负责读,分工合作。
让Master(主数据库)来响应事务性(增删改)操作,让slave(从数据库)来响应非事务性(查询)操作。然后再采用主从复制来把Master上的事务性操作同步到slave数据库中。
image.png

  • 第四阶段

MySQL的主从复制,读写分离的基础上,MySQL的主库开始出现瓶颈。由于MyISAM使用表锁,并发性能特别差,分库分表开始流行,MySQL也提出了表分区但是还不稳定。有了MySQL集群。
image.png

Redis简介

Redis是一种运行速度很快,并发性能很强,并且运行在内存上的NoSql(not only sql)数据库 针对传统关系型数据库的优势:

  • NoSQL数据库无需事先为存储的数据建立字段,随时可以存储自定义的数据格式
  • 而在关系数据库里,增删字段是一件非常麻烦的事情。如果是非常大数据量的表,增加字段就是噩梦

Redis的常用使用场景:

  • 缓存:缓存是Redis最常用的使用场景,在提升服务性能方面非常有效;一些频繁访问的数据,经常被访问的数据如果放在关系型数据库,每次查询的开销会很大,而放在Redis中,Redis是放在内存中的可以很高效的访问。
  • 排行榜:如果使用传统的关系型数据库来实现,会非常的麻烦,而利用Redis的SortSet数据结构能够简单的搞定。
  • 计算器/限速器:利用Redis中原子性的自增操作,可以统计类似用户点赞数,用户访问数、播放量等,这类操作如果用MySQL,频繁的读写会带来相当大的压力;限速器比较典型的使用场景就是限制某个用户访问某个API的频率,常用的有抢购业务时,防止用户疯狂点击带来不必要的压力。
  • 好友关系:利用集合的一些命令,比如求交集、并集、差集等,可以方便搞定一些共同好友、共同爱好之类的功能;
  • 简单消息队列:除了Redis自身的发布/订阅模式,可以用List来实现一个队列机制,比如:到货通知、邮件发送之类的需求,不需要高可靠,但是会带来非常大的DB压力,完全可以用List来完成异步解耦
  • Session共享:以jsp为例,默认session是保存在服务器的文件中,如果是集群服务,同一个用户过来可能落在不同机器上,这就会导致用户频繁登录;采用Redis保存Session后,无论用户落在哪台机器上都能够获取对应的session信息

缓存还有其他的:Memcache、MongoDB等

  • memcache 可以缓存其他东西:图片、视频等
  • memcache 数据结构单一KV,Redis更加丰富,还提供了list set hash等数据结构的存储,有效的减少网络IO的次数
  • 虚拟内存-Redis当物理内存用完时,可以将一些很久没用到的value交换到磁盘
  • 存储数据安全0memcache挂掉后,数据没了(没有持久化机制);Redis可以定期保存到磁盘(持久化)
  • 灾难恢复-memcache挂掉后,数据不可恢复;Redis数据丢失后可以通过RBD或AOF恢复
  • Redis和MongoDB并不是竞争关系,更多的是一种协作共存的关系
  • MongoDB本质上还是硬盘数据库,在复杂查询时仍然会有大量的资源消耗,而且在处理复杂逻辑时仍然要不可避免进行多次查询
  • 需要Redis或memcache这样的内存数据库来作为中间层进行缓存和加速
  • 在某些复杂页面的场景中,整个页面的内容如果都从MongoDB中查询,可能要几十个查询语句,耗时很长。如果需求允许,则可以把整个页面的对象缓存到Redis中,定期更新,这样MongoDB和Redis能很好的协作起来

分布式数据库CAP原理

传统的关系型数据库事务具备ACID:

  • A:原子性
  • C:一致性
  • I:独立性
  • D:持久型

分布式数据库的CAP:

  • C(Consistency):强一致性。“all nodes see the same data at the same time”.即更新操作成功并返回客户端后,所有节点在同一时间的数据完全一致,这就是分布式的一致性。一致性的问题在并发系统中不可避免,对于客户端来说一致性指的是并发访问时更新过的数据如何获取的问题,从服务端来看,则是更新如何复制分布到整个系统,以保证数据最终一致
  • A(Availability):高可用性:“Reads and Writes always succeed”即服务一直可用,而且要是正常的响应时间,好的可用性主要是指系统能够很好的为用户服务,不出现用户操作失败或者访问超时等用户体验不好的情况。
  • P(Partition tolerance):分区容错性:即分布式系统在遇到某节点或网络分区故障时,仍然能够对外提供满足一致性或可用性的服务。分区容错性要求能够使应用虽然是一个分布式系统,而看上去好像是在一个可以运转正常的整体,比如现在的分布式系统中有某一个或者几个机器宕掉了,其他剩下的机器还能够正常运转满足系统需求,对于用户而言并没有什么体验上的影响。

CAP 理论:

  • CAP理论提出就是针对分布式数据库环境的,P属性必须容忍他的存在,而且是必须具备的
  • P是必须的,那么需要选择A和C
  • 在分布式环境下,为了保证系统可用性,通常采取了复制的方式,避免一个节点损坏,导致系统不可用,那么就出现了每个节点上的数据出现了很多个副本的情况,而数据从一个节点复制到另外的节点时需要时间和要求网络畅通的,所以当P发生时,也就是无法向某个节点复制数据时,这时候有两个选择:
    • 选择可用性A,哪个失去联系的节点依然可以向系统提供服务,不过它的数据就不能保证是同步的了(失去了C属性)
    • 选择一致性C,为了保证数据库的一致性,我们必须等待失去联系的节点恢复过来,在这个过程中,那个节点是不允许对外提供服务的,这时候系统处于不可用状态(失去了A属性)

举个例子:读写分离,某个节点负责写入数据,然后将数据同步到其他节点,其他节点提供读取的服务,当两个节点出现通信问题时,就面临着选择A(继续提供服务,但是数据不保证准确),C(用户处于等待状态,一致等到数据同步完成)
分区是常态,不可避免,三者不可共存。
可用性和一致性是一对冤家:

  • 一致性高,可用性低
  • 一致性低,可用性高

根据CAP理论,将NoSQL数据库分成了满足CA原则、满足CP原则和满足AP原则三大类:

  • CA-单点集群,满足一致性、可用性的系统,通常在可扩展性上不太强大
  • CP-满足一致性、分区容忍性的系统,通常性能不是特别高
  • AP-满足可用性、分区容忍性的系统,通常可能对一致性要求低一些

Redis的安装及使用

如下安装包:把Redis安装到Linux虚拟机上,将压缩包传输到虚拟机中/opt/进行解压,然后安装gcc:yum -y install gcc

进入redis目录,进行编译:make 编译之后,可以开始安装了make install

Redis 的启动和关闭

Redis默认不会使用后台运行,需要修改配置文件:

  1. vi redis.conf
  2. ## 修改daemonize为yes
  3. daemonize yes
  • 启动Redis

    cd /usr/local/bin
    # 加上配置文件启动
    redis-server /opt/redis-5.0.4/redis.conf
    

    这样Redis就会在后台启动成功了
    image.png
    检测6379端口,看一下端口是否被占用,如果被占用则说名Redis正在运行

    [root@localhost bin]# netstat -lntp | grep 6379
    tcp        0      0 127.0.0.1:6379          0.0.0.0:*               LISTEN      48654/redis-server
    
  • 关闭redis

    redis-cli shutdown
    # 多实例关闭
    redis-cli -p 6379 shutdown
    
  • 检测后台进程是否存在

    ps -ef|grep redis
    

    image.png

  • 连接redis并测试 ```shell redis-cli

[root@localhost bin]# redis-cli 127.0.0.1:6379> ping PONG 127.0.0.1:6379>


- HelloWord
```shell
127.0.0.1:6379> set k1 china
OK
127.0.0.1:6379> get k1
"china"
127.0.0.1:6379> set k HelloWorld
OK
127.0.0.1:6379> get k
"HelloWorld"
127.0.0.1:6379>
  • 测试性能 ```shell redis-benchmark

ctrl+c 退出Redis。以最大的并发数测试

[root@localhost bin]# redis-benchmark ====== PING_INLINE ====== 100000 requests completed in 0.76 seconds # 10万个请求0.76s 50 parallel clients 3 bytes payload keep alive: 1

99.69% <= 1 milliseconds 99.92% <= 2 milliseconds 100.00% <= 2 milliseconds 130718.95 requests per second # 每秒处理的请求数量


- Redis默认有16个数据库
```shell
cat /opt/redis-5.0.4/redis.conf

在Redis的配置文件有如下解释:0 - 15

# Set the number of databases. The default database is DB 0, you can select
# a different one on a per-connection basis using SELECT <dbid> where
# dbid is a number between 0 and 'databases'-1
databases 16

切换数据库:

127.0.0.1:6379> select 16 # 切换到16号数据库 Redis默认是0-15
(error) ERR DB index is out of range
127.0.0.1:6379> select 15 # 切换到15号数据库
OK
127.0.0.1:6379[15]> keys * # 查询15号数据库的 key
(empty list or set)
127.0.0.1:6379[15]> 
127.0.0.1:6379[15]> select 0 # 切换到之前操作 默认的是0号数据库
OK
127.0.0.1:6379> keys *
1) "k2"
2) "key:__rand_int__"
3) "counter:__rand_int__"
4) "k1"
5) "k"
6) "mylist"
127.0.0.1:6379>
  • 数据库键的数量

    [root@localhost bin]# redis-cli 
    127.0.0.1:6379> dbsize
    (integer) 5
    127.0.0.1:6379> set k2 v2
    OK
    127.0.0.1:6379> dbsize
    (integer) 6
    127.0.0.1:6379> keys *
    1) "k2"
    2) "key:__rand_int__"
    3) "counter:__rand_int__"
    4) "k1"
    5) "k"
    6) "mylist"
    127.0.0.1:6379>
    
  • 清空数据库

    flushdb # 清空当前库
    flushall # 清空所有库
    

    模糊查询(key)

    模糊查询keys命令,有三个通配符:

    • :通配任意多个字符

*keys * 查询所有
keys k* : 查询k开头的
keys *1 : 查询结果为1的
keys *k* : 查询包含k的

127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k33"
4) "k1"
127.0.0.1:6379> keys k*
1) "k3"
2) "k2"
3) "k33"
4) "k1"
127.0.0.1:6379> keys *1
1) "k1"
127.0.0.1:6379> set abckabc a
OK
127.0.0.1:6379> keys *k*
1) "k2"
2) "k33"
3) "k3"
4) "abckabc"
5) "k1"
  • ? : 通配单个字符

keys k? : 模糊查询k字开头的,并且匹配一个字符
keys k?? : 模糊匹配k字开头,长度是3

127.0.0.1:6379> keys k?
1) "k2"
2) "k3"
3) "k1"
127.0.0.1:6379> keys k??
1) "k33"
  • [] : 通配括号内的某一个字符

记得其他字母,就第二个字母可能是a或e

keys r[ae]dis

127.0.0.1:6379> set radis a
OK
127.0.0.1:6379> set redis  b
OK
127.0.0.1:6379> keys r[ae]dis
1) "redis"
2) "radis"

127.0.0.1:6379> keys ab[cd]kabc
1) "abckabc"

键 (key)

  • 判断某一个键是否存在

    127.0.0.1:6379> exists k1
    (integer) 1
    127.0.0.1:6379> exists k
    (integer) 0
    
  • 移动键到几号库

    127.0.0.1:6379> move k1 8
    (integer) 1 # 移到8号库
    127.0.0.1:6379> exists k1
    (integer) 0 # 当前库已经不存在k1了
    127.0.0.1:6379> select 8 # 切换到8号库
    OK
    127.0.0.1:6379[8]> exists k1
    (integer) 1
    
  • 查看键还有多久过期(-1 永不过期,-2 已过期)

    127.0.0.1:6379> ttl k2
    (integer) -1
    127.0.0.1:6379> ttl redis
    (integer) -1
    
  • 为键设置过期时间

    127.0.0.1:6379> set k1 v1 
    OK
    127.0.0.1:6379> get k1
    "v1"
    127.0.0.1:6379> ttl k1
    (integer) -1
    127.0.0.1:6379> expire k1 10 # 设置k1 存活10s
    (integer) 1
    127.0.0.1:6379> ttl k1 # 查看k1 还能存活多久
    (integer) 8
    127.0.0.1:6379> ttl k1
    (integer) 6
    127.0.0.1:6379> ttl k1
    (integer) 0
    127.0.0.1:6379> ttl k1
    (integer) -2 # k1已经过期了
    127.0.0.1:6379> keys *
    (empty list or set)  # k1已经不存在了
    
  • 查看键的数据类型

    127.0.0.1:6379> set k1 v1
    OK
    127.0.0.1:6379> set k2 1
    OK
    127.0.0.1:6379> type k1
    string
    127.0.0.1:6379> type k2
    string
    

    使用Redis

    Redis有五大数据类型。

    字符串String

    127.0.0.1:6379> set k1 v1
    OK
    127.0.0.1:6379> get k1
    "v1"
    127.0.0.1:6379> set k2 v2
    OK
    127.0.0.1:6379> keys *
    1) "k2"
    2) "k1"
    127.0.0.1:6379> del k2 # 删除k2
    (integer) 1
    127.0.0.1:6379> keys *
    1) "k1"
    127.0.0.1:6379> get k1
    "v1"
    127.0.0.1:6379> append k1 abc # 追加k1的value
    (integer) 5 # 返回k1的value长度
    127.0.0.1:6379> get k1
    "v1abc"
    127.0.0.1:6379> strlen k1 # 查询k1的value长度
    (integer) 5
    
  • incr/decr/incrby/decrby(加减操作):

    127.0.0.1:6379> set k1 1
    OK
    127.0.0.1:6379> incr k1 # k1的值加1(++)
    (integer) 2
    127.0.0.1:6379> incr k1 # k1的值加1 (++)
    (integer) 3
    127.0.0.1:6379> get k1
    "3"
    127.0.0.1:6379> decr k1 # k1的值减1 (--)
    (integer) 2
    127.0.0.1:6379> decr k1
    (integer) 1
    127.0.0.1:6379> get k1
    "1"
    127.0.0.1:6379> incrby k1 3 # k1的值加3
    (integer) 4
    127.0.0.1:6379> get k1
    "4"
    127.0.0.1:6379> decrby k1 3 # k1的值减3
    (integer) 1
    127.0.0.1:6379> get k1
    "1"
    
  • getrange/setrange:

    127.0.0.1:6379> set k1 abc
    OK
    127.0.0.1:6379> append k1 def
    (integer) 6
    127.0.0.1:6379> get k1
    "abcdef"
    127.0.0.1:6379> getrange k1 0 -1 # 查询k1的值全部的值
    "abcdef"
    127.0.0.1:6379> getrange k1 0 3  # 查询k1的值,范围下标0-3
    "abcd"
    127.0.0.1:6379> setrange k1 1 xx # 从下标为1的位置开始,替换xx
    (integer) 6
    127.0.0.1:6379> get k1
    "axxdef"
    
  • setex/setnx:

set with expir : 添加数据的同时设置生命周期
set if not exist : 添加数据的时候判断是否已经存在,防止已存在的数据被覆盖掉

127.0.0.1:6379> setex k1 5 v1  # 设置k1的生命周期为5s
OK
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> get k1
(nil)
127.0.0.1:6379> setnx k1 v2 # 设置k1的值,如果不存在则设置
(integer) 1
127.0.0.1:6379> get k1
"v2"
127.0.0.1:6379> setnx k1 v3 # 设置k1的值,已经存在,设置失败
(integer) 0
127.0.0.1:6379> get k1
"v2"
  • mset/mget/msetnx:

    127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 # 设置多个key/value
    OK
    127.0.0.1:6379> keys *
    1) "k3"
    2) "k2"
    3) "k1"
    127.0.0.1:6379> mget k1 k2 k3 # 获取多个键的值
    1) "v1"
    2) "v2"
    3) "v3"
    127.0.0.1:6379> msetnx k1 v1 k4 v4 # 只要设置的key有一个存在的 就会失败
    (integer) 0
    127.0.0.1:6379> keys *
    1) "k3"
    2) "k2"
    3) "k1"
    127.0.0.1:6379> msetnx k4 v4 k5 v5 # 添加不存在的key 添加成功
    (integer) 1
    127.0.0.1:6379> keys *
    1) "k2"
    2) "k3"
    3) "k5"
    4) "k4"
    5) "k1"
    
  • getset:先get后set

    127.0.0.1:6379> getset k6 v6 # 如果不存在则get为null,然后再将值添加到库 返回null
    (nil)
    127.0.0.1:6379> keys *
    1) "k2"
    2) "k3"
    3) "k5"
    4) "k4"
    5) "k6"
    6) "k1"
    127.0.0.1:6379> getset k6 v61 # 如果存在k6,先获取值v6,再修改值为v61
    "v6"
    127.0.0.1:6379> get k6 # 获取k6的值为v61
    "v61"
    

    列表List

    push和pop

  • lpush/rpush/lrange :

上:左,下:右

  • l:left
  • r:right
    127.0.0.1:6379> lpush list01 1 2 3 4 5 # 从上往下添加数据
    (integer) 5
    127.0.0.1:6379> lrange list01 0 -1 # 查询list01中的全部数据,0表示开始 -1 表示结尾
    1) "5"
    2) "4"
    3) "3"
    4) "2"
    5) "1"
    127.0.0.1:6379> lrange list01 0 5
    1) "5"
    2) "4"
    3) "3"
    4) "2"
    5) "1"
    127.0.0.1:6379> rpush list02 1 2 3 4 5 # 从下往上添加数据
    (integer) 5
    127.0.0.1:6379> lrange list02 0 -1
    1) "1"
    2) "2"
    3) "3"
    4) "4"
    5) "5"
    
  • lpop/rpop:移除第一个元素(上左下右)

    127.0.0.1:6379> lpop list02  左(上)移除第一个元素
    "1"
    127.0.0.1:6379>  lrange list02 0 -1
    1) "2"
    2) "3"
    3) "4"
    4) "5"
    127.0.0.1:6379> rpop list02
    "5"
    127.0.0.1:6379>  lrange list02 0 -1
    1) "2"
    2) "3"
    3) "4"
    
  • lindex:根据下标查询元素(自上而下)

    127.0.0.1:6379> lrange list01 0 -1
    1) "5"
    2) "4"
    3) "3"
    4) "2"
    5) "1"
    127.0.0.1:6379> lindex list01 2
    "3"
    127.0.0.1:6379> lindex list01 1
    "4"
    127.0.0.1:6379> lindex list01 0
    "5"
    
  • llen:返回集合长度

    127.0.0.1:6379> llen list01
    (integer) 5
    
  • lrem:删除n个value

    127.0.0.1:6379> lpush list01 1 1 2 2 3 3 4 4 5
    (integer) 9
    127.0.0.1:6379> lrem list01 2 3
    (integer) 2
    127.0.0.1:6379> lrange list01 0 -1
    1) "5"
    2) "4"
    3) "4"
    4) "2"
    5) "2"
    6) "1"
    7) "1"
    127.0.0.1:6379> lrem list01 2 1 # 从list01中移除2个1
    (integer) 2
    127.0.0.1:6379> lrange list01 0 -1
    1) "5"
    2) "4"
    3) "4"
    4) "2"
    5) "2"
    127.0.0.1:6379> lrem list01 1 1
    (integer) 0
    127.0.0.1:6379> lrange list01 0 -1
    1) "5"
    2) "4"
    3) "4"
    4) "2"
    5) "2"
    127.0.0.1:6379> lrem list01 1 2 # 从list01中移除1个2
    (integer) 1
    127.0.0.1:6379> lrange list01 0 -1
    1) "5"
    2) "4"
    3) "4"
    4) "2"
    
  • ltrim : 截取指定范围的值

    127.0.0.1:6379> lpush list01 1  2 3 4 5 6 7 8 9
    (integer) 9
    127.0.0.1:6379> lrange list01 0 -1
    1) "9"
    2) "8"
    3) "7"
    4) "6"
    5) "5"
    6) "4"
    7) "3"
    8) "2"
    9) "1"
    127.0.0.1:6379> ltrim list01 3 6 # 截取下标3到6的值,其他的全都扔掉 从上而下坐标查询
    OK
    127.0.0.1:6379> lrange list01 0 -1
    1) "6"
    2) "5"
    3) "4"
    4) "3"
    
  • rpoplpush : 从一个集合搞一个元素到另一个集合中(右出一个,左进一个)

    127.0.0.1:6379> rpush list01 1 2 3 4 5
    (integer) 5
    127.0.0.1:6379> lrange list01 0 -1
    1) "1"
    2) "2"
    3) "3"
    4) "4"
    5) "5"
    127.0.0.1:6379> rpush list02 1 2 3 4 5
    (integer) 5
    127.0.0.1:6379> lrange list02 0 -1
    1) "1"
    2) "2"
    3) "3"
    4) "4"
    5) "5"
    127.0.0.1:6379> rpoplpush list01 list02 # list01右边出一个,从左进入到list02的第一个位置
    "5"
    127.0.0.1:6379> lrange list02 0 -1
    1) "5"
    2) "1"
    3) "2"
    4) "3"
    5) "4"
    6) "5"
    127.0.0.1:6379> lrange list01 0 -1
    1) "1"
    2) "2"
    3) "3"
    4) "4"
    

    image.png

  • lset: 改变某个下标的值

    127.0.0.1:6379> lrange list02 0 -1
    1) "5"
    2) "1"
    3) "2"
    4) "3"
    5) "4"
    6) "5"
    127.0.0.1:6379> lset list02 0 x # 将list02下标为0的值改为x
    OK
    127.0.0.1:6379> lrange list02 0 -1
    1) "x"
    2) "1"
    3) "2"
    4) "3"
    5) "4"
    6) "5"
    
  • linsert:插入元素 (可以插入某个元素之前或者之后)

头尾插入操作效率高,中间操作效率低

127.0.0.1:6379> linsert list02 before x java # 将值Java插入到值为x之前
(integer) 8
127.0.0.1:6379> lrange list02 0 -1
1) "java"
2) "x"
3) "1"
4) "java"
5) "2"
6) "3"
7) "4"
8) "5"
127.0.0.1:6379> linsert list02 after x java # 将值为java插入到值为x的后面
(integer) 9
127.0.0.1:6379> lrange list02 0 -1
1) "java"
2) "x"
3) "java"
4) "1"
5) "java"
6) "2"
7) "3"
8) "4"
9) "5"

集合Set

和Java中的set特点类似,不允许重复的元素存在

  • sadd/smembers/sismember:

    127.0.0.1:6379> sadd set01 1 1 2 2 3 3
    (integer) 3
    127.0.0.1:6379> smembers set01 # 查询set集合set01的值
    1) "1"
    2) "2"
    3) "3"
    127.0.0.1:6379> sismember set01 2 # 查询set集合set01是否存在值2,存在返回1
    (integer) 1
    127.0.0.1:6379> sismember set01 4 # 查询set集合set01是否存在值4,不存在返回0
    (integer) 0
    
  • scard

    127.0.0.1:6379> scard set01 # 查看集合中的元素
    (integer) 3
    
  • srem

    127.0.0.1:6379> srem set01 2 # 移除set01中的元素2
    (integer) 1
    127.0.0.1:6379> smembers set01
    1) "1"
    2) "3"
    
  • srandmember:从集合中随机获得几个元素

    127.0.0.1:6379> sadd set01 1 2 3 4 5 6 7 8 9
    (integer) 9
    127.0.0.1:6379> smembers set01
    1) "1"
    2) "2"
    3) "3"
    4) "4"
    5) "5"
    6) "6"
    7) "7"
    8) "8"
    9) "9"
    127.0.0.1:6379> srandmember set01 3 # 从set01集合中随机获得3个数据
    1) "4"
    2) "5"
    3) "6"
    127.0.0.1:6379> srandmember set01 3
    1) "3"
    2) "9"
    3) "6"
    
  • spop:随机出栈(随机移除一个元素)

    127.0.0.1:6379> smembers set01
    1) "1"
    2) "2"
    3) "3"
    4) "4"
    5) "5"
    6) "6"
    7) "7"
    8) "8"
    9) "9"
    127.0.0.1:6379> spop set01 # 随机移除一个元素
    "3"
    127.0.0.1:6379> spop set01 2 # 随机移除2个元素
    1) "4"
    2) "8"
    127.0.0.1:6379> smembers set01
    1) "1"
    2) "2"
    3) "5"
    4) "6"
    5) "7"
    6) "9"
    
  • smove:移动元素 将key1的某个值赋值给key2

    127.0.0.1:6379> sadd set01 1 2 3 4 5
    (integer) 5
    127.0.0.1:6379> sadd set02 x y z
    (integer) 3
    127.0.0.1:6379> smove set01 set02 3 # 将set01中的值3 移动到set02中
    (integer) 1 # 1 表示移动成功
    127.0.0.1:6379> smembers set01
    1) "1"
    2) "2"
    3) "4"
    4) "5"
    127.0.0.1:6379> smembers set02
    1) "3"
    2) "y"
    3) "z"
    4) "x"
    
  • 数学集合类

    • 交集:sinter

      127.0.0.1:6379> sadd set01 1 2 3 4 5
      (integer) 5
      127.0.0.1:6379> sadd set02 2 a 1 b 3
      (integer) 5
      127.0.0.1:6379> smembers set01
      1) "1"
      2) "2"
      3) "3"
      4) "4"
      5) "5"
      127.0.0.1:6379> sinter set01 set02 # 获取set01和set02共同存在的元素
      1) "1"
      2) "2"
      3) "3"
      
    • 并集:sunion

      127.0.0.1:6379> sunion set01 set02 # 合成一个整体 重复的只保留一个
      1) "5"
      2) "a"
      3) "b"
      4) "4"
      5) "3"
      6) "2"
      7) "1"
      
    • 差集:sdiff

      127.0.0.1:6379> sdiff set01 set02 # 在set01中存在,在set02中不存在
      1) "4"
      2) "5"
      127.0.0.1:6379> sdiff set02 set01 # 在set02中存在,在set01中不存在
      1) "b"
      2) "a"
      

      哈希Hash

      KV模式不变,但V是一个键值对

  • hset/hget/hmset/hmget/hgetall/hdel: 添加、获取、多添加、多得到、删除

    127.0.0.1:6379> hset user id 1001 # 添加user,值id=1001
    (integer) 1
    127.0.0.1:6379> hget user
    (error) ERR wrong number of arguments for 'hget' command
    127.0.0.1:6379> hget user id # 查询user必须指明具体的字段
    "1001"
    127.0.0.1:6379> hmset phone id 2 name prim # 添加多个属性
    OK
    127.0.0.1:6379> hget phone id # 获得phone中的id的值
    "2"
    127.0.0.1:6379> hget phone name
    "prim"
    127.0.0.1:6379> hmget phone id name # 获得phone的多个字段值
    1) "2"
    2) "prim"
    127.0.0.1:6379> hmget user id 
    1) "1001"
    127.0.0.1:6379> hgetall phone # 获得phone全部信息
    1) "id"
    2) "2"
    3) "name"
    4) "prim"
    127.0.0.1:6379> hdel user id # 删除user的字段id
    (integer) 1
    127.0.0.1:6379> hmget user id
    1) (nil)
    127.0.0.1:6379> hdel phone name
    (integer) 1
    127.0.0.1:6379> hgetall phone
    1) "id"
    2) "2"
    
  • hlen:返回元素的属性个数

    127.0.0.1:6379> hgetall phone
    1) "id"
    2) "2"
    127.0.0.1:6379> hlen phone
    (integer) 1
    
  • hexists:判断元素是否存在某个属性

    127.0.0.1:6379> hexists phone name
    (integer) 0 # 0 表示没有name属性
    127.0.0.1:6379> hexists phone id
    (integer) 1 # 1 表示有id属性
    
  • hkeys/hvals:获得属性的所有key/获取属性的所有value

    127.0.0.1:6379> hkeys phone
    1) "id"
    127.0.0.1:6379> hvals phone
    1) "2"
    
  • hincrby(自增整数)/hincrbyfloat(自增小数)

    127.0.0.1:6379> hmset student id 1001 name jake age 27
    OK
    127.0.0.1:6379> hincrby student age 2 # age增加整数
    (integer) 29
    127.0.0.1:6379> hget student age
    "29"
    127.0.0.1:6379> hmset user id 100 money 1000
    OK
    127.0.0.1:6379> hincrbyfloat user money 5.5 # money增加小数
    "1005.5"
    127.0.0.1:6379> hget user money
    "1005.5"
    
  • hsetnx : 添加的时候,先判断是否存在

    127.0.0.1:6379> hsetnx student age 18
    (integer) 0  # 添加成功
    127.0.0.1:6379> hsetnx student age2 18
    (integer) 1 # 添加失败
    127.0.0.1:6379> hgetall student
    1) "id"
    2) "1001"
    3) "name"
    4) "jake"
    5) "age"
    6) "31"
    7) "age2"
    8) "18"
    

    有序集合Zset

  • zadd/zrange:

    127.0.0.1:6379> zadd zset01 10 vip1 20 vip2 30 vip3 40 vip4 50 vip5
    (integer) 5
    127.0.0.1:6379> zrange zset01 0 -1 # 查询数据
    1) "vip1"
    2) "vip2"
    3) "vip3"
    4) "vip4"
    5) "vip5"
    127.0.0.1:6379> zrange zset01 0 -1 withscores # 带着分数查询数据
    1) "vip1"
    2) "10"
    3) "vip2"
    4) "20"
    5) "vip3"
    6) "30"
    7) "vip4"
    8) "40"
    9) "vip5"
    10) "50"
    
  • zrangebyscore 模糊查询

    127.0.0.1:6379> zrangebyscore zset01 20 40 # 查询 20<= score <=40
    1) "vip2"
    2) "vip3"
    3) "vip4"
    127.0.0.1:6379> zrangebyscore zset01 20 (40 # 查询 20<= score <40
    1) "vip2"
    2) "vip3"
    127.0.0.1:6379> zrangebyscore zset01 (20 (40 # 查询 20< score <=40
    1) "vip3"
    # 查询 10<= score <=40 共返回4个跳过前两个,取2个
    127.0.0.1:6379> zrangebyscore zset01 10 40 limit 2 2
    1) "vip3"
    2) "vip4"
    # 查询 10<= score <=40 共返回4个跳过前两个,取1个
    127.0.0.1:6379> zrangebyscore zset01 10 40 limit 2 1
    1) "vip3"
    # 查询 10<= score <=40 共返回4个跳过前1个,取1个
    127.0.0.1:6379> zrangebyscore zset01 10 40 limit 1 1
    1) "vip2"
    
  • zrem : 删除元素

    127.0.0.1:6379> zrem zset01 vip5
    (integer) 1
    127.0.0.1:6379> zrange zset01 0 -1
    1) "vip1"
    2) "vip2"
    3) "vip3"
    4) "vip4"
    
  • zcard/zcount/zscore/zrank

    127.0.0.1:6379> zrange zset01 0 -1
    1) "vip1"
    2) "vip2"
    3) "vip3"
    4) "vip4"
    127.0.0.1:6379> zcard zset01 # 查询zset01集合的长度
    (integer) 4
    127.0.0.1:6379> zcount zset01 20 50 # 查询zset01 分数在20和50之间共有几个元素
    (integer) 3
    127.0.0.1:6379> zcount zset01 20 30 # 查询zset01 分数在20和30之间共有几个元素
    (integer) 2
    127.0.0.1:6379> zrank zset01 vip3 # 查询vip3 在集合中的下标
    (integer) 2
    127.0.0.1:6379> zscore zset01 vip2 # 通过元素获得对应的分数
    "20"
    
  • zrevrank : 逆序查找下标 从下向上数

    127.0.0.1:6379> zrevrank zset01 vip3 # 逆序查找vip3下标
    (integer) 1
    127.0.0.1:6379> zrank zset01 vip3 # 查询vip3 在集合中的下标
    (integer) 2
    
  • zrevrange:逆序查询

    127.0.0.1:6379> zrevrange zset01 0 -1
    1) "vip4"
    2) "vip3"
    3) "vip2"
    4) "vip1"
    
  • zrevrangebyscore : 逆序范围查找

    127.0.0.1:6379> zrevrangebyscore zset01 40 20 limit 1 1
    1) "vip3"
    127.0.0.1:6379> zrevrangebyscore zset01 40 20 # 先写大值 在写小值 如果小值在前结果为空
    1) "vip4"
    2) "vip3"
    3) "vip2"
    127.0.0.1:6379> zrevrangebyscore zset01 20 40
    (empty list or set)
    

    持久化

    RDB

    Redis DataBase

    在指定的时间间隔内,将内存中的数据集的快照写入磁盘; 默认保存在/usr/local/bin中,文件名:dump.rdb

自动备份:
Redis是内存数据库,当我们每次用完Redis,关闭Linux时,按道理来说,内存释放,Redis中的数据也会随之消失,每次关机时,Redis会自动将数据备份到一个文件中:/usr/local/bin/dump.rdb

  1. 修改redis.conf文件汇总的自动备份策略 ```shell vi redis.conf
########################## SNAPSHOTTING

#

Save the DB on disk:

#

save

#

Will save the DB if both the given number of seconds and the given

number of write operations against the DB occurred.

#

In the example below the behaviour will be to save:

after 900 sec (15 min) if at least 1 key changed

after 300 sec (5 min) if at least 10 keys changed

after 60 sec if at least 10000 keys changed

#

Note: you can disable saving completely by commenting out all “save” lines.

#

It is also possible to remove all the previously configured save

points by adding a save directive with a single empty string argument

like in the following example:

#

save “” 如果只是用Redis的缓存的功能 注释掉所有的save,打开save “” 就会停止持久化

save 900 1 # 900s内 至少变更1次,才会自动备份 save 300 10 # 改成120s
save 60 10000 //60S内 至少变更10000次,才会自动备份


2. 使用`shutdown` 模拟关机,关机之前和关机之后,对比`dump.rdb`文件创建时间更新了

![image.png](https://cdn.nlark.com/yuque/0/2021/png/375694/1611925983177-a3e8d23c-c9b8-42d4-af3a-8c13059a1528.png#align=left&display=inline&height=279&margin=%5Bobject%20Object%5D&name=image.png&originHeight=558&originWidth=1238&size=332210&status=done&style=none&width=619)

3. 开机启动redis,在120S内插入10条数据,查看`dump.rdb` 的更新时间
3. 备份dump.rdb - dump.bak.rdb,可以删除掉dump.rdb,然后将备份的dump.bak.rdb 改为dump.rdb 数据就会还原回来

手动备份:<br />每次操作完成,执行命令`save`就会立刻备份
```shell
127.0.0.1:6379> set k17 v17 
OK
127.0.0.1:6379> save
OK

image.png
与RDB相关的配置:

# 类似进水口和出水口,出水口发生故障与否
# yes : 当后台备份是发生错误,前台停止写入
# no:不管死活,往死里怼
stop-writes-on-bgsave-error yes  

# 对于存储到磁盘中的快照,是否启动LZF压缩算法,一般会启动
rdbcompression yes

# 在存储快照后,是否启动CRC64算法进行数据校验
# 开启后,大约增加10%左右的CPU消耗
# 如果希望获得最大的性能提升,可以选择关闭
rdbchecksum yes

# 备份文件的名字
dbfilename dump.rdb

# 备份文件保存的目录
dir ./

RDB的优势:适合大规模数据恢复,对数据完整性和一致性要求不高
RDB的劣势:一定间隔备份一次,意外down掉,就失去最后一次快照的所有修改
如果想要数据的完整性和一致性要求高,那么就可以使用AOF

AOF

Append Only File 以日志的形式记录每个写操作; 将Redis执行过的写指令全部记录下来(读操作不记录) 只许追加文件,不可以改写文件; Redis在启动之初会读取该文件从头到尾执行一遍,这样来重新构建数据;

开启AOF
在做操作之前,将redis.conf总配置文件备份,修改如下:

 cp redis.conf redis.conf.bak

 vi redis.conf # 编辑配置文件

 # 修改appendonly为yes 开启AOF
 appendonly yes

# The name of the append only file (default: "appendonly.aof")
# 保存的文件名
appendfilename "appendonly.aof"

重新启动reids,设置如下:

[root@localhost bin]# redis-cli
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> set k1 v1 
OK
127.0.0.1:6379> set k2 v2 
OK
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> shutdown
not connected> quit

然后在看多了一个appendonly.aof
image.png
查看这个文件:vi appendonly.aof 里面写了如下一些东西:

$6
SELECT
$1
0
*3
$3
set
$2
k1
$2
v1
*3
$3
set
$2
k2
$2
v2
*1
$8
flushall

其实翻译过来就是:

select 0 # 选择0号数据库
set k1 v1 
set k2 v2 
flushall

如果我们想要去重新启动Redis恢复数据的话,注意AOF把flushall命令也记录下来的了,所以我们需要把appendonly.aof 文件中的flushall这句话删掉,然后重启Redis数据就恢复了。删掉最后一行,然后esc -> :wq 保存退出

SELECT
$1
0
*3
$3
set
$2
k1
$2
v1
*3
$3
set
$2
k2
$2
v2
*1
$8

重新启动redis: 如下看到没有,数据恢复了

[root@localhost bin]# redis-server /opt/redis-5.0.4/redis.conf
57288:C 29 Jan 2021 06:35:35.069 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
57288:C 29 Jan 2021 06:35:35.069 # Redis version=5.0.4, bits=64, commit=00000000, modified=0, pid=57288, just started
57288:C 29 Jan 2021 06:35:35.069 # Configuration loaded
[root@localhost bin]# redis-cli
127.0.0.1:6379> keys *
1) "k2"
2) "k1"

注意 在redis.conf 文件中,AOF和RDB两种备份策略同时开启,那么系统会如何选择呢?

  1. 编辑appendonly.aof 胡乱编写,保存退出

    $6
    SELECT
    $1
    0
    *3
    $3
    set
    $2
    k1
    $2
    v1
    *3
    $3
    set
    $2
    k2
    $2
    v2
    abcHell!abc
    hello aof
    
  2. 启动redis失败,所以是AOF优先载入来恢复原始数据,因为AOF比RDB数据保存的完整性更高

image.png

  1. AOF文件是可以修复的,Redis提供了redis-check-aof 会杀光不符合Redis语法规范的代码
    [root@localhost bin]# redis-check-aof --fix appendonly.aof 
    0x              51: Expected prefix '*', got: 'a'
    AOF analyzed: size=105, ok_up_to=81, diff=24
    This will shrink the AOF from 105 bytes, with 24 bytes, to 81 bytes
    Continue? [y/N]: y
    Successfully truncated AOF
    [root@localhost bin]# redis-server /opt/redis-5.0.4/redis.conf
    57587:C 29 Jan 2021 06:53:01.278 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
    57587:C 29 Jan 2021 06:53:01.278 # Redis version=5.0.4, bits=64, commit=00000000, modified=0, pid=57587, just started
    57587:C 29 Jan 2021 06:53:01.278 # Configuration loaded
    [root@localhost bin]# redis-cli
    127.0.0.1:6379> keys 8
    (empty list or set)
    127.0.0.1:6379> keys *
    1) "k2"
    2) "k1"
    127.0.0.1:6379>
    
    image.png
  • AOF 相关的配置 ```shell

    开启aof模式

    appendonly yes

    aof的文件名,别动

    appendfilename “appendonly.aof”

appendfsync 追写策略

appendfsync always # 每次数据变更,就会立即记录到磁盘,性能较差,但数据完整性好

appendfsync everysec # everysec 默认设置,异步操作,每秒记录,如果一秒内宕机,会有数据丢失

appendfsync no # 不追写

重写时是否运用appendfsync追写策略;用默认no即可,保证数据安全性

AOF采用文件追加的方式,文件会越来越大,为了解决这个问题,增加了重写机制,redis会自动记录上一次AOF文件大小,当AOF文件大小达到预先设定的大小时,redis就会启动AOF文件进行内容压缩,值保留可以恢复数据的最小指令集合

no-appendfsync-on-rewrite no

如果AOF文件大小已经超过原来的100%,也就是一倍,才会重写压缩

auto-aof-rewrite-percentage 100

如果AOF文件已经超过了64mb,才重写压缩

auto-aof-rewrite-min-size 64mb

<a name="0yskR"></a>
#### 如何选择?

- RDB : 只用作后备用途,建议15分钟备份一次就好
- AOF:
   - 在最恶劣的情况下,也只丢失不超过2秒的数据,数据完整性较高,但代价太大,会带来持续的IO
   - 对硬盘的大小要求也高,默认64mb太小了,企业级最少都是5G以上
   - master/slave才正确的选择(主从复制在后面讲解)


<a name="GEx1Y"></a>
### 事务
> 事务:可以一次执行多个命令,是一个命令组,一个事物中,所有命令都会序列化,不会被插队;
> 一个队列中,一次性、顺序性、排他性的执行一系列命令

三个特性:

- 隔离性:所有命令都会按照顺序执行,事务在执行的过程中,不会被其他客户端用来的命令打断
- 没有隔离级别:队列中的命令没有提交之前都不会被实际的执行,不存在“事务中查询要看到事务里的更新,事务外查询不能看到”。例如:有三个命令:当执行2号命令:钱加了20元,而3号命令依赖与2 这种事务是不可以的。
- 不保证原子性:如果一个命令失败,但是别的命令可能会执行成功的,没有回滚

三步走:

- 开启multi
- 入队queued
- 执行exec
- 放弃discard
```shell
127.0.0.1:6379> multi    # 开启事务
OK
127.0.0.1:6379> set k1 v1
QUEUED  # 加入队列
127.0.0.1:6379> set k2 v2 
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> exec  # 执行事务
1) OK
2) OK
3) "v2"
4) OK
127.0.0.1:6379> keys *
1) "k2"
2) "k1"
3) "k3"

一起失败:

127.0.0.1:6379> multi  # 开启事务
OK
127.0.0.1:6379> set k1 v1111
QUEUED
127.0.0.1:6379> set k2 v2222
QUEUED
127.0.0.1:6379> discard  # 放弃操作
OK
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> get k2
"v2"

一句报错,全部取消:

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> setlllll   # 出现错误
(error) ERR unknown command `setlllll`, with args beginning with: 
127.0.0.1:6379> set k5 v5
QUEUED
127.0.0.1:6379> exec  # 执行事务 有一个命令错误,其他的命令全部取消
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> keys *
1) "k2"
2) "k1"
3) "k3"

追究责任,谁的错,找谁去:

127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr k1 # 执行失败 v1 不能执行++操作
QUEUED
127.0.0.1:6379> set k4 v4 # 执行成功
QUEUED
127.0.0.1:6379> set k5 v5 # 执行成功
QUEUED
127.0.0.1:6379> exec
1) (error) ERR value is not an integer or out of range # 在执行时报错
2) OK # 成功
3) OK # 成功
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> keys * # k4和k5 添加成功了
1) "k2"
2) "k1"
3) "k4"
4) "k3"
5) "k5"

watch监控
模拟收入和支出
正常情况下:

127.0.0.1:6379> set in 100  # 收入100
OK
127.0.0.1:6379> set out 0  # 支出0
OK
127.0.0.1:6379> mulit # 开启事务
(error) ERR unknown command `mulit`, with args beginning with: 
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> decrby in 20 # 收入减20
QUEUED
127.0.0.1:6379> incrby out 20 # 支出加20
QUEUED
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20

在进行支出的时候后:

127.0.0.1:6379> watch in # 监控
OK
127.0.0.1:6379> multi 
OK
127.0.0.1:6379> decrby in 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED

#在执行事务之前,在另一个窗口,对in做了修改
127.0.0.1:6379> set in 1000
OK
127.0.0.1:6379> get in
"1000"

# 然后再执行事务,所以本次的事务将被打断,类似于“乐观锁”
127.0.0.1:6379> exec # 一旦执行exec就取消了监控
(nil)

unwatch:取消watch命令对所有key的操作

Redis的发布订阅

进程间的一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息
在一个窗口订阅三个频道:cctv1 cctv5 cctv6

127.0.0.1:6379> subscribe cctv1 cctv5 cctv6
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "cctv1"
3) (integer) 1
1) "subscribe"
2) "cctv5"
3) (integer) 2
1) "subscribe"
2) "cctv6"
3) (integer) 3
1) "message"
2) "cctv5"
3) "NBA"  # 接收到cctv5发送过来的消息

在另一个窗口发送消息:

127.0.0.1:6379> publish cctv5 NBA # 向cctv5发送消息
(integer) 1

主从复制

主从复制,就是redis集群的策略 配从(库)不配主(库) : 小弟可以选择谁是大哥,但大哥没有权利选择小弟 读写分离:主机写,从机读

一主二仆

  1. 准备三台服务器,安装redis 修改redis.conf

    bind 0.0.0.0
    
  2. 启动三台redis,并查看每台机器的角色

    info replication
    

    分别执行,三台机器都是master

    127.0.0.1:6379> info replication
    # Replication
    role:master
    connected_slaves:0
    master_replid:c89df123f3bf307476a2aa2e3e5ee8694cbd6eea
    master_replid2:0000000000000000000000000000000000000000
    master_repl_offset:0
    second_repl_offset:-1
    repl_backlog_active:0
    repl_backlog_size:1048576
    repl_backlog_first_byte_offset:0
    repl_backlog_histlen:0
    

    我这边的三台机器的 IP分别是:176.16.150.130 176.16.150.131 176.16.150.132

  3. 配从(库)不配主(库) : 小弟可以选择谁是大哥,但大哥没有权利选择小弟

我们让176.16.150.131 176.16.150.132去选择176.16.150.130作为大哥,那么就要在176.16.150.131 和 176.16.150.132分别配置:

slaveof 172.16.150.130 6379 # 主库的IP 和 端口号

然后我们在176.16.150.131 和 176.16.150.132在查看一下他们的角色:角色变成了salve,并显示了master的ip和端口
image.png
在看一下主库的角色:角色是Master,并且显示了连接两个slaves.
image.png

  1. 下面我们来测试一下读写分离

在主库写入数据:

127.0.0.1:6379> set k1 v2
OK
127.0.0.1:6379>

在两个从库中读取数据:从库并没有添加任何数据,就读取到了主库的数据

127.0.0.1:6379> keys *
1) "k1"

当我们在从库中添加数据呢?如下:会出现错误,也就说如果角色被定为salve只能有读取的权限,没有写入的权限,只有主库有写入的权限

127.0.0.1:6379> set k2 v2
(error) READONLY You can't write against a read only replica.
  • 只要跟了大哥(主库),数据会立刻同步
  • 主库可以添加数据,从库只负责读取数据,无权写入数据
  • 主库shutdown,从库会如何呢?从库仍然是slave并显示mater:down 离线
  • image.png
  • 主库重启,从库如何呢?从库仍然是slave,并且显示master:up 已上线
  • image.png
  • 从库shutdown,主库会如何,从库重启身份是否为发生变化?

假设:131从库shutdown,看一下主库的状态,主库没有发生变化,只是从库连接数变成了一个
image.png
重启131从库,看一下从库的状态:131从库成了mater自立门户了,不和原来的集群一起了。
image.png

血脉相传

一个主机理论上可以多个从机,但是这样的话,这个主机会很累。可以使用Java中继承中的传递性来解决这个问题,减轻主机的负担。
130:爷爷
131:爸爸
132:儿子
131虚拟机:slaveof 172.16.150.130 6379 131跟随130
132虚拟机:slaveof 172.16.150.131 6379 132 跟随 132
查看130库的状态:
image.png
查看131库的状态:
image.png
查看132库的状态:
image.png
在主库添加数据:set k5 v5 会同步到131库,同时132库也会同步查询到数据。

谋权篡位

一个主机,2个从机,当1个主机挂掉了,只能从2个主机中在选一个主机

当130主库挂掉,在131从库执行,如下代码,131就成了主库

slaveof no one

132跟随131

slaveof 172.16.150.131 6379

当130重启回来了,会如何呢?
131和132已经形成了新的集群,和130没有任何关系了,所以130成为了光杆司令了。

复制原理

image.png
上面几个步骤是从服务器数据初始化的所有操作。

  • 全量复制:slave初始化阶段,这是slave需要将master上所有数据都复制一份slave接收到数据文件后,存盘,并加载到内存中来。就是上图中的1234步骤
  • 增量复制:slave初始化后,开始正常工作时主服务器发生写操作同步到从服务器的过程:5和6
  • 注意:只要是重新连接master,都会执行全量复制同步
  • Redis主从同步策略
    • 主从刚刚连接的时候,进行全量同步;全不同结束后,进行增量同步
    • 如果有需要,slave在任何时候都可以发起全量同步
    • Redis的策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步(自立门户,重新拜大哥,就可以再次全量同步)

      哨兵模式

      Sentinel是Redis的高可用性解决方案: 有一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。 相当于自动版的谋权篡位:有个哨兵在一致巡逻,突然发现老大挂了,小弟们就会自动投票,从众小弟中选出新的老大。

模拟测试:130主服务器,131和132是从服务器

  1. 在每台服务器中创建一个配置文件/usr/local/bin/sentinel.conf 名字不能改变,编辑 ```shell

    sentinel monitor 被监控的主机名(自定义) ip port 票数

    130服务器

    sentinel monitor redis130 172.16.150.130 6379 1

131服务器

sentinel monitor redis131 172.16.150.131 6379 1

132服务器

sentinel monitor redis132 172.16.150.132 6379 1


2. 启动Redis,注意启动顺序:主Redis ->从Redis  -> 哨兵1/2/3 (使用多开几个shell)
```shell
# 先启动主服务器的哨兵 然后在启动两个从库的哨兵
 redis-sentinel sentinel.conf

如下是主服务器的哨兵,可以看到两个从服务器的ip和端口信息
image.png

  1. 将主服务挂掉,看看会如何?

主服务器执行shutdown命令,后台自动发起激烈的投票,选举新的主服务器,保证既能写和读系统可运行高可用,观察哨兵命令行的输出.
132服务器的哨兵输出:
image.png
131服务器的哨兵输出:
image.png
可以看出131和132在争抢老大的位置。
在132服务器:info replication 查看角色已经成为了master,而131服务器成为了132服务器的从服务器
image.png
在131服务器查看角色:
image.png

  1. 如果之前的130老大归来了呢?

130再次归来自己成为了master,过了一段时间之后,被哨兵检测到130服务器的归来,因为已经有了132主服务器,130只能成为了132的从服务器了。
image.png
缺点

  • 由于所有的写操作都是在master完成的
  • 然后再同步到slave上,所以两台机器之间通信会有延迟
  • 当系统很繁忙的时候,延迟问题会加重
  • slave机器数量增加,问题也会加重(血脉相传来解决)

    redis.conf配置详解

    如下是对redis配置文件的一个翻译,主要是在工作中的时候经常会用到。
    通常情况下,默认配置足以解决问题
    没有极特殊的要求不要修改配置。
    image.png
    image.png
    image.png
    image.png
    image.png
    image.png
    image.png
    image.png
    image.png
    image.png