Redis持久化、主从模式、哨兵模式、分片集群

40 阅读20分钟

一、单机Redis存在的问题

单机Redis存在的问题

  • 数据持久化问题

    Redis为了提高性能,数据是在内存中存储的。而内存不能持久化存储数据,所以Redis提供了持久化机制

  • 并发能力问题

    单机Redis的并发能力有限,可以通过主从集群来实现读写分离,从而提高并发能力

  • 故障恢复问题

    在Redis集群中,一旦某个节点宕机,就必须要手动进行故障转换与恢复。Redis提供了哨兵模式来实现自动故障恢复

  • 存储能力问题

    无论是主从模式集群,还是哨兵的集群,都没有解决存储能力的扩展问题。Redis提供了分片集群模式,可以很方便的动态扩容

二、Redis持久化

1. 持久化介绍

所谓持久化,就是将数据永久保存。Redis是一个内存数据库,它的所有的数据都在内存中,所以有极高的读写性能,但是内存中的数据都是临时的,所以一旦Redis进程结束,所有的数据就会清空。

为了防止这样的问题,Redis提供了持久化机制:

  • RDB模式:快照模式,默认是开启状态
  • AOF模式:日志模式

2. RDB模式

RDB,Redis Database Backup file,称为Redis数据快照。

快照文件,也称RDB文件,默认保存在当前运行目录(在哪个目录里启动Redis服务,就保存在哪个目录)

2.1 如何持久化的?

当执行RDB持久化时,会把Redis内存中的数据全部都保存到磁盘文件上;当Redis服务重启时,就会从磁盘上加载文件,把数据恢复到内存中

2.2 什么时候持久化?

在一下情况下,会执行RDB快照持久化:

  • 执行save命令
  • 执行bgsave命令
  • Redis服务正常关闭
  • 触发自动RDB的条件

2.2.1 save命令

当执行save命令时,Redis会执行一次RDB持久化。但是save命令是一个阻塞式命令:

  • 它由Redis主进程执行RDB持久化,在持久化过程中Redis将不能处理客户端的操作,知道RDB执行完毕
  • 如果Redis中数据非常多,持久化将会花费比较长的时间,Redis也将会阻塞比较长的时间
  • 目前,save命令已经很少使用了,通常使用bgsave代替它

image-20240506175426955.png

2.2.2 bgsave命令

当执行bgsave命令时,Redis会执行一次RDB持久化。

而bgsave是非阻塞的、异步式的:

  • Redis会fork一个子进程,由子进程负责RDB持久化
  • 主进程负责处理客户端的操作命令

使用redis-cli连接Redis服务之后,执行命令:bgsave,就会执行一次RDB持久化

127.0.0.1:6379> bgsave
Background saving started
127.0.0.1:6379> 

image-20240506180750283.png

2.2.3 Redis服务关闭

当结束Redis服务时,Redis会先执行一次save命令,把内存中的数据进行RDB持久化。

关闭Redis服务的方式有很多,可以:

  • 在redis-server服务进程中,按ctrl + c,立即结束redis服务
  • 在redis-cli客户端中,执行命令shutdown,关闭redis服务
  • 或者直接执行Linux命令redis-cli shutdown命令

image-20240506181104218.png

2.2.4 RDB自动触发

Redis的RDB模式提供了默认的自动触发机制,相关的配置参数在配置文件redis.conf中,默认值如下:

  • 如果3600秒内有1次数据变更,就执行一次RDB
  • 如果300秒内有100次数据变更,就执行一次RDB
  • 如果60秒内有10000次数据变更,就执行一次RDB
save 3600 1
save 300 100
save 60 10000

提示:如果写成 save "",表示禁用RDB

2.3 bgsave原理

默认情况下,Redis服务只有一个进程,由这个进程负责处理客户端的一切操作请求。

当执行bgsave时:

  • 会fork一个子进程,会拷贝主进程里的页表,然后子进程就可以通过页表读取物理内存里的数据,保存到磁盘文件上
  • 同时,为了防止子进程RDB过程中,物理内存里的数据被修改:会把物理内存里的数据设置为read-only只读。子进程RDB时,数据就不会被修改
  • 如果这时有主进程执行了新的命令要修改数据:物理内存里的数据是只读的,Redis会采用copy-on-write机制,把数据拷贝一个副本,对这个数据副本进行读写操作
  • 当子进程RDB结束之后,会丢弃旧版本的数据,保留最新的副本数据

