《Redis 设计与实现》读书笔记(三)
多机数据库实现
十五 、复制
从服务器通过命令 slaveof 127.0.0.1 6000 成为主服务器的从服务器。然后执行复制操作,保持自己的状态和主服务器一样
1.理论
同步
成为从服务器后的同步操作:
- 从服务器会发送SYNC命令给主服务器,
- 主机会执行bgsave命令,并记录当前的偏移量。
- bgsave命令执行期间执行的写命令,都会记录到缓冲区
- bgsave命令执行成功后,主机发送RDB文件给从机
- 从机加载RDB文件
- 主机发送缓冲区的命令给从机
- 从机执行缓冲区命令
命令传播
当主从机的状态一致后
主机每次执行写命令,都会通过命令传播的方式,发送给从机
从机执行写命令,这样主从的状态又会一致了。
2.8版本前的缺陷
如果主从之间网络断开,这样主机的写命令就不能通过命令传播发给从机了,这时候主从就不一致了。
当主从重新连接上后,在2.8版本前的做法是重新执行一次同步操作。
如果主从断开前执行了很多命令,断开期间期间主机只执行了几条写命令,重新执行一次同步操作,效率会比较慢。更好的做法是只同步断开期间执行的命令给从机就好了。所以为了优化这个缺陷,2.8后新出了PSYNC命令
新版命令
新版增加了PSYNC命令,这个命令支持完整重同步
和部分重同步
简单来讲就是重连后,主机会判断当前能不能执行部分重同步,如果可以就做,如果不可以,就执行完整重同步。
其他知识
- 复制偏移量(offset)。
- 主机和从机都会保存复制偏移量,这个是当前执行过的所有命令的字节数。例如set key value命令的字节数是33。(这个不是简单的把命令转成字符串的,有一定的算法,算法应该和RDB文件的算法一样的。总之就是把一条命令多个参数转成一个字符串。例如
set test 3
命令就占44
个字节) - 当执行一条新命令,例如偏移量是100,。主机执行完后,就会把自己的偏移量加100
- 主机命令传播给从机后,从机执行完,也把自己的偏移量加100
- 这个的作用就是识别主从之间是否一致以及不一致的程度有多少
- 主机和从机都会保存复制偏移量,这个是当前执行过的所有命令的字节数。例如set key value命令的字节数是33。(这个不是简单的把命令转成字符串的,有一定的算法,算法应该和RDB文件的算法一样的。总之就是把一条命令多个参数转成一个字符串。例如
- 复制积压缓冲区 (repl_backlog)
- 这个缓冲区和同步的时候的缓冲区不一样
- 主机每次执行写命令,就把命令转换成的字符串,存入这个缓冲区
- 缓冲区采用固定长度,先进先出的队列。
- 默认缓冲区的大小是1M。通过info replication命令可以查看缓冲区信息,
repl_backlog_size:1048576
- 缓冲区每一个字节,都有自己的偏移量号码对应上面的复制偏移量。
- 服务器运行ID(run id)
- 每个redis节点都有自己的运行ID。是40个随机的十六进制字符组成。
- 主从关系建立后,从机会记录主机的ID
- 每次从机执行PSYNC都要把主机的ID传输过去,如果主机ID变更,只能使用完整重同步。
info replication
127.0.0.1:6811> info replication
# Replication
role:master #当前节点的角色
connected_slaves:1 #从机数量
slave0:ip=127.0.0.1,port=6801,state=online,offset=2095671,lag=0 #从机1信息
master_repl_offset:2095671 #主机的offset
repl_backlog_active:1 #缓冲区是否可用
repl_backlog_size:1048576 #缓存的大小,默认是1M
repl_backlog_first_byte_offset:1047096 #缓冲区第一个字节的offset
repl_backlog_histlen:1048576 #
在主机执行这个命令,可以查看主从复制的情况,包括有多少个从机,偏移量,缓冲区大小等。
2.过程
PSYNC命令实现
PSYNC的调用方法有两种
- 从机之前没有成为别人的从机,也就是第一次成为从机。会发送PSYNC ? -1命令。这时候肯定会执行完整重同步
- 从机之前成为过别人的从机。会发送命令PSYNC runid是之前的主机的ID,offset是从机当前的offset。
- 主机收到命令后会判断runid是否和自己的一样,如果不一样,就执行完整重同步
- 如果一样,判断offset是否小于自己的
repl_backlog_first_byte_offset
,也就是从机缺失的写命令是否还在缓冲区内- 如果不在,就执行完整重同步
- 如果再,就执行部分重同步
- 所以,只有当runid没有变更,而且offset小于
repl_backlog_first_byte_offset
,才会执行部分重同步,否则执行完成重同步
如果可以执行部分重同步,主机会返回+CONTINUE命令,然后发送缺失的写命令给从机
如果需要执行完整重同步,主机会返回+FULLRESYNC命令,然后后面的步骤和同步一样。
主从同步完整流程
- slaveof命令
- 执行完slaveof命令后,从机会把主机的ip和端口存在redisServer结构体里面,然后就返回ok了
- 返回ok后才会执行同步操作,所以是异步的。
- 从机与主机建立socket连接。这时候从机相当于主机的客户端
- 从机发送ping命令给主机,主机如果正常返回pong命令。如果主机超时不返回或者返回错误。从机断开连接重试。
- 身份验证,如果需要从机需要发送
auth 密码
命令 - 从机发送端口信息给主机。也就是从机节点的端口。
- 从机发送PSYNC命令
- 主机判断执行那种同步,不管是那种同步,主机都会成为从机的客户端,也就是连接从机的端口。
- 如果是完整重同步,主机记录当前offset,执行bgsave,发送RDB文件给从机,发送offset后面的写命令给从机。
- 如果是部分重同步,主机发送从机的offset之后的写命令给从机
- 从机执行写命令,主从状态达到一致
- 然后进入命令传播阶段,主机执行的所有写命令,都发送给从机,从机执行后,主从状态达到一致。
心跳
从机每隔一秒会向主机发送心跳命令 REPLCONF ACK <replication_offset>
心跳可以实现功能:
检测主从之间的网络状态
辅助实现min-slaves
检测命令丢失
检测主从之间的网络状态
- 如果主机超过1秒没有收到从机的ack命令,就表名从机网络出现了故障
- info replication命令可以看到从机上一次ack距离现在的时间,就是lag参数,一般在0-1之间,超过就是有故障了
辅助实现min-slaves
- min-slaves选项是指在从机数小于min-slaves-to-write,而且全部从机的lag值大于min-slaves-max-lag秒时,主机拒绝执行写命令。
- 这个功能主要是防止主机的主从复制处于不安全状态
检测命令丢失
- 假如主机的写命令没有成功传输给从机,例如网络丢失了。这时候从机的offset就会小于主机。通过心跳,主机会发现从机的offset不等于自己,就会补发对应的写命令给从机。
- 从机通过offset可以避免重复执行相同offset的命令
- 命令补发这种情况较为容易触发。
- 例如主机刚执行一条新命令,也把命令传播出去了,但是从机还没有收到,然后心跳过来了,这时候从机offset肯定会小于主机offset。
- 所以不知道Redis有没有机制可以避免这种情况。例如两次心跳都一样而且offset小于自己,才触发命令补发。
- 假如主机的写命令没有成功传输给从机,例如网络丢失了。这时候从机的offset就会小于主机。通过心跳,主机会发现从机的offset不等于自己,就会补发对应的写命令给从机。
十六、哨兵
哨兵是Redis高可用的一种方案。Redis的架构是一主多从,然后有一个或者多个哨兵进程去监听主服务器的情况。当哨兵认为主服务器已经下线,提升其中一个从服务器为主服务器,然后修改其他从服务器的复制配置。
哨兵的作用类似Mysql的MHA,只是哨兵支持多个,MHA只有一个manager。
1.初始化哨兵Sentinel
哨兵也是一个Redis进程,启动方式是redis-sentinel /config.conf
哨兵进程只能执行哨兵相关的命令,不能执行其他的Redis命令。
数据结构
sentinelState 哨兵状态结构
- uint64_t current_epoch 当前纪元,用于选取领头羊哨兵
- dict *masters 监视的主服务器信息,一个哨兵集群可以监视多个主服务器。key是主服务器的名字,例如127.0.0.1::6479 value是sentinelRedisInstance结构
- tilt 是否进入tilt模式
sentinelRedisInstance 哨兵实例结构
- flags 标志值,表示实例当前的状态,可取值:主服务器,从服务器,主观下线,客观下线
- char *name 名字 例如127.0.0.1:6379
- char *runid 运行ID
- uint64_t config_epoch 配置纪元
- *addr 地址包括ip和端口
- down_after_period 实例无响应多久判断为主观下线
- quorum 判断为客观下线所需的投票数
- dict slaves 这个主服务器下面的所有从服务器,结构和masters结构一样。
- dict sentinels 监视这个主服务器的其他哨兵,不包含哨兵自己
哨兵配置
port 6711
#监听的存储redis,TestMaster1是redis名称,127.0.0.1是ip,6702 是端口,1是升级为Master的权重
sentinel monitor mymaster 127.0.0.1 6721 1
sentinel down-after-milliseconds mymaster 3000
sentinel failover-timeout mymaster 10000
daemonize yes
#指定工作目录
dir "/data/redis_demo"
logfile "/data/redis_demo/log/sentinel.log"
#redis主节点密码
sentinel auth-pass mymaster 123456
- mymaster是主服务器的名字
- 后面是ip和端口 1是quorum
- down-after-milliseconds 实例无响应多久判断为主观下线
哨兵启动后,初始化后,就会和主服务器建立连接,有两个:
- 命令连接。也就是哨兵充当主服务器的客户端。用于向客户端发送PING,发送订阅等命令
- 订阅连接,会订阅频道
__sentinel__:hello
,用于接收订阅消息。
2.获取主从服务器信息
建立连接后,哨兵会每10秒向主服务器发送INFO命令。INFO命令会返回主服务器的所有从服务器信息。这样哨兵就能知道主服务器有多少从服务器了。
然后会新建或者更新主服务器的slaves结构
slaves结构的key是从服务器的ip和端口,例如127.0.0.1:7000,value是sentinelRedisInstance数据结构。
当有新的从服务器,哨兵会像和主服务器建立的连接一样,和从服务器也建立两个连接。
然后会每10向从服务器发送INFO命令。
3.获取其他哨兵信息
哨兵会每个2秒向主和从服务器发送订阅消息,频道是__sentinel__:hello
,消息是:PUBLISH __sentinel__:hello "s_ip,s_port,s_runid,s_epoch,m_name,m_ip,m_port,m_epoch"
- s开头的是哨兵自己的信息
- m开头的是主服务器的信息
假如哨兵1发送了这个消息,因为其他哨兵,例如2和3,都会订阅这个频道,所以它们也能收到这个消息,哨兵1自己也会收到。
所以当它们收到这个信息后:
- 如果run_id是自己,不处理
- 如果run_id不是自己,
- 更新或者新建其他哨兵的数据结构。更新master的sentinels结构,key是哨兵2和3的ip端口,例如127.0.0.1::8000 value是sentinelRedisInstance结构。
- 和其他哨兵建立连接,只会建立命令连接,不会建立订阅连接。
4.判断主观下线
哨兵会每1秒向其他哨兵和主从服务器发送PING命令。其他服务器会返回:
- PONG,LOADING MASTERDOWN3种回复
- 除此之外的其他回复或者超时不回复称为无效回复。
当在down-after-milliseconds时间内,例如是5s,对方连续返回无效回复,例如是5次PING都返回无效回复,哨兵就会把这个服务标记为主观下线,就是把flags值修改为SRI_S_DOWN。
5.判断客观下线
当哨兵判断一个主服务器主观下线后(从服务器不会触发),会向其他哨兵发送命令:
SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>
分别为主服务器的ip 端口,自己的配置纪元,runid=*号。
其他哨兵接收这个命令后,会返回
- down_state 下线状态1=下线,0=未下线
- leader_runid 选举的leader的run_id,
- leader_epoch 选举的leader的配置纪元
上面的 current_epoch run_id leader_runid leader_epoch都是用于选举领头羊哨兵的,在判断客观下线中没有用。
所以总的来说,哨兵判断一个主服务器下线后,会询问其他哨兵,是否也把这个服务器标记为下线,如果有大于等于quorum参数的哨兵投票说主服务器已下线,哨兵会把主服务器标记为客观下线,也就是把flags标记为SRI_O_DOWN
6.选举领头羊leader
当一个哨兵把主服务器标记为客观下线后,就会进入选领头羊leader环节,在多个哨兵中选择一个领头羊哨兵,来执行故障转移操作。
- 哨兵1判断主服务器为客观下线后,向所有其他哨兵发送上面的
is-master-down-by-addr
命令,current_epoch设置为自己的配置纪元,runid是自己的runid - 哨兵2收到这条命令后,如果在自己的配置纪元没有选过领头羊,就会返回leader_runid=哨兵1的runid,leader_epoch=哨兵1的配置纪元。如果已经选过领头羊,就会返回选中的领头羊信息
- 如果超过总哨兵的半数都投票给哨兵1,哨兵1就会成为领头羊
配置纪元问题:
- 全部哨兵的配置纪元是否需要相同,如果相同,怎么同步?
- 如果不相同,怎么判断这个配置单元中有没有选过其他人
解答
- 在哨兵A认识其他哨兵的时候,会传送自己的配置纪元给对方
- 一开始所有的哨兵的配置纪元都是0
- 当哨兵看到对方的配置纪元比自己大,就会更新自己的配置纪元为对方的配置纪元
- 这样当所有哨兵都认识后,所有哨兵的配置纪元都会统一,也就是所有哨兵中最大的那个,例如A是1,B是2,C是3,最后ABC的配置纪元都会设置为3
- 当哨兵A发起投票的时候,它会先把自己的配置纪元+1,例如变为4,然后要求BC投票。然后计时(例如等待5s)。
- 当B收到A的投票要求,如果B的配置纪元比自己的大(例如B现在是3),就会认为4是没有投票的配置纪元,就把票投给A,然后设置自己的配置纪元为4.
- 当B收到C的投票要求,发现自己的配置纪元等于C的配置纪元(例如都是4),因为在配置纪元=4时,B已经把票投给A了。所以B不能投票给C,它会返回A的runid和A的配置纪元
- A计时结束后(也就是5s后),如果A只收到B的票,但是没有收到C的票(可能C把票投给B了),所以成为领头羊失败。这时A会把配置纪元再+1=5,然后再次要求BC投票,然后再计时
异常情况
- 如果A的配置纪元是5,C是4,B是3
- C先发起投票请求,B会投票给C,但是A不会,因为C的纪元比自己小
- A发起投票请求,B会投票给A,C也会投票给A
- 所以最终A和C都认为自己成为了领头羊。
- 可能的解决方法:
- 方法1:
- C收到A的返回中会标明A投票给了A,纪元是5
- C发现A的纪元比自己的纪元大,所以应该停止成为领头羊
- 方法2:
- C和A成为领头羊后,向所有节点群发自己成为领头羊的消息,以及自己的纪元
- 当C发现A成为领头羊,而且纪元比自己大,就自动放弃领头羊
- 方法1:
7.故障转移
成为领头羊leader的哨兵将执行主服务器的故障转移工作
- 从从服务器中选一个成为主服务器
- 优先选择近期ping后有回应的服务器
- 优先选择数据较新的从节点
- 对新的主服务器执行slaveof no one命令 让它成为主服务器
- 每2秒对新服务器执行INFO命令,查看role是否从slave更新为master
- 如果成为master,对其他从服务器执行slaveof操作,让它们从新的主服务器复制数据
- 把旧的主服务器记录下来,等下次它上线,执行slaveof命令,让它从新的主服务器复制
十七、集群
集群通过分片(sharding)来进行数据共享,并提供复制和故障转移功能。
1.节点
一个集群由多个节点组成,一开始这些节点是互相不能感知的。
我们需要通过命令cluster meet <ip> <port>
,让节点加入集群。例如在A节点执行meet命令,ip和port是B节点的,这样A和B节点就相互感知了。
通过 cluster nodes命令可以查看当前集群的情况。
127.0.0.1:6812> cluster nodes
1fdfb5833caf8e9cf3b7f1233ce3969e0a324db7 127.0.0.1:6804 master - 0 1572954527331 12 connected 0-1104 5461-5779 11423-12004
72234454d061c86c630e8eb7995e2480fe340b95 127.0.0.1:6803 master - 0 1572954527331 8 connected 12005-16383
- 分别是 节点ID,IP 端口,角色
启动
节点需要配置 cluster-enabled yes
才会开启集群模式。
集群模式的节点启动后,其他都和单机节点一样的,只会在serverCron函数中增加一个clusterCron函数的调用
集群数据结构
集群增加了3种数据结构
- clusterNode 集群节点信息,有字段
- mstime_t ctime 创建时间
- char name 节点名,也叫节点ID
- int flags 存储节点的角色(master还是slave)和集群状态(在线或者下线)
- uint_64_t configEpoch 配置纪元 用于故障转移
- char ip 节点IP地址
- int port 节点端口
- clusterLink link 和其他节点的连接
- clusterLink 和其他节点的连接,和redisClient结构很像,有字段:
- mstime_t ctime 创建时间
- int fd 套接字描述符
- sds sndbuf 待发送缓冲区
- sds rcvbuf 已接收缓冲区
- clusterNode node 这个连接对应的节点信息
- clusterState 集群状态,有字段:
- clusterNode myself指向自己的clusterNode结构
- uint64_t currentEpoch 配置纪元,用于故障转移
- int state 集群状态 上线还是下线
- int size 集群中至少处理着一个槽的节点数量。
- dict *nodes 集群中所有的节点,key是节点名,value是clusterNode对象,也包括节点自己的node实例。
cluster meet命令
- 客户端向节点A发送Meet命令
- 节点A创建节点B的ClusterNode对象
- A节点发送Meet命令给节点B
- 节点B创建节点A的ClusterNode对象
- 返回Pong命令
- 节点A收到Pong命令
- 节点A返回Ping命令给节点B
- 节点B收到Ping命令
- 握手完成
然后节点A和B通过Gossip协议,然自己一直的节点认识彼此。
2.槽指派
Redis集群有16384个槽。数据库中每个键都对应这些槽中的一个。每个节点处理0-16384个槽。
只有当全部槽都有节点处理,集群才会进入上线状态。
槽指派命令
cluster addslots 0 1 2
这里把槽 0 1 2 3个槽指派给当前连接的节点。
槽的数据结构
槽的信息存储在clusterNode结构的unsigned char slots[16384/8]
。这是一个二进制字符串列表,只有0 1。 如果是1表示这个下标的槽由当前节点处理。还要个numslots记录处理的槽的总数。
在clusterState结构有个 clusterNode *slots[16384]变量用来存储每个槽对应的节点对象。
这样就能实现通过O1复杂度可以
- 查找自己是否负责某个槽
- 某个槽是哪个节点在处理,还是没有节点在处理
执行cluster addslots命令后,当前节点会把自己负责的槽都同步给其他节点。
当机器所有槽都有节点处理,机器就会进入上线状态
集群中执行命令
- 客户端发送命令给其中一个节点
- 计算这个key对应的槽,使用CRC16 校验和算法
- 槽是否有当前节点处理。检查clusterState.slots[i]是否指向clusterState.self,如果是就是自己处理。
- 是,执行命令
- 否,查看槽在哪个节点负责,返回MOVED错误给客户端
MOVED 10000 127.0.0.1:6801
分别是槽号,处理该槽的IP和port
- 客户端收到MOVED错误,连接到对应的节点,重试
通过命令cluster keyslot test 可以查看test这个key属于哪个槽.
如果使用-c集群模式启动客户端,MOVED命令会被隐藏。否则会抛出。
数据库实现
集群模式,的数据库实现和单机模式差不多,不同点:
- 集群模式只有一个数据库,就是0
- clusterState对象有个变量是
zskiplist *slots_to_keys
是个跳跃表对象,保存当前数据库的所有key,以及key的slot,slot是分数的形式。- 保存这个信息的好处是
- 可以快速执行
cluster getkeysinslot <slot> <count>
用于返回指定槽的N个key。这个命令主要用于重新分片
- 可以快速执行
- 保存这个信息的好处是
4.重新分片
重新分片就是把N个槽从节点A迁移到节点B。重新分片过程中,集群是一直在线状态的。
重新分片工作一般是使用管理软件redis-trib负责的
步骤是
- 对目标节点发送
cluster setslot <slot> IMPORTING <source_id>
命令,让目标节点做好导入槽的准备 - 对源节点发送
cluster setslot <slot> MIGRATING <target_id>
命令,让源节点做好导出槽的准备。 - 对源节点发送
cluster getkeysinslot <slot> <count>
命令,获取count个属于槽slot的key - 对源节点发送
migrate <target_ip> <target_port> <key_name> 0 <timeout>
命令,将对应的key迁移到目标节点。一条命令只能迁移一个key。
数据结构
- IMPORTING命令
- 当目标节点接收IMPORTING命令后,会查看clusterState对象的
clusterNode *importing_slots_from[16384]
变量对应的slot是否指向NULL,如果否,证明节点正在导入这个slot。如果是,将slot执行source_id对应的clusterNode对象
- 当目标节点接收IMPORTING命令后,会查看clusterState对象的
- MIGRATING命令
- 当元节点接收MIGRATING命令后,会查看clusterState对象的
clusterNode *migrating_slots_to[16384]
变量的对应的slot是否执行NULL,如果否,证明节点正在导出这个slot。如果是,将slot执行target_id对应的clusterNode对象
- 当元节点接收MIGRATING命令后,会查看clusterState对象的
客户端请求
因为迁移的过程,机器是一直上线的,所以就会存在问题:迁移过程中,如果客户端操作迁移中的key,怎么办。解决方法就是引入ASK错误。
在迁移的过程中,迁移的slot依然由源节点负责,所以对这个slot的key的操作依然是对源节点发送命令的。
- 客户端发送命令给源节点
- 源节点查看key是否在数据库中。
- 如果是,执行命令
- 如果否
- 判断key对应的槽i是否在迁移。查看migrating_slots_to[i]是否指向clusterNOde对象。
- 如果是,有可能在目标节点,返回ASK错误
ASK 10000 127.0.0.1:6801
分别是槽号,处理该槽的IP和port - 如果否,返回key不存在
- 如果是,有可能在目标节点,返回ASK错误
- 判断key对应的槽i是否在迁移。查看migrating_slots_to[i]是否指向clusterNOde对象。
- 客户端收到ASK命令后,连接到对应的节点
- 执行命令REDIS_ASKING打开标识
- 执行命令
- 目标节点收到命令后
- 查看slot是否由自己负责
- 如果是,执行命令
- 如果否,查看slot是否正在导入查看importing_slots_from[i]是否指向clusterNOde对象。
- 如果是,判断客户端是否带ASKING标识。
- 如果是,执行命令
- 如果否,返回MOVED命令
- 如果否,返回MOVED命令
- 如果是,判断客户端是否带ASKING标识。
ASK命令
- ASK命令和MOVED命令一样,也可能被隐藏。
- 客户端只有打开REDIS_ASKING标识,才能执行命令
- 打开REDIS_ASKING表示只会对下一条命令生效
- 下一条该slot的命令,还是会发给源节点
5.复制和故障转移
集群里面有
- 主节点,负责处理槽
- 从节点,从主节点复制数据,但是不处理槽
如果主节点故障,集群会自动把其中一个主节点的从节点提升为新主节点。之前复制旧主节点的从节点会重新复制新主节点
消息
集群的节点通过消息来进行交流。
发送消息的节点成为发送者
接收消息的节点成为接收者
消息有5种:
- MEET消息。执行cluster meet命令后,发送的消息
- PING消息。 集群内每个节点每隔一秒钟,就会从集群里面随机选出最多5个节点,然后选出最长时间没有发送PING消息的节点,来发送PING消息。(也就是每一秒只会给一个节点发送PING消息)
- 如果节点A最后一次接受节点B的PONG消息的时间距离现在超过了cluster-node-timeout配置的一半。节点A也会想节点B发送PING消息。
- PONG消息。当接受者收到MEET消息或者PING消息,为了向发送者确认已收到这条消息,接受者会向发送者发送PONG消息。
- 另外,节点可以通过向集群广播PONG消息来让别的节点刷新对该节点的认识
- FAIL 消息 当一个主节点A判断另一个节点B已经进入FAIL状态时,就会广播FAIL消息。接收到这个消息的节点,会立刻把节点B标志为下线
- PUBLISH消息。当一个节点收到PUBLISH命令时,会执行这个命令,并向集群广播PUBLISH消息
MEET PING PONG3中消息称为Gossip协议消息。
一条消息由消息头和正文组成。
消息头
消息头是一个结构,里面包含正文和其他属性
- uint32_t totlen。消息的长度,包含消息头和正文
- uint64_t type。消息类型
- uint6_t count 消息正文包含的节点信息数量。只有在MEET PING PONG三种消息使用
- uint64_t currentEpoch 发送者所处的配置纪元
- uint64_t configEpoch 如果发送者是主节点,记录主节点的配置纪元。如果是从节点,记录正在复制的主节点的配置纪元
- char sender[REDIS_CLUSTER_NAMELEN]。发送者名称,也就是node id
- unsigned char myslots[REDIS_CLUSTER_SLOTS/8] 。发送者目前的槽指派信息
- char slaveof[REDIS_CLUSTER_NAMELEN] 如果是从节点,记录主节点的名称。
- uint16_t port 发送者端口
- uint16_t flags 发送者标识值
- char state 集群状态
- union clusterMsgData data 正文。是个联合对象。消息不同,这里的数据结构不一样。
消息正文
- MEET PING PONG消息的实现
- 正文是两个clusterMsgDataGossip结构的实例
- 因为MEET PING PONG3种消息的正文结构一样,所以通过消息头的type来判断是哪种消息
- 发送者会从自己已知节点里面随机找两个节点(可以是主或者从)。然后把两个节点的信息保存到两个clusterMsgDataGossip结构里面,有数据
- char nodeName[REDIS_CLUSTER_NAMELEN] 节点名称
- uint32_t ping_sent 最后一次向该节点发送PING的时间戳
- uint32_t pong_received 最后一次从该节点接受PONG消息的时间戳
- char ip[16] IP
- uint16_t port 该节点端口
- uint16_t flags 节点的标识值
- 接受者收到这三种消息后,会查看里面的两个Gossip结构,也就是两个其他节点的信息
- 如果接受者第一次接触节点,就会向这个节点握手
- 如果接受者已接触这个节点,就会更新节点信息
- FAIL消息的实现
- 消息使用clusterMsgDataFail结构,只有一个变量
char nodename[REDIS_CLUSTER_NAMELEN]
- 当接受者收到这个消息,就会标识这个节点为下线状态
- 消息使用clusterMsgDataFail结构,只有一个变量
- PUBLISH消息的实现
- PUBLISH命令有两个参数,channel和msg,例如publish "channel1" "msg1"
- 消息使用clusterMsgDataPublish
- uint32_t channel_len channel的长度
- uint32_t message_len 消息的长度
- unsigned char bulk_data[8] 消息内容,不一定是8字节
- 例如上面的例子:bulk_data存储的是channel1msg1。channel_len =8 message_len =4
设置从节点
cluster replicate <node_id>
通过这个命令,可以让接收命令的节点成为node_id的从节点。
接收命令的节点会:
- 修改clusterState.myself.slaveof的属性,执行node_id对应的clusterNode对象
- 修改clusterState.myself.flags的属性,关闭REDIS_NODE_MASTER标志,打开REDIS_NODE_SLAVE标志
- 调用复制代码,从主节点复制数据。复制的逻辑和单机复制是一样的,所以相当于执行命令
slaveof <master_ip> <master_port>
- 把消息发送给集群所有节点,让所有节点都知道该节点成为node_id的主节点
- 其他节点收到消息后
- 修改主节点对应的clusterNode结构的slaves,这是一个custerNode列表,把从节点加入到列表后面
- 修改主节点对应的clusterNode结构的numslaves,int类型,加一
故障检测
集群内每个节点都会定期向其他节点发送PING消息,目标节点收到ping消息后,返回PONG消息。如果目标节点超时没有返回,发送节点会在该节点的clusterNode结构里面修改flags属性,打开REDIS_NODE_PFAIL
标识,标识位疑似下线
状态。
例如节点A标记节点B为疑似下线。然后通过PING PONG命令,节点A会把这个信息同步给集群其他节点。
当节点C收到节点A认为节点B疑似下线。节点C会在节点B的clusterNode结构的fail_reports链表里面添加一个clusterNodeFailReport结构,有变量:
- clusterNode *node 执行报告节点B疑似下线的节点。这里是节点A
- time 收到下线报告的时间。
当集群里面半数以上负责槽的主节点
都将某个节点标记为疑似下线,那么这个节点会被标记为下线,标记的节点会向集群广播FAIL消息,通知其他节点。
例如这里的节点C,它收到了A节点的报告,同时如果他自己PING节点B也是失败,而且集群里面只有ABC3个负责槽的主节点,那么节点C就会标记节点B位下线,并广播FAIL消息。
故障转移
当集群中其中一个主节点,例如节点B被标记为下线
- 那节点B的从节点,会有一个成为主节点,例如节点D
- 节点D会执行slaveof no one命令,成为新的主节点
- 节点D撤销所有对节点B的槽指派,并将这些槽都指派给自己
- 节点D向集群广播一条PONG消息。让其他节点知道自己成为了主节点并接管了节点B的所有槽指派
- 节点D开始接受和处理客户端的命令请求,转移完成
选举新节点
- 配置纪元是一个自增变量,初始值是0
- 当集群某个节点开始一次故障转移时,配置纪元的值会加一
- 在一个配置纪元中,主节点只有一次投票机会。它会把票投给第一个要求它投票的节点
- 当从节点知道自己的主节点已下线后,会广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求其他主节点为它投票
- 如果主节点有投票权(它正在负责处理槽),并且没有投过票给其他节点,那它会给第一个要求投票的节点返回CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息
- 每个参选的从节点都会受到ACK消息,如果自己收到的ACK消息大于可投票的节点的半数,这个节点就会成为新的主节点。
- 如果在一个配置纪元里面,没有从节点收到足够多的票数(例如3个主节点,挂了一个,剩下2个,2个从节点各自收到一个投票)。那集群就会进入一个新的配置纪元。再次进行选举。
- 有点不太明白。怎么进入新的纪元?谁来决定是否进入新的纪元?
- 选举算法和哨兵的类似,也是Raft算法
《Redis 设计与实现》读书笔记(三)的更多相关文章
- 《修炼之道:.NET开发要点精讲》读书笔记(四)
委托的作用:1)它允许把方法作为参数,传递给其它的模块:2)它允许我们同时调用多个具有相同签名的方法:3)它允许我们异步调用任何方法. “方法签名”指方法的参数个数.参数类型以及返回值等,具有相同签名 ...
- 《修炼之道:.NET开发要点精讲》读书笔记(三)
后几章的习题 1.异步调用开始后,什么时候才能使用异步执行的结果? A:最好在EndInvoke()方法返回之后才能使用异步执行的结果,其它时候不能保证异步调用已完成. 2.委托的异步调用开始后(即调 ...
- 《修炼之道:.NET开发要点精讲》读书笔记(二)
1.简述.NET中CTS.CLS以及CLR的含义与作用. A:CTS指公共类型系统,是.NET平台中各种语言必须遵守的类型规范:CLS指公共语言规范,是.NET平台中各种语言必须遵守的语言规范:CLR ...
- 《修炼之道:.NET开发要点精讲》读书笔记(一)
CLR 公共语言运行库 没有CLR的存在,就不能讲该中间件转换成对应操作系统中的机器指令. 程序集是非完全编译的产物,它兼备了源代码和本地代码的特性,是一种介于源代码和本地代码之间的独立存在的一种数据 ...
- 关于新书《修炼之道:.NET开发要点精讲》的各种说明
索引 新书介绍 新书封面 新书目录 试读章节 原稿试读 网购地址 规格参数 反馈方式 一些感谢 附加说明 1.新书介绍 从2013年年底到2014年9月,历时将近10个月,这本书终于看到了“出版发行” ...
- 《android开发艺术探索》读书笔记(二)--IPC机制
接上篇<android开发艺术探索>读书笔记(一) No1: 在android中使用多进程只有一种方法,那就是给四大组件在AndroidMenifest中指定android:process ...
- 《Android开发艺术探索》读书笔记 (13) 第13章 综合技术、第14章 JNI和NDK编程、第15章 Android性能优化
第13章 综合技术 13.1 使用CrashHandler来获取应用的Crash信息 (1)应用发生Crash在所难免,但是如何采集crash信息以供后续开发处理这类问题呢?利用Thread类的set ...
- delphi 精要-读书笔记(内存分配释放)
delphi 精要-读书笔记(内存分配释放) 1.内存分为三个区域:全局变量区,栈区,堆区 全局变量区:专门存放全局变量 栈区:分配在栈上的变量可被栈管理器自动释放 堆区:堆上的变量内存必须人 ...
- 《android开发艺术探索》读书笔记(十五)--Android性能优化
接上篇<android开发艺术探索>读书笔记(十四)--JNI和NDK编程 No1: 如果<include>制定了这个id属性,同时被包含的布局文件的根元素也制定了id属性,那 ...
- 《android开发艺术探索》读书笔记(十四)--JNI和NDK编程
接上篇<android开发艺术探索>读书笔记(十三)--综合技术 No1: Java JNI--Java Native Interface(java本地接口),它是为了方便java调用C. ...
随机推荐
- Python基础总结之第八天开始【while循环以及for循环,循环嵌套等循环相关的知识点】(新手可相互督促)
ennnnn,年薪20万的梦想是不是又进了一步: 循环,什么是循环,循环就是电池有电,手机屏幕可以循环一整天的使用:循环就是地球不毁灭,太阳日复一日的出现...... 不接受反驳,谢谢!~ 只要条件满 ...
- vue中的axios.post使用json数据传输,出现请求头字段内容类型是不被允许的情况的解决方案
如何解决出现AXIOS的Request header field Content-Type is not allowed by Access-Control-Allow-Headers in pref ...
- SQL SERVER导入EXCEL文件:无法创建链接服务器 "(null)" 的 OLE DB 访问接口 "Microsoft.Ace.OLEDB.12.0" 的实例。
[方法一] --开启导入功能 exec sp_configure 'show advanced options',1 reconfigure exec sp_configure 'A ...
- Scrapy payload 报错400
首先Scrapy 发送payload请求格式如下: def start_requests(self): querystr = { "ctoken": "U-ang1zmp ...
- InnoDB 中的锁实现
InnoDB 中的锁实现 原贴:InnoDB 锁系统及死锁检测实现分析 InnoDB 中,所有事务加的行锁通过一个全局的 hash 表 lock_sys 维护: /* The lock system ...
- spring cloud链路追踪组件sleuth和zipkin
spring cloud链路追踪组件sleuth 主要作用就是日志埋点 操作方法 1.增加依赖 <dependency> <groupId& ...
- Java 判断字符是大写小写或者数字
使用character类 Character.isLowerCase(Schar.charAt(i)) //获取字符串Schar中的某一个字符然后借用character类的方法来判断是不是小写. 其他 ...
- shell习题第19题:最常用的命令
[题目要求] 查看使用最多的10个命令 [核心要点] history 或者 ~/.bash_history sort uniq [脚本] #!/bin/bash # history就是调用cat ~/ ...
- nginx运行基本指令
测试配置文件: 安装路径下的/nginx/sbin/nginx -t启动: 安装路径下的/nginx/sbin/nginx停止 安装路径下的/nginx/sbin/nginx -s stop 或者是: ...
- Django rest-framework框架-认证组件的简单实例
第一版 : 自己写函数实现用户认证 #models from django.db import models #用户表 class UserInfo(models.Model): user_type_ ...