复制
- slaveof ip port 命令让某台机器成为ip port 服务器的从节点
- 复制分为同步、命令传输
- redis 2.8版本后 采用了优化后的同步,解决了旧版同步的低效问题
旧版复制:
- 通过sync命令实现
旧版复制步骤:
- 从服务器发送sync命令给主服务器
- 收到sync的主服务器此时开始执行bgsave,后台生成一个rdb文件,并通过一个缓冲区记录从此刻开始的所有写命令;
- 当主服务器的bgsave 执行完后,主服务器会将这个rdb文件发送给从服务器,从服务器接受并载入这个rdb文件,将自己的数据库更新至rdb文件的状态。
- 主服务器将缓冲区的所有内容发送给从服务器,从服务器执行这些写命令,将自己的数据库状态更新到主服务器状态。
命令传输
- 同步后不是一成不变的,当主服务器有新的写操作后,主从也需要同步,不然又不一致了
- 主服务器写了后,会将该条写命令发送给从服务器,从服务器执行这条写命令,再次回到一致状态;
旧版复制的缺点
- 对于初次复制来说,sync能够很好的完成任务,但是对于断线后的重连,旧版复制虽然也能完成任务,但是效率非常低。
- 比如说就是断线了一会,就几条命令没同步,却要全部重新载入rdb文件,既消耗主服务器的io和cpu、网络 等资源,从服务器载入的时间也很长。
新版复制的实现
- 使用psync 命令代替了sync
- 分为完整同步 和增量同步
- 完整同步 使用在 初次复制、不能使用增量同步的情况,和sync一样
- 增量同步 使用在 断线重连,而且满足一定的条件,主服务器只需要将断线期间的所有写命令发送给从服务器,从服务器只要执行这些命令即可。
原理:
- 借助下面三个部分:
- 主服务器的 复制偏移量 和从服务器的 复制偏移量;
- 主服务器的 复制积压缓冲区
- 服务器的 运行id runid
复制偏移量
- 执行复制的双方,主和从都维护一个复制偏移量;
- 主服务器 每次传输n个字节,自己的复制偏移量就增加n
- 从服务器 每次接受到n个字节,自己的复制偏移量也增加n
- 这样,只要主从 的复制偏移量一致,就可以认为他们在同一个状态;
复制积压缓冲区
- 本质 固定长度 fifo的队列
- 主服务器每次写操作,不管是复制还是命令传输,都会写到复制积压缓冲区内;
- 默认1mb,这个大小很重要
- 如果这个大小设置太小,会导致断线很短时间也要全部更新,不能利用增量更新这个优点,如果设置太大,浪费内存
- 一般使用 2* 平均断线重连的时间 * 每s平均写入命令大小;
服务器运行id
- 每个服务器都有自己的运行id,40个随机16进制字符;
- 初次复制时,从服务器会将主服务器的runid存起来,后续断线重连后,会将这个id发给主服务器,主服务器判断是否是从自己那复制的
复制的完整步骤
- 1.从服务器收到slaveof 命令
- 2.判断自己是否是初次复制,估计使用是否有主服务器的runid吧
- 2.1 是第一次,给主服务器发送psync ?-1 ,执行完整同步
- 2.2 不是第一次,发送 psync runid offset
- 3.主服务器判断:
- 如果从服务器 复制偏移量 后的数据都在 自己的复制积压缓冲区内,那么就可以采用增量更新,
- 如果不行,那还是进行完整同步
哨兵
集群
主要学习集群节点、槽指派、命令执行、重新分配、转向、故障转向
节点
- 一个集群由多个节点组成,通过cluster meet ip port 加入集群
- redis启动时会根据配置cluster-enabled 来判断是否开启服务器的集群模式
- 集群模式和单点模式的区别?
- 集群模式只使用数据库1
- 集群模式下的客户端会隐藏moved错误,直接转向
- 单机模式下的客户端会打印moved错误,而且不会转向,因为他不能理解moved错误
- 每个节点都会使用一个clusterNode结构来记录自己的状态,并为集群中所有的节点都创造一个clusterNode结构
clusterNode
clusterNode:
ctime; // 节点创建的时间
char name; // 节点的名字
int flags; // 节点的标志,比如说主还是从?在线还是下线?
uint configEpoch; // 节点当前的配置纪元,用于故障转移
char ip; // 节点ip
int port; // 节点端口
clusterLink link; // 保存节点连接的相关信息
char slots[]; // 二进制位数组,类似bitmap?下面详细介绍
int numslots;
clusterNode slaveof; // 如果是 从节点, 那么指向主节点
clusterLink
clusterLink:
ctime; //连接 建立时间
int fd; // tcp 连接 套接字的文件描述符
sds sndbuf; // 输出缓冲区,保存要发给其他节点的信息
sds rcvbug; // 输入缓存区,保存从其他节点接受到的消息
clusterNode node; // 连接对应的节点,可以为null
clusterState
这个结构记录了 在当前节点视角下,当前集群的状态,
clusterState:
clusterNode myself; // 指向自己节点结构的指针
uint currentEpoch;// 配置纪元,用于实现故障转移
int state;// 集群状态,是在线还是下线
int size;// 集群中 至少处理一个槽的 节点数
dict node;// 一个字典,key 是节点名,value 是对应的结构指针,包括myself
clusterNode[] slots;// 16384 个clusterNode数组,下面细说
cluster meet 命令的实现:
向节点A发送cluster meet 命令: cluster meet bip bport ,可以将节点b加入a的集群
- a与b进行握手,如下操作
- a 为b创建一个clusterNode结构,并加入到clusterState.node里
- a 向b 发送一条meet消息
- b收到a的meet消息后,会为a创建一个clusterNode结构,并加入到自己的clusterState.node里
- b返回a一条pong消息
- a收到后,返回一条ping,握手完成。类似三次握手吧
槽指派
- redis 通过分片的方式来保存数据库中的键值对,共16384个槽,每个键都属于0-16383槽内,集群中每个节点都可以出来0-最多16384个槽
- 当所有的槽都有节点处理时,集群处于上线状态,否则就是下线状态,记录在clusterState.state里
- 在某个节点执行 cluster addslots 0 1 2 ... 5000 ,可以将这5001个槽 指派给这个节点
- 节点负责的槽信息记录在clusterNode里的slots 和numslot里
- slots: 长度为16384/8=2048字节,包含16384个二进制数据,要是该节点负责处理哪个槽,该索引对应的二进制位为1,要不然的话为0,不知道是不是bitmap实现,思路是一致的,这样的话,检查节点是否负责哪个槽,时间复杂度是O(1)
- numslots 记录该节点处理的槽的数量
- 节点除了自己记录自己处理的槽 信息外,还会把自己的slots数组发送给集群的其他节点。告诉其他节点,自己负责哪一块。节点收到其他节点发送的该消息后,会在自己的clusterState.nodes 字典找到对应的clustNode结构,更新其中的slots数组
- clusterState.slots 里有16384个clusterNode 元素,记录 每个槽 对于的 clusterNode节点信息的指针,为啥光使用clusterState.node里的slots数组还不够?
- 为了知道槽i被谁处理,可能需要遍历clusterState.nodes 字典。复杂度为O(n)
- 而使用 clusterState.slots后,只需要O(1)的时间复杂度。
- 那为啥不能只用 clusterState.slots?还需要clusterState.nodes么?
- 但是如果需要将某个节点所处理的全部槽信息发送出去时,只需要去clusterState.nodes 中寻找就行了 ,不需要遍历 clusterState.slots
cluster addslots 命令实现:
- 遍历检查 这些槽,哪怕有一个已经被指派,也返回错误
- 再次遍历,将这些槽指派给自己节点
集群执行命令
- 计算该键属于哪个槽,并检查这个槽是不是自己处理
- 如果是,直接执行这个命令
- 如果不是,返回一个moved错误,并指引客户端转向正确的节点,再次执行该命令
计算键属于哪个槽
计算键的crc-16校验和,然后 & 16383,
检查是否是自己处理
检查自己的clusterState.slots 里的槽i 是否指向自己
moved错误
- 集群模式下的客户端会隐藏moved错误,直接转向
- 单机模式下的客户端会打印moved错误,而且不会转向,因为他不能理解moved错误
重新分片
可以在线操作
实现原理:
- 由redis-trib软件负责,
-
- redis-trib 对目标节点发送cluster setslot 命令,让目标节点准备好从源节点导入属于槽的键值对
-
- redis-trib 对源节点发送cluster setslot 命令,让源节点准备好讲属于槽的键值对发送目标节点
-
- 源节点返回最多count个键值对
- 4.对于每个键,都向源节点发送一个migrate命令,让该键值对转移到目标节点
- 5.重复直到完成全部键值对
ask错误
在重新分片的过程中,如果客户端要查询属于这些槽内的键值对,可能会出现ask错误
- 源节点会首先从自己的数据库里找,如果找到的话,直接执行命令
- 如果没找到,这个键可能已经迁移到新节点了,返回一个ask错误,并指引客户端转向正确的节点
- ask错误也是隐藏的
复制
集群的节点也是有主节点、从节点的,主节点负责处理这些槽,从节点复制某个主节点,当主节点下线时,可以顶上
- cluster replicate nodeid 成为该节点的从节点
- 收到命令后,会将自己clusterNode 中的slaveof指向 这个主节点
- 然后发送给集群中的其他节点,最终全部节点都会知道
故障检测
- 每个节点都会定期向其他节点发送ping消息,如果某个接受到的节点没有在固定时间返回pong,就会将该节点标为疑似下线;会在自己的clusterState.nodes里找到该节点的clusterNode结构,将其flags属性改为pfali
- 当主节点a从主节点b得知b认为主节点c下线了,a会在自己的clusterState.nodes字典里找到主节点c对应的clusterNode结构,将b的下线报告记录在clusterNode.fail_reports链表里
- 如果集群中一般的节点都将主节点c报告疑似下线,那么c会变成fail状态,向集群广播
故障转移
- 下线主节点的所有从节点中,会有一个被选中
- 被选中的节点执行slaveof no one 命令,成为新的主节点
- 新节点会将之前主节点处理的槽下掉,指向自己
- 新的主节点向集群广播一条pong小学,通知自己变成主节点了,并且负责之前下线的槽
选举新的主节点
- 1.集群的配置纪元 初始值为0,
- 2.每开始一次故障转移,配置纪元++
- 3.对于每个配置纪元。每个主节点都有一次投票机会,而第一个向主节点要求投票的从节点将获得这一票
- 4.当从节点发现自己的主节点下线了,会广播一条消息,要求其他主节点给自己投票
- 5.如果主节点收到并且还有投票机会,就会返回一条消息,
- 6.每个参与的从节点会统计自己获得了多少主节点的支持
- 7.假设集群有n个主节点,当某个从节点统计到自己得到了n/2+1张票,就会广播自己成为新的主节点