Redis设计与实现读书笔记

复制

概述

  • 复制:是将某事物通过某种方式制作成相同的一份或多份的行为(维基百科)
  • redis的复制就是让一个服务器(从)通过某种方式去获得另一台服务器(主)的数据

Redis-2.8版本旧版复制

  • 旧版复制有同步(sync)和命令传播(command propagate)操作
  • 怎么保证主服务器和从服务器数据一致呢?那就首先主服务器把所有的数据都给从服务器都发一份,这样在某个时间点数据就保证一致了。但是主服务器肯定还会写入啊,一写入就又不一致了,那就主服务器把之后写入的那些命令实时同步给从服务器(增量发送),这样又一致啦。

同步

  • SYNC命令
  • 作用:把主服务器所有的数据都给从服务器发一份
主从服务器交互步骤
  • 从服务器收到SLAVEOF命令后,会给主发送SYNC命令
  • 主服务器收到SYNC命令,执行BGSAVE命令,利用fork()来生成一个RDB文件
  • 主服务器使用一个缓冲区记录从BGSAVE开始执行的所有写命令
    • 生成的RDB文件只是执行BGSAVE那个时间点的,万一在命令执行过程中,或者传给从服务器之前有命令写入,尽管把RDB文件给了从服务器,还是不一致
  • 主服务器生成完RDB文件,把RDB文件给从服务器,从服务器载入这个RDB文件
  • 主服务器把缓冲区的写命令发给从服务器,从服务器执行这些写命令使得保持一致

命令传播

  • 作用:主一直会有命令写入,这些命令会造成主从不一致,所以主服务器需要把一些会造成不一致实时的传播给从服务器,让从服务器也执行保持一致

旧版复制缺陷

  • 断线后重复制
    • 主从正常运行,忽然网络抖动,从服务器断开了5秒,5秒后重新连上
    • 由于在这断开的5秒期间也会有数据写入,主从不一致了,从服务器需要采取某些措施,让主从再次保持一致。
    • 最直观的想法,从服务器断开了5秒,我把丢失的5秒发给从服务器就可以了啊。
    • 当旧版的复制从断开重连后,会向主服务器发送SYNC命令,前面说了SYNC命令会生成RDB,全量同步一遍,只断了5秒,非得给我来一波全量同步,开销极大。

新版复制

  • 老版本断线重连后,得SYNC,重新同步RDB文件,低效!Redis2.8版本开始使用PSYNC命令代替SYNC命令
  • 同样思路还是没变,先全量同步一次,然后增量同步。
  • 全量同步在2.8叫完整重同步 , 增量同步叫部分重同步

完整重同步

  • 和老版本一样,主服务器生成和发送RDB文件,发送缓冲区中的命令,主从保持一致。

部分重同步

部分重同步功能主要由以下三个部分构成:

  • 主服务器的复制偏移量和从服务器的复制偏移量
  • 主服务器的复制积压缓冲区
  • 服务器的运行ID(run ID)
复制偏移量
  • 主服务器在向从服务器传播N个字节的数据时,自己的复制偏移量就加上N
  • 从服务器收到主服务器传播来的N个字节的数据时,就将自己的复制偏移量加上N
复制积压缓冲区
  • 一个固定长度的先进先出FIFO队列 , 默认1MB
  • 固定长度是指如果大小满了,那么后面的数据就会把前面的数据给顶替掉
  • 将数据传播给所有从服务器时,还会将写命令入队到复制积压缓冲区
  • 缓冲区中还维护了每个字节的偏移量
运行ID
  • 每个Redis服务器,无论是主服务器还是从服务器,都有自己的运行ID
  • 运行ID在服务器启动时自动生成,由40个随机的十六进制字符组成。
  • 当从服务器对主服务器进行初次复制时,主服务器会把自己的运行ID传给从服务器,从服务器会把这个运行ID保存起来
部分重同步步骤
  • 从服务器把自己的offset给主服务器 , 比如是从100开始断的 , 把100告诉主
  • 主服务器已经到150了 , 主服务器会先去看100及其之后的偏移量是不是在自己的复制积压缓冲区内,如果在就执行部分重同步操作。如果不在就需要执行完整重同步。
  • 所以根据业务场景来调整复制积压缓冲区的大小非常重要哦 , repl-backlog-size来进行调整

PSYNC命令完整步骤

  • 如果从服务器之前没有复制过任何主服务器,那么从服务器开始时会向主服务器发送PSYNC ? -1
  • 如果从服务器之前复制主服务器,那么就发送PSYNC <runid> <offset> runid就是上次保存的主服务器的runid , offset表示上次复制到的偏移量
  • 如果主服务器发现runid跟自己对不上,说明这个从上次复制的不是自己,主服务器返回 + FULLRESYNC <runid> <offset>执行完整重同步
  • 如果主服务器发现runid对得上,看看offset及其之后的数据是否在复制积压缓冲区内,如果在,返回+CONTINUE执行部分重同步 , 如果不在执行完整重同步
  • 执行完整重同步,从服务器会把主服务器发送过来的runid和offset保存起来

