主从复制

Redis使用SLAVEOF命令实现主从复制

Redis 2.8 之前的旧版复制功能

旧版复制的具体过程

两个过程:

  1. 同步
  2. 命令转播
  • 同步:将从服务器的数据库状态更新至主服务器当前的数据库状态,具体命令SYNC
    复制旧版

    1. 从服务器发送SYNC命令给主服务器
    2. 主服务器收到SYNC命令,执行BGSAVE同时创建子进程生成RDB文件,并使用缓存区记录从当前时刻开始执行的命令
    3. 主服务器执行BGSAVE命令结束,由BGSAVE生成的RDB文件发送给从服务器,从服务器接收并载入RDB文件
    4. 主服务器将缓存区记录的命令发送给从服务器执行
  • 命令传播:同步完成后,主服务器接收的命令,同时传播给从服务器命令,使得主从服务器数据一致。

旧版复制的缺陷

主从复制包含两种情况:

  1. 初次复制:从服务器是一台新的服务器,或者之前的主服务器不是目前的主服务器。
  2. 断线后复制:处于命令传播的时候,如果出现网络波动问题,从服务器又重新连接了主服务器,那么就要重新执行SYNC复制完整的主服务器数据库状态。(缺陷)

Redis 2.8 之后的新版复制功能

新版复制的具体过程

新版复制包含三个操作:

  1. 同步
  2. 命令传播(同旧版命令传播)
  3. 心跳检测

同步