图示如下:

image-20210725151319695.png

3. AOF模式

AOF,Append Only File。也有人称为“日志模式”。

3.1 如何持久化的?

当执行AOF持久化时,会把执行的每个数据变更的命令追加保存到磁盘文件里。当AOF恢复数据时,会读取磁盘文件,按顺序依次执行所有的命令,恢复数据。

3.2 什么时候持久化?

3.2.1 开启AOP

Redis的AOF默认是关闭状态的,如果要开启AOF模式,则需要:

  1. 修改redis.conf配置文件

    appendonly yes
    # AOF文件的名称
    appendfilename "appendonly.aof"
    
    • appendonly:AOF模式的开关。如果值是yes,表示开启;如果是no,表示关闭

    • appendfilename:AOF文件的名称。默认是appendonly.aof,可以自定义文件名

  2. 重启Redis服务

    先关闭Redis服务,执行命令:redis-cli shutdown

    再重启Redis服务,执行命令:redis-server redis.conf

3.2.2 执行时机

开启AOF模式之后,在redis.conf中配置有的AOF持久化执行时机,可通过appendfsync参数进行配置

#appendfsync always 
appendfsync everysec 
#appendfsync no
  • 如果值为always:同步刷盘,每次的写数据的命令,就立即追加到aof文件中
  • 如果值为everysec:每次的写命令先放到AOF缓冲区,然后以每秒一次的频率,把缓冲区里的命令保存到aof文件
  • 如果值为no:每次的写命令先放到AOF缓冲区,然后完全由操作系统决定何时 把缓冲区里命令保存到aof文件

三种时机的对比:

配置项刷新时机优点缺点
always同步刷盘可靠性高,几乎不丢数据·对性能影响大
everysec每秒刷盘性能适中最多丢失1秒的数据
no由操作系统控制性能最好可靠性较差,可能丢失大量数据

刷盘:把缓冲区里的数据存储到磁盘上,称为刷盘。 flush

3.3 AOF文件重写

3.3.1 AOF冗余记录

因为AOF保存的是写命令,类似于日志文件,所以通常情况下AOF日志文件都比RDB文件大,且随着使用时间的推移,AOF文件通常会越来越大,从而导致:

  • 磁盘空间的占用越来越大
  • 恢复数据的速度越来越慢

主要原因在于,AOF忠实的记录每次写操作的命令,就可能有大量重复的命令;比如对同一key有多次修改操作,每次修改操作都会记录到AOF文件里,而实际上只有最后一次操作命令才有效。假如先后执行以下命令:

set num 1
set num 10
set num 100
set num 1

那么AOF文件里会保存4条命令,实际上只需要最后一条命令就可以了

3.3.2 bgrewriteaof重写

手动执行AOF重写

通过在redis-cli中执行bgrewriteaof命令,可以让AOF文件执行重写:

  1. 删除无效的命令。例如:set a A, set a B, del a三个命令,其实全部都可以删除掉
  2. 合并多余的命令。例如:

image-20210725151729118.png

自动触发AOF重写

Redis也会在触发阈值时自动去重写AOF文件,阈值的配置在redis.conf文件中

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
  • auto-aof-rewrite-min-size:AOF文件必须大于这个值,才可能触发AOF重写
  • auto-aof-rewrite-percentage:当AOF文件 与 上次重写后的文件对比,超过这个百分比后,会触发AOF重写

4. RDB和AOF对比

RDB和AOF两种持久化模式各有各的优缺点。在实际开发中往往是两者结合使用

对比项RDBAOF
持久化方式定时对整个内存的数据做快照记录每次执行的写命令
数据完整性两次备份之间数据可能丢失丢失数据的可能性较小,取决于刷盘策略
文件大小相对较小,且有RDB压缩记录命令,文件相对较大
恢复速度很快
恢复时优先级
系统资源占用高,大量CPU和内存消耗低,主要是磁盘IO较高
但AOF重写时会占用较高的CPU和内存
使用场景可能容忍数分钟内的数据丢失,
追求更快的启动速度
对数据安全性要求较高时使用