复制的实现

客户端执行SLAVEOF master_ip master_port

  • 从服务器收到命令后,将ip + port保存起来,返回OK,异步执行复制
  • 从服务器向主服务器建立套接字连接
  • 从服务器给主服务器发送PING命令
    • 检查套接字读写状态是否正常
    • 检查主服务器目前是否可以正常处理命令请求
  • 主服务器如果可以正常运行,返回PONG,如果暂时没法处理从服务器的命令请求,就返回一个错误,从会断开重连
  • 身份验证
  • 从服务器告诉主服务器自己的端口
  • 执行PSYNC命令
  • 命令传播

心跳检测

  • 在命令传播阶段,从服务器默认每秒一次的频率,向主服务器发送Reolconf ACK offset
  • 如果命令传播丢失了,主服务器可以根据心跳中的offset进行数据补发

Sentinel

  • 主从复制存在的问题:如果主下线了,整个集群将处于崩溃状态。需要有个角色来帮我们及时发现有主服务区下线了,并且及时让下面的从服务器变为新的主服务器,这个角色就是sentinel(哨兵)

启动sentinel

通过命令 redis-sentinel /path/to/you/sentinel.conf启动

  • sentinel本质是运行在特殊模式下的redis服务器
  • 启动的时候会把一部分普通redis服务器代码替换成Sentinel专用代码
  • 启动时需要初始化SentinelState

初始化SentinelState

初始化masters属性
  • dict *master
  • masters属性记录了所有被Sentinel监视的主服务器信息
  • sentinel会载入用户指定的配置文件,将主服务器的名字当作key , 并且创建一个 sentinelRedisInstance 结构当作value,配置文件可以使用如下模版
  • 初始化后的masters属性
获取主服务器信息
  • 上一步已经初始化了masters属性,所以知道了master的ip和port,这样就可以建立连接了(建立两个连接,命令连接和订阅连接)
  • sentinel默认会以每十秒一次的频率,向主服务器发送info命令
  • info命令中会有主的一些信息,比如runid , 更新到对应的instance结构体中
  • info命令中会有所有从服务器的信息,把从服务器的信息保存到主服务器的实例结构中的slves字典中 (如果存在就更新,不存在就创建)
    • key:ip:port
    • value:也是sentinelRedisInstance类型,不过flags会区分这个实例是主还是从
获取从服务器信息
  • 根据主服务器发现了所有的从服务器之后,sentinel会为所有的从服务器建立连接。
  • 建立连接之后,sentinel会每10秒发送一次info命令,更新从服务器的实例结构,比如runid
更新sentinels字典
  • 主服务器和从服务器都建立连接后,Sentinel默认以每两秒一次的频率,向每个服务器的__sentinel__:hello频道中发送自己本身sentinel的信息和监视的主服务器信息
  • sentinel同时又会订阅__sentinel__:hello频道 , 所以如果有sentinel1 , sentinel2 , sentinel3 同时监控一个服务器,sentinel1往频道发送一条消息,sentinel2,sentinel3是能收到这条消息的
  • sentinel收到消息后,会先根据runid检查是否是自己发的,如果自己发的就直接丢弃即可,如果不是自己发的,就会开始更新sentinels字典(sentinels字典位于主服务器的实例结构中)
  • sentinels字典的key是一个sentinel的ip port 格式ip:port
  • sentinels字典的value就是对应的sentinel实例结构
  • 首先接收到别的sentinel消息的sentinel,会先根据发送的主服务器信息,去master找到对应的主服务器实例结构,然后去主服务器实例结构的sentinels字典中查找是否有sentinel实例,没有就创建,有则更新。
  • sentinel可以通过接收频道信息来获知其他Sentinel的存在,监视同一个主服务器的多个Sentinel可以自动发现对方 , 并且互相建立连接进行通信

检查主观下线状态

  • 初始化之后,sentinel与监视的主服务器,主服务器下面的从服务器,监视同个主服务器的其他sentinel都建立了连接。
  • Sentinel会以每秒一次的频率向所有与它创建了连接的实例发送PING命令
  • 如果在down-after-milliseconds,实例连续向sentinel返回无效恢复,那么sentinel就会把这个实例标识为主观下线状态
    • 无效回复为除返回 +PONG , -LOADING , -MASTERDOWN三种回复之外的其他回复或者指定时限内没有返回任何回复

检查客观下线状态

  • sentinel向其他监控主服务器的sentinel询问,看看其他人是否也认为主服务器已经进入了下线状态
  • 其他sentinel会将自己是否认为主服务器已经下线回复回去
  • 当认为主服务器已经下线的数量到达了配置指定的数量时,sentinel就会将这个主服务器在自己的实力结构中标识为客观下线状态。

选举领头Sentinel

  • 当一个主服务器被判断为客观下线,监视这个下线主服务器的各个Sentinel会进行协商,选举出一个领头Sentinel。Raft协议