新版同步命令PSYNC,命令有两种模式

  1. 完整重同步
    用于初次复制,执行步骤和旧版sync命令一样。
  2. 部分重同步(解决断线后复制问题
    部分重同步不用重新执行完整SYNC命令,主服务器将从服务器断开期间的写命令,发送给从服务器,从服务器接收就可更新成相同状态。
    部分重同步
    具体实现关键点:
    1. 复制偏移量:主从服务器偏移量要一致,不一致则需要同步
      复制偏移量
    2. 复制积压缓冲区:主服务器在发送写命令的同时,发送到复制积压缓冲区队列一份。当从服务器偏移量不一致的时候,主服务器通过偏移量来决定从复制积压缓冲区队列的哪里开始同步。如果偏移量大于队列里的数据量,则需要完整重同步。
      复制积压缓冲区队列
      复制积压缓冲区
    3. 运行ID:
      1. 初次复制:主从服务器连接后,主服务器发送自己的ID给从服务器记录。
      2. 断线重连:从服务器连上主服务器之后,从服务器发送保存的主服务器ID,由主服务器判断:
        1. 与自己ID相等,主服务器继续执行部分重同步
        2. 与自己ID不相等,主服务器对其执行完整重同步

命令转播

命令传播和旧版相同,主服务器接收命令,把写命令发送给从服务器执行。同步主服务器的数据库状态。

心跳检测

命令传播阶段,从服务器以每秒一次的频率向主服务器发送命令,作用包括:

  1. 检测主从服务器网络连接是否正常
  2. 防止主服务器在不安全的情况下执行写操作
  3. 检测命令丢失

复制的具体实现步骤

  1. 设置主服务器地址和端口
  2. 建立套接字连接
    建立连接
  3. 发送ping命令
    PINg
  4. 身份验证
    身份验证
  5. 发送端口信息
    发送端口
  6. 同步
  7. 命令传播

哨兵(Sentinel)

什么是哨兵:

  • 哨兵是Redis为高可用性提供的解决方案。
  • 由一个或多个sentinel实例组成的Sentinel系统,可以监测任意多个主服务器、从服务器,并且在被监视的主服务器下线的时候,对该服务器进行故障转移,然后由新的主服务器代替下线的主服务器继续接收命令。
    哨兵系统

哨兵的初始化步骤

  1. 初始化普通Redis服务器
  2. 将普通的Redis服务器使用的代码替换成Sentinel专用代码
  3. 初始化Sentinel状态
  4. 根据给定的配置文件,初始化Sentinel监视的主服务器列表
  5. 创建连接主服务器的网络连接
    • Sentinel创建两个对主服务器进行的异步网络连接(因Sentinel需要与多个主服务器实例创建多个连接,所以使用异步连接)
      Sentin创建
      • 命令连接:用于向主服务器发送和接收命令
      • 订阅连接:用于订阅主服务器的频道,以便后续与其他Sentinel相认。
    • 当Sentinel发现主服务有新的从服务器时,Sentinel也会与其建立连接
      Sentinel创建

哨兵获取服务器状态

Sentinel默认每十秒一次,通过命令连接向服务器发送INFO命令,并通过分析回复获取服务器状态。

哨兵集群搭建

监视统一主从服务器的多个Sentinel可以通过与主从服务器建立的订阅连接互相发现对方。原理就时Sentinel向订阅频道里发送信息,同时订阅了相同频道里的Sentinel会发现对方。
哨兵集群

哨兵如何判断服务器下线

Sentinel通过每秒一次的向建立了命令连接的所有实例(主从服务器、其他Sentinel)发送PING命令,并通过PING命令的返回判断实例是否在线。

  • 如果在规定事件没有PING响应,则标记为主观下线
  • 因存在网络波动,短时间没有响应,所以为了减少误判,设计了客观下线(客观下线只针对主服务器)
  • 客观下线规则:需要3台以上的已经与该主服务器建立连接的Sentinel集群一起判断。
    • Sentinel1将主服务器标记主观下线,同时通过该主服务器的频道询问其他Sentinel,看他们是否标记主观下线
    • 当Sentinel1接收到足够多的已下线判断后(数量由Sentinel中的quorum属性,设置为Sentinel个数/2+1,如3个Sentinel设置为2),Sentinel1判断该主服务器客观下线,并对该主服务器进行故障转移。

选举领头哨兵(由领头哨兵进行故障转移)

从监视该主服务器的Sentinel中协商选举领头哨兵进行故障转移。

选择规则:

  1. 每次进行领头哨兵选举时,所有的哨兵配置纪元(内置的计数器)的值都自增1。
  2. 在一个配置纪元里,所有的哨兵都有一次设置某个哨兵为局部领头的机会,一旦设置成功,在该设置纪元无法更改。
    • 每个发现主服务器客观下线的哨兵都会要求其他哨兵把自己设置为局部领头
    • 哨兵局部领头的设置规则为先到先得,最先要求设置的设置成功,后面再要求的一律失败。
    • 如果某个哨兵被半数以上(n/2+1)的哨兵设置为局部领头,那么该哨兵称为领头。
  3. 如果在规定时间里没有选举出,那么将在一段时间后继续选举。

该算法是基于Raft算法的领头选举方法

为什么哨兵集群最少3个

因为在进行领头选举的时候,如果只有2个哨兵。要获得半数以上的票,就要2/2+1=2票,而不是1票。

如果哨兵集群中有哨兵下线,那么只有1票,永远无法选出领头哨兵,无法进行主从节点切换。

所以通常配置3个哨兵以上。

故障转移

步骤:

  1. 选出新的主服务器
  2. 通知从服务器修改复制目标
  3. 将旧主服务器降级成从服务器

集群(Cluster)

什么是集群

Redis提供的分布式数据库方案,通过分片进行数据共享,提供复制和故障转移功能。

集群里的每个Redis服务器表示为一个节点

每个节点存在两个结构:

  1. clusterNode:保存节点当前的状态
  2. clusterState 保存当前节点视角下,集群的状态,如集群的状态、配置纪元、节点数等

集群结构图

节点初始为单个节点,通过CLUSTER MEET命令将节点与节点连接,组成集群。

命令格式:

    CLUSTER MEET <ip> <port>

示例:现有三个节点,127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002

  1. 使用客户端连接7000发送cluster node命令查看7000节点当前端口
$ redis-cli -c -p 7000
127.0.0.1:7000> CLUSTER NODES
51549e625cfda318ad27423a31e7476fe3cd2939 :0 myself,master - 0 0 0 connected
  1. 发送命令将70017002加入7000所在集群
127.0.0.1:7000> CLUSTER MEET 127.0.0.1 7001
OK

127.0.0.1:7000> CLUSTER NODES
68eef66df23420a5862208ef5b1a7005b806f2ff 127.0.0.1:7001 master - 0 1388204746210 0 connected
51549e625cfda318ad27423a31e7476fe3cd2939 :0 myself,master - 0 0 0 connected

127.0.0.1:7000> CLUSTER MEET 127.0.0.1 7002
OK

127.0.0.1:7000> CLUSTER NODES
68eef66df23420a5862208ef5b1a7005b806f2ff 127.0.0.1:7001 master - 0 1388204848376 0 connected
9dfb4c4e016e627d9769e4c9bb0d4fa208e65c26 127.0.0.1:7002 master - 0 1388204847977 0 connected
51549e625cfda318ad27423a31e7476fe3cd2939 :0 myself,master - 0 0 0 connected

图片理解,节点之间握手连接:
集群组成
集群组成
集群组成
集群组成
集群组成

槽指派

什么是槽

集群通过分片来保存数据库中的键值对。

集群的整个数据库被分为2^14个槽,数据库中的每个键都属于一个槽,每个节点可以处理若干个槽。

集群使用哈希槽,方便添加和删除节点,只要把槽挪到新节点即可。

集群状态判断:

  • 上线状态:所有的槽都有节点在处理
  • 下线状态:只要有槽未被分配到节点处理

记录节点的槽指派信息

槽指派就是通过节点clusterNodeslot numslot属性指派该节点负责处理的槽。
集群槽指派

传播节点的槽指派信息

节点会把自己的slot数组通过消息发送给集群里的其他节点,告诉他们自己负责处理的槽。
集群传播槽指派

节点7001 7002接收到7000slot数组后,对自己的clusterState.slot数组进行更新。

clusterState.slot记录的集群中所有的槽指派信息。
集群槽指派信息

槽的数量为什么是2^14

slot数组是一个二进制数组,长度为2^14/8=2048字节(2KB)
集群槽数组

从业务层面,Redis不太可能扩展1000+主节点,节点越多,携带数据也越多,网络就会越拥堵,Redis不建议扩展超过1000节点。

在集群中执行命令(moved错误)

在客户端连接的当前节点执行命令时,如果键所在槽没有在指派在当前节点就会报moved错误。

引导客户端转向正确的节点,并发送命令。
集群moved错误

重新分片

什么是重新分片

把已经交给节点的槽,重新指派到另一个节点。并且槽所属的键也一同过去。

集群可以在线进行重新分片,源节点和目标节点都可以继续接收命令操作。

重新分片原理

由Redis集群管理软件redis-trib执行。

步骤:

  1. 目标节点准备好从源节点导入键值对
  2. 源节点准备好把键值对迁移
  3. 获得若干个属于槽的键值对的键名
  4. 根据键名,将键原子性的迁移到目标节点
  5. 重复3、4步骤,直到2准备的键全部迁移。
  6. 槽指派给目标节点

集群重新分片

ASK错误

在进行3、4步骤进行槽迁移的时候,一部分键值对在源节点,一部分键值对在目标节点。这时对源节点发送命令,操作的键值对就是正在迁移的键:

  • 源节点找到键 -> 直接执行命令
  • 源节点未找到键 -> 可能被迁至目标节点了,返回ASK错误,并引导客户端转向目标节点,再次发送命令。

集群ASK错误

引导转向并发送ASKING 命令 ,然后再执行命令。 集群ASK错误转向

注意:如果没有发送ASKING,直接执行命令,因为槽指派还是源节点,那么就会返回moved错误。

集群重分片槽指派

moved错误和ASK错误区别

  • moved代表槽指派已经更变,在第一次报出moved错误转向目标节点后,以后所有都会直接把命令发送到目标节点。
  • ask错误只是在迁移发生的时候使用的临时措施。不会因报出一次ASK错误下次就会改变发送对象。

集群中的主从复制

主节点处理槽,从节点复制主节点。如果主节点下线,代替成为主节点继续接收命令。

集群中如何判断节点是否下线

集群里每个节点定期向其他节点发送PING,检测对方是否在线。

  • 如果PING无返回,或者返回超时。那么发送PING的节点就会把接收PING的节点标记疑似下线

    当A节点通过消息得知B节点标记了C节点为疑似下线。就会在自己的clusterState.nodes里找到C节点的结构,并在结构里的fail_reports链表里添加下线报告
    下线报告

  • 如果半数以上操作槽的主节点都标记为疑似下线,那么就会被标记为已下线
    将主节点标记为已下线的节点会向集群广播节点下线。所有收到广播的节点都把节点标记为已下线。
    节点下线

故障转移

从节点发现主节点下线,对主节点进行故障转移。

步骤:

  1. 选出新的主服务器成为主节点

    基于Raft算法的领头选举

    1. 开始故障转移,节点配置纪元+1
    2. 每个纪元里,负责处理槽的主节点都有一次投票
      • 从节点发现主节点下线,广播让有投票权的主节点为自己投票
      • 投票规则:先到先得。
      • 如果有半数以上的票,则成为主节点。
    3. 如果该纪元选举失败,那么集群进入新纪元,重新选举,直到选出新主节点。
  2. 新主节点撤销所有旧主节点的槽指派并把槽指派指向自己
  3. 新主节点向集群广播自己成为主节点操作处理槽
  4. 新主节点接收命令处理操作负责的槽

一致性哈希

Redis中引用的一致性哈希思想,但是没有直接引用,而是引入了哈希槽的概念。

简单哈希

问题在于,集群扩容的时候,length发生变化,大部分数据重新hash导致被分配到其他机器上,这样操作导致服务器在一定时间不可用,每次扩容都会存在这个问题。导致缓存雪崩。

一致性哈希

一致性hash算法主要应用于分布式存储系统。

一致性hash算法构建了一个虚拟的哈希环。
哈希环
按顺时针方向从0开始到2^32-1。

将各个服务器进行哈希,具体使用服务器主机名,作为key进行哈希,确定在哈希环上的位置。
哈希环节点

插入key-value数据的时候,对key进行哈希,然后放入对应位置,顺时针找到第一个服务器存储。
哈希环存储

容错性和可扩展性

假设node2宕机,只有key-b受到影响,会接着顺时针找到第一个服务器node3进行存储。

假设新增一台node4服务器,受影响的只有node1和node4之间的key-b。 哈希环删除

总结:在一致性哈希中,新增或者下机一台服务器,受影响的只有该服务器和前一服务器之间的数据。具有较好的容错性和可扩展性。

数据倾斜

一致性哈希在服务器数很少的时候,容易因为节点分布不均导致有的服务器数据量很大,而有的服务器仅仅只有少量数据。

示例:node2数据量很大,而node1很小。
哈希环数据倾斜

为了解决,一致性哈希引入了虚拟节点机制:为每个服务器计算多个哈希,每个计算的位置都放一个虚拟节点。可用node#1来表示。如图:
哈希环数据倾斜

这样只是多了一个对实际节点的映射。

Redis为什么不用一致性哈希(缓存雪崩)

  • 无法很好的手动设置数据分布,哈希槽则可以灵活的配置每个节占用的哈希槽数量。
  • 缓存雪崩:一致性哈希环里如果有节点承受不住数据量,就会宕机,数据传给下一个节点,下一节点也承受不住宕机,数据就滚雪球越来越大,最终集群雪崩。
  • 一致性哈希比哈希槽更复杂。

如何解决集群数据丢失问题

Redis数据丢失的两种情况

  1. 异步复制导致丢失
  2. 集群脑裂导致丢失
    • 主节点发生了假故障,与哨兵失联,但是客户端正常。这时客户端发送命令,因网络问题无法同步给从服务器。
    • 哨兵选举新主服务器,集群有两个主服务器——集群脑裂
    • 网络好了,旧主服务器降级,清空自己的数据,复制新主服务器的数据,但是,在一开始客户端写入的命令就会丢失,导致数据丢失。

解决方法

通过参数设置,限制主服务器在假故障的时候接收客户端命令。

主从复制、哨兵、集群的区别

主从复制为了数据备份
哨兵为了高可用
集群为了在单实例环境下,分散压力

  • 主从模式:备份数据,负载均衡,读写分离,主服务器可以有多个从服务器。
  • 哨兵模式:监控,故障转移,哨兵发现主服务器挂了,就会在从服务器中重新选举主服务器。
  • 集群模式:解决单机Redis容量有限问题,将数据分给多个服务器。