三、Redis主从模式

1. 主从模式介绍

单节点的Redis服务支持的并发是有限的:所有客户端的请求全部冲击到仅有的一台服务器上,服务器可能因为压力过大而阻塞不能及时响应

单节点的Redis,只有一个节点,一旦节点宕机,整个服务不可用

所以可以采用主从架构,做到**读写分离**:多个Redis节点共同提供服务,所有写数据的操作都请求到主服务器,所有读数据的操作都请求到从服务器。

image-20210725152037611.png

如上图所示,集群中有一个master节点、两个slave节点(现在叫replica)。当我们通过Redis的Java客户端访问主从集群时,应该做好路由:

  • 如果是写操作,应该访问master节点,master会自动将数据同步给两个slave节点
  • 如果是读操作,建议访问各个slave节点,从而分担并发压力

2. 主从数据同步原理

2.1 全量同步

发生在什么时候:一个节点第一次变成slave的时候,先进行一次全量同步

全量同步过程

  • 第一阶段:建立主从关系
    • slave把自己数据集的id(replication id)发送给master
    • master收到以后判断 slave 给的 replication id 和自己的 replication id是否相同。不同说明slave是第一次连接master
    • master会首先把自己的replication id发送给slave;slave丢弃自己的replication id,接受master replication id
  • 第二阶段:开始全量同步
    • master执行bgsave,生成RDB。把RDB发送给slave
    • slave丢弃自己的所有的旧数据,加载master提供的RDB
  • 第三阶段:后续的增量同步
    • 在第二阶段全量同步期间,master所执行的新命令会存储到repl_backlog里
    • master在第二阶段结束,会把repl_backlog里的命令发送给slave
    • slave再执行接收到的命令。最终slave的数据和master同步了

流程图如下:

image-20210725152700914.png

2.2 增量同步

全量同步需要先做RDB,然后将RDB文件通过网络传输个slave,成本太高了。因此除了第一次做全量同步,其它大多数时候slave与master都是做增量同步。什么是增量同步?就是只更新slave与master存在差异的部分数据。

增量同步的过程

slave向master发请求,要求同步数据,会给master发送自己的replication id,和上次同步的位置offset

master判断replication id和自己的相同,再判断offset和自己master的offset

  • 如果slave的offset 与 master的offset 相差超过一圈:就只能全量同步了
  • 如果slave的offset 与master的offset 相差不超过一圈:就从slave的offset开始,把后边的所有命令同步给slave,是增量同步

2.3 repl_backlog原理

master怎么知道slave与自己的数据差异在哪里呢?

这就要说到全量同步时的repl_baklog文件了。这个文件是一个固定大小的数组,只不过数组是环形,也就是说角标到达数组末尾后,会再次从0开始读写,这样数组头部的数据就会被覆盖。

repl_backlog实现增量复制的过程

1) Master把写命令存储到repl_backlog里

repl_backlog中会记录Redis处理过的写命令日志及offset,包括master当前的offset,和slave已经拷贝到的offset。slave与master的offset之间的差异,就是salve需要增量拷贝的数据了。

image-20210725153359022.png

2) Slave同步repl_backlog中的命令

随着不断有数据写入,master的offset逐渐变大。

slave也不断的读取repl_backlog中的命令,同步到slave中,追赶master的offset:

image-20210725153524190.png

3) repl_backlog填满后环绕从头开始

直到repl_backlog数组被完全填满,如果再有新的数据写入,就会覆盖数组中的旧数据。不过,旧的数据只要是绿色的,说明是已经被同步到slave的数据,即便被覆盖了也没什么影响。因为未同步的仅仅是红色部分。

image-20210725153715910.png

4) Slave落后过多后只能全量同步

但是,如果slave出现网络阻塞,导致master的offset远远超过了slave的offset;如果master继续写入新数据,其offset就会覆盖旧的数据,直到将slave现在的offset也覆盖:

image-20210725153937031.png image-20210725154155984.png

棕色框中的红色部分,就是尚未同步,但是却已经被覆盖的数据。此时如果slave恢复,需要同步,却发现自己的offset都没有了,无法完成增量同步了。只能做全量同步。

注意:repl_backlog大小有上限,写满后会覆盖最早的数据。如果slave断开时间过长,导致尚未备份的数据被覆盖,则无法基于log做增量同步,只能再次进行全量同步

2.4 主从同步优化

主从同步可以保证主从数据的一致性,非常重要。

可以从以下几个方面来优化Redis主从就集群:

  • Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO
  • 在master中配置repl-diskless-sync yes启用无磁盘复制,避免全量同步时的磁盘IO
  • 适当提高repl_backlog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步
  • 限制一个master上的slave节点数量,如果实在是太多slave,则可以采用主-从-从链式结构,减少master压力

主从从架构图:

image-20210725154405899.png

四、Redis哨兵模式

1. 哨兵模式介绍

主从集群存在的问题:故障转移的问题,一旦Master节点宕机,就需要人工干预切换Master主节点。可以增加哨兵解决问题

2. 哨兵模式的架构原理

02.redis哨兵集群.jpeg

2.1 哨兵的作用

  • 监控:监控主从节点的健康状态
  • 故障转移:当主节点宕机时,由哨兵负责选出新的Master
  • 通知:通知整个集群有新的Master,构建新的主从关系

2.2 集群监控原理

Sentinel基于心跳机制监测服务状态,定时(每秒1次)向集群的每个实例发送ping命令:

  • 主观下线:如果某sentinel节点发现某实例未在规定时间(通过参数配置)响应,则认为该实例主观下线

  • 客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。

image-20210725154632354.png

2.3 集群故障恢复原理

选举原则

一旦发现master故障,sentinel需要在salve中选择一个作为新的master,选择依据是这样的:

  • 首先会判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds * 10)则会排除该slave节点
  • 然后判断slave节点的slave-priority值(通过参数配置),越小优先级越高,如果是0则永不参与选举
  • 如果slave-prority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高
  • 最后是判断slave节点的runid大小,越小优先级越高(越小表示redis实例启动的越早)。

切换Master

当选出一个新的master后,该如何实现切换呢?

流程如下:

  • sentinel给备选的slave节点发送slaveof no one命令,让该节点成为master
  • sentinel给所有其它slave发送slaveof 命令,让这些slave成为新master的从节点,开始从新的master上同步数据。
  • 最后,sentinel将故障节点标记为slave,当故障节点恢复后会自动成为新的master的slave节点

image-20210725154816841.png

五、Redis分片集群

1. 分片集群介绍

主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:

  • 主从集群只有一个master具备写数据的能力,存在高并写问题
  • 主从集群里各节点数据相同,如果数据太多,就存不下,存在海量数据存储问题

使用分片集群可以解决上述问题

2. 分片集群的架构

image-20210725155747294.png

分片集群特征:

  • 集群里可以有多个master主节点,每个主节点存储一部分数据

  • 每个master节点可以有多个slave节点,实现高可用

  • 分片集群不需要再有哨兵监控和选举,而是master之间互相心跳监控、实现选举

  • 连接任意一个master,master都会帮我们重定向到正确的master上存取数据

3. 散列插槽(Hash插槽)

Redis会把每一个master节点映射到0~16383共16384个插槽(hash slot)上,查看集群信息时就能看到:

image-20230519203045641.png

数据key不是与节点绑定,而是与插槽绑定。redis会根据key的有效部分计算插槽值,分两种情况:

  • 如果key里没有{}crc16算法(key) % 16384 结果就是最终的hash值。根据hash值判断存储到哪个节点
  • 如果key里有{}crc16算法({}里的内容)%16384结果就是最终的hash值。根据hash值判断存储到哪个节点

例如:key是num,那么就根据num计算;如果是{order}num,则根据order计算。计算方式是利用CRC16算法得到一个hash值,然后对16384取余,得到的结果就是slot值。

image-20230519203448421.png

提问:如何将一批key固定的保存在同一个Redis实例上呢?

答案:可以给这些数据的key,增加相同的{标识}。例如这些key都以{typeId}为前缀

查看key的slot值:cluster keyslot key

hash插槽:数据和节点之间没有直接的关联关系,方便进行集群的伸缩与数据的转移

4. 集群伸缩

因为Redis的分片集群,数据并没有与Redis节点直接进行绑定,而是:数据与插槽绑定,插槽与Redis节点绑定。这样就可以很方便的进行集群的伸缩,增加或减少Redis节点。

我们通过下面例子,演示一下给集群增加节点,需要如何实现。要求:向集群中添加一个新的master节点,并向其中存储num = 10

  1. 启动一个新的Redis实例,端口为7383
  2. 把7383节点添加到之前的集群,并作为一个master节点
  3. 给7383节点分配插槽,使得这个num可以存储到7383实例

这需要有两大步:

  1. 添加一个节点到集群中
  2. 转移插槽

4.1 添加新节点到集群中

创建7383节点

#创建7383文件夹
mkdir ~/04cluster/7383
#把配置文件拷贝到7383文件夹里
cp ~/04cluster/7380/redis.conf ~/04cluster/7383
#修改配置文件
sed -i s/7380/7383/g ~/04cluster/7383/redis.conf
#启动7383实例
redis-server ~/04cluster/7383/redis.conf

添加到集群

查看redis集群操作帮助: redis-cli --cluster help

执行命令:

#把7383节点添加到集群
redis-cli --cluster add-node  192.168.150.132:7383 192.168.150.132:7380
#查看集群状态
redis-cli -p 7380 cluster nodes

image-20230519211944742.png

4.2 转移插槽

查看num的插槽

我们要将num存储到7383节点,因此需要先看看num的插槽是多少:redis-cli -c -p 7380 cluster keyslot 键

image-20231015171905713.png

我们可以将0~3000插槽,从7380节点移动到7383节点上

转移插槽

转换插槽的命令,操作如下:

  1. 建立连接:redis-cli --cluster reshard 192.168.200.136:7380

image-20230519212701597.png

  1. 输入要移动的插槽数量,我们输入 3000

image-20230519212721450.png

  1. 输入目标节点的id(哪个节点要接收这些插槽,就输入哪个节点的id)

    我们从刚刚的输出结果中,找到7383节点的id设置过来

image-20230519212900551.png

  1. 从哪个节点里转移出插槽?

    输入all,表示全部。即从现有每个master节点中各转移一部分出来

    输入节点id,表示从指定节点中转移出来

    输入done,表示输入完毕了

    我们这里从7380节点转移出来一部分,要输入7380节点的id

image-20230519213305108.png

  1. 输入yes,确认转移

image-20230519213355940.png

确认结果

执行命令:redis-cli -p 7380 cluster nodes,可以看到7383节点拥有0~2999插槽,说明转移插槽成功了

image-20230519213531816.png

5. 故障转移

当master宕机时,Redis的分片集群同样具备故障转移的能力。

接下来给大家演示一下分片集群的故障转换,先确认一下集群的初始状态:

  • 7380是master节点,8382是其slave节点
  • 7381是master节点,8380是其slave节点
  • 7382是master节点,8381是其slave节点
  • 7383是master节点,没有slave节点

image-20230519213856015.png

5.1 自动故障转移

当master宕机时,分片集群会自动将其slave节点提升为master。

我们直接将7381节点关闭:redis-cli -p 7381 shutdown

然后查看集群状态 redis-cli -p 7380 cluster nodes,发现7381是fail状态,8380已经提升成为master

image-20230519214415929.png

再次启动7381节点:redis-server 7381/redis.conf

再次查看集群状态:redis-cli -p 7380 cluster nodes,发现7381节点成为了slave

image-20230519214717422.png

5.2 手动故障转移

利用cluster failover命令可以手动让集群中的某个master宕机,切换到执行cluster failover命令的这个slave节点,实现无感知的数据迁移。

这种failover命令可以指定三种模式:

  • 缺省:默认的流程,如图1~6歩
  • force:省略了对offset的一致性校验
  • takeover:直接执行第5歩,忽略数据一致性、忽略master状态和其它master的意见

其流程如下:

image-20210725162441407.png

我们以7381这个slave节点为例,演示如何手动进行故障转移,让7381重新夺回master:

  1. 利用redis-cli连接7381这个节点:redis-cli -p 7381
  2. 在7381节点上执行cluster failover命令
  3. 重新查看集群状态:cluster nodes

image-20230519215650823.png