故障转移

  • 领头的Sentinel将对已经下线的主服务器执行故障转移操作。
  • 从主服务器属下的从服务器选择一个新的做为主服务器。
  • 其他从服务器开始复制新的主服务器。
  • 监视旧的主服务器,如果重新上线了,就会让它成为新的主服务器的从服务器。

总结

集群

节点

  • 刚开始每个节点都是相互独立的,为了组建集群,需要将各个独立的节点连接起来。
  • 使用CLUSTER MEET <ip> <port> 让各个节点连接起来,当向一个节点发送这个命令时,这个节点会跟指定的ip + port(目标节点)进行握手,握手成功后,就会让目标节点加入自己的集群
  • 每个节点都会保存一个clusterState结构,这个结构记录了在当前节点的视角下,集群目前所处的状态,集群包含多少个节点,集群当前的配置纪元等
    • clusterState.nodes : 集群节点字典,key:节点的名字 value : 节点的实例(clusterNode)

CLUSTER MEET命令的实现

  • 给A节点发送 cluster meet ip port , ip 和 port 为B节点
  • A节点创建一个B节点的clusterNode结构,放入clusterState.nodes字典中。
  • A节点和B节点建立连接,给B节点发送MEET消息
  • B节点收到MEET消息,在自己的clusterNode.nodes字典中加入A节点
  • B节点返回PONG消息
  • A节点返回一条PING命令
  • 完成握手,A节点通过Gossip协议传播给集群中的其他节点,让其他节点和B节点握手,互相加入对方的nodes字典中,这样集群中所有的节点的nodes字典都是全量的节点

槽指派

  • Redis集群把整个数据库分为16384个槽,每个key通过求hash取余的方式都会落到一个槽中

  • 当数据库的16384个槽都有节点在处理的时候,集群处于上线状态,否则集群处于下线状态。

    • 使用CLUSTER ADDSLOTS命令,将槽指定给节点
  • 给节点分配槽后

    • 节点首先检查分配的槽是不是指派过给别人了,如果已经指派给别人,直接报错
    • 初始化clusterState.slots数组,这个数组每个项都是一个指针, 表示这个槽由哪个节点进行负责,用户快速定位这个槽是哪个节点负责
    • 初始化clusterNode中有一个slots数组,16384个bit,把这个数组的槽初始化,如果1表示是自己负责的槽,如果0表示不是自己负责的槽

  • 初始化自己的slots数组之后,就需要把这个数组传给集群中其他的节点,其他节点收到了这个数组,更新culsterState.slots数组并且去自己的clusterState.nodes找到这个节点,并且更新里面的slots数组

  • 每个节点最终会知道哪个槽是由哪个节点负责的

集群处理命令

  • 16384个槽都进行指派之后,集群就会进入上线状态。
  • 客户端往集群任意一个节点发送命令,节点会计算key对应哪个槽,如果这个槽是自己负责的就处理这个命令,如果不是自己负责的,就给客户端发一个MOVED错误,指引客户端转向正确的节点(每个节点的clusterState中保存了全量节点和知道每个槽该由哪个节点负责)

节点保存数据

  • 除了正常保存之外,还会在clusterState使用跳表维护每个槽下面有哪些key,为了方便快速批量操作某个槽下面所有的key

重新分片

  • 重新分片的作用:将任意数量的槽在线(online)从某个节点迁移到另一个节点
  • redis-trib会把槽迁移的信息发给集群中所有的节点,所有节点都会知道槽slot已经指派给新的节点了
  • 使用redis-trib工具(类似于redis-cli客户端 都是官方提供的命令行工具)
    迁移一个槽的步骤,迁移多个槽就是每个槽都执行如下的步骤
  • redis-trib给目标节点发送命令,让目标节点准备好从源节点导入slot的所有键值对
  • redis-trib给源节点发送命令,让源节点准备好发送slot的所有键值对
  • redis-trib给源节点发送命令,一次性获得count数量的slot的key名 (直接从之前说的维护了slot和key的跳表中取)
  • 获得多个key之后,每个key都给源节点发送migrate命令,把每个key都原子的迁移到目标节点
  • 依次重复,直到slot中所有的key都迁移到目标节点

ASK错误

  • 迁移过程中,可能一个槽的部分key在源节点里面,另一部分在目标节点中。
  • 源节点首先会在自己的数据库中查找key,如果找不到就发送ASK错误,指引客户端向目标节点查找。
  • 之前槽迁移的步骤中说了,redis-trib会给目标节点发送命令,让他准备好槽导入。收到命令后,目标节点会在clusterState中的importing_slots_form数组中记录当前节点正在从其他节点导入槽
  • redis-trib会给源节点发送命令,让他准备好槽迁移。收到命令后,目标节点会在clusterState结构的migrating_slots_to数组中记录当前节点正在槽迁移
  • 如果节点收到了一个key请求,会先去自己的数据库中找是否存在这个key,如果没找到,会去看这个key对应的槽是否正在迁移中,如果在迁移中,会给客户端发送一个ACK错误,引导客户端去目标节点查找key
原文地址:https://www.cnblogs.com/dddyyy/p/15217613.html