Redis 多机数据库
主从复制
通过向服务器发送 SLAVEOF <master_ip> <master_port>
命令,可以让其变成一个从服务器,去复制主服务器。在真正开始同步前,会有以下操作:
- 将主服务器的地址和端口信息保存到
redisServer
结构体的属性中 - 和主服务器建立套接字连接
- 发送PING命令检查主服务器能否正常处理命令请求。如果不行则会重新创建套接字
- 身份验证。如果主从服务器都未设置身份验证,或者都设置了身份验证且密码相同才能验证通过,否则验证不通过
- 从服务器向主服务器发送端口信息,主服务器将其记录在
redisClient
的slave_listening_port
字段中。因为之后主服务器需要向从服务器发送命令,所以需要端口信息
接下来就会进行复制操作,可以分为同步和命令传播两个操作。同步操作是为了将从服务器的数据库状态更新至主服务器当前所处的数据库状态,命令传播操作是为了主服务器状态被修改时,让两者的数据库状态保持一致。
同步操作可以分为完整重同步(full resynchronization)和部分重同步(partial resynchronization)两种模式,前者用于初次复制的情况,后者用于处理断线后复制的情况
- 完整重同步时,从服务器会向主服务器发送
PSYNC ? -1
命令。主服务器会返回+FULLRESYNC <runid> <offset>
,然后进行完整重同步。主服务器会执行BGSAVE
命令,在后台生成RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令。生成完RDB文件后,主服务器会将其发送给从服务器,从服务器接收并载入。然后主服务器将记录在缓冲区里的所有写命令发送给从服务器让从服务器执行,使从服务器的状态更新为主服务器的状态 - 部分重同步时,从服务器会向主服务器发送
PSYNC <runid> <offset>
命令,其中runid
就是之前主服务器发送回来的主服务器的运行id,主服务器会判断该id是否是自己的运行id(每个服务器在启动时会生成自己的运行id),如果相同说明从服务器之前复制的就是自己,如果不是则说明需要进行完整重同步操作。offset
代表复制偏移量,主服务器每传播N个字节的数据,就将自己的复制偏移量加N;从服务器每收到N个字节的数据,将自己的复制偏移量加N。
主服务器进行命令传播时,不仅会将写命令发送给所有从服务器,还会将其写入一个固定长度的先进先出队列,叫做复制积压缓冲区,默认大小为1M。当从服务器发出部分重同步请求时,主服务器会检查从服务器发送过来的offset
+1的偏移量是否还在复制积压缓冲区里,如果存在,则执行部分重同步操作,将复制积压缓冲区里offset之后的数据都发给从服务器;如果不存在,那么将执行完整重同步操作。
在命令传播阶段,从服务器会以默认一秒的频率,向主服务器发送命令:REPLCONF ACK <replication_offset>
,其中replication_offset
是从服务器当前的复制偏移量,该命令主要有三个作用:
- 心跳检测,检测主从服务器之间的连接状态,检测lag
- 辅助实现
min-slaves
选项。即lag大于多少时,主服务器拒绝执行写命令 - 检测命令丢失,主服务器会将丢失的命令从复制积压缓冲区里找到,再次发送给从服务器
Sentinel
Sentinel 系统由一个或多个 Sentinel 实例组成,负责监视任意多个主服务器,以及这些主服务器下面的所有从服务器。当主服务器下线时,Sentinel会自动将其下面的某个从服务器升级为主服务器,并让其他的从服务器成为新的主服务器的从服务器;Sentinel还会继续监视之前下线的主服务器,如果重新上线,会将其设置为新的主服务器的从服务器。
启动一个Sentinel实例时,本质上就是启动一个特殊的Redis服务器,不过有一些地方不同,比如不会载入AOF或RDB,不支持很多命令比如Get、Set等;监视的主服务器的信息会从配置文件初始化到一个字典中进行保存,字典的键是主服务器的名字,值是一个sentinelRedisInstance
结构,该结构代表一个被该Sentinel监视的Redis服务器实例,可以是主服务器、从服务器或者另一个Sentinel
typedef struct sentinelRedisInstance {
int flags; // 比如主服务器为SRI_MASTER,从服务器为SRI_SLAVE
char *name; // 实例的名字,主服务器在配置文件中设置,从服务器自动设置,格式为ip:port
char *runid;
uint64_t config_epoch; // raft选举使用
sentinelAddr *addr; // 实例地址,记录了ip, port
mstime_t down_after_period; // 实例无响应多少ms后被判断为主观下线
int quorum; // 判断实例客观下线需要的投票数量
// ...
} sentinelRedisInstance;
这些属性初始化完成之后,Sentinel会创建向被监视主服务器的网络连接,一个是命令连接用于向其发送命令;一个是订阅连接,专门用于订阅__sentinel__:hello
频道。
Sentinel会默认以每10s一次的频率,通过命令INFO
获取主服务器的信息,包括run_id
,以及主服务器属下所有从服务器的信息等,并对相应的信息进行更新。从服务器的信息记录在主服务器的sentinelRedisInstance
结构中的slaves
字典中。Sentinel还会创建和从服务器的命令连接和订阅连接,也是默认每10s发送INFO
命令获取从服务器的信息。
如果有多个Sentinel实例在监视同一台服务器,那么这些Sentinel可以通过订阅频道互相发现对方。Sentinel默认会以2s的频率向订阅频道发送命令,包含了自身的信息和监视的主服务器的信息。其他Sentinel收到这些信息后,如果不是自身发送的信息,则会提取并更新信息。每个Sentinel实例都用一个sentinels字典保存了其他Sentinel实例的信息。除此之外,Sentinel实例之间还会互相建立命令连接,可以互相发送命令请求。因此,最终监视同一主服务器的多个Sentinel将会互相连接。
当Sentinel将一个主服务器判断为主观下线之后(down_after_period
属性),它会向其他Sentinel询问,以确认该服务器是否客观下线。当确认该服务器下线的Sentinel数量达到quorum
配置的数量时,Sentinel会将该主服务器的flag标记为客观下线状态。不同Sentinel判断主观下线、客观下线的条件都可能不同。
当主服务器被判断为客观下线时,整个Sentinel系统会选举出一个领头Sentinel,对其进行故障转移操作。选举的方式就是raft算法,半数以上的支持就能成为领头Sentinel。领头Sentinel接下来会对下线的主服务器进行故障转移操作,包括以下步骤:
- 在从服务器里挑选出新的主服务器。会先排除掉掉线、最近5s内没有回复过Sentinel、与已下线主服务器断开连接过长的从服务器;然后在剩下的从服务器中,选出优先级最高的,优先级为,配置好的优先级、复制偏移量最大的、run_id
- 选好之后,会向其发送
slaveof no one
的命令,之后会以每秒一次的频率(之前是每10s一次)发送INFO
命令观察其是否已经升级为主服务器 - 接下来让其他从服务器开始复制新的主服务器
- 最后,还会监视原先已下线的主服务器,当其重新上线时,使其成为新的主服务器的从服务器
集群
一个Redis集群由多个节点(node)组成,一个节点就是一个运行在集群模式下的Redis服务器,大部分功能都和单机模式下的服务器相同。使用cluster meet <ip> <port>
命令可以将新的节点添加到集群。
每个节点都有一个clusterState
结构保存了自身视角下集群的一些信息,比如其中用一个字典保存了集群中所有节点的信息clusterNode
集群通过分片的方式来保存数据库中的键值对,集群中的节点只能使用0号数据库,整个数据库被分为16384个槽(slot),用2048个字节保存,每个节点处理0~16384个槽,数据库中的每个键都会对应到16384个槽的其中一个,分配到对应节点进行处理。当16384个槽都有节点对应时,集群处于上线状态,否则处于下线状态。使用cluster addslots <slots...>
可以将多个槽指派给节点,不能将已经指派的槽指派给其他节点。
在保存节点信息的clusterNode
结构中,有一个二进制位数组slots
保存了该节点负责的槽信息,在该数组索引i上的值为1表示该节点负责处理槽i,因此保存和检查某个节点的槽信息的复杂度都是O(1)。当节点之间发送消息时,消息头里会带上自己的槽指派信息,因此节点可以利用消息来更新保存的其他节点的槽信息。
除了clusterNode
保存了每个节点的槽信息外,clusterState
结构中也用一个slots
数组保存了16384个槽对应的节点信息,索引i指向一个clusterNode
结构,指向的节点管理槽i。这样记录也是为了高效的查找槽i是否被指派或者负责槽i的节点。
当客户端发送与键值对有关的命令时,节点会先计算出给定键属于哪个槽:CRC16(key) & 16383
,然后判断该槽是否由自己负责,如果是则执行命令,不是则会返回MOVED
错误,将客户端重定向至正确的节点(客户端不会显示报错,而是会自动重定向后发送命令)。
集群的重新分片操作可以对各个节点管理的槽进行重新分配,并且相关槽所属的键值对也会进行转移,重新分片操作可以在线进行,是由 redis-trib 负责执行的。对单个槽进行重新分片的操作如下:
- 目标节点准备导入槽的键值对
- 源节点准备迁移槽的键值对
- 将源节点的每个键值对迁移到目标节点
- 迁移完成后,将槽指派给目标节点
在迁移过程中,如果客户端请求的键刚好属于被迁移的槽,那么首先会在源节点中查找,找到的话执行客户端命令,如果没有找到,则会向客户端返回ASK
错误,将客户端重定向到目标节点(同样,客户端不会显示报错)。客户端会先向目标节点发送一个ASKING
命令,然后才会发送原本的命令,因为这时这个槽还没有指派给目标节点,正常情况下目标节点会返回MOVED
错误,先发送ASKING
命令可以让目标节点继续执行这个槽的命令。
Redis 集群中的节点分为主节点和从节点,前者用于处理槽,后者用于复制某个主节点,并在主节点下线时代替主节点。集群中的每个节点都会定期向其他节点发送PING消息,如果规定时间内没有收到PONG回复,则会标记为疑似下线。集群中的各个节点会互相通过消息交换各个节点的状态信息,当一个主节点通过消息得知半数以上的主节点都将某个节点x标记为疑似下线时,该主节点就会将这个节点x的状态标记为已下线,并向集群广播一条节点x的FAIL消息,所有收到这条FAIL消息的节点都会将节点x标记为已下线。
当一个主节点已下线时,其下属的从节点会广播要求所有具有投票权的主节点投票,还是raft算法,最终会有一个从节点被选举成新的主节点,将已下线主节点的槽全部指派给自己,然后向集群广播一条PONG消息,让其他节点知道它已经由从节点变成了主节点,故障转移完成。
最后介绍一下节点之间的消息,前面说过节点通过消息头可以带上自己保存的槽信息,各个节点会互相通过消息交换各个节点的状态信息,等等。消息主要有以下五种:
- MEET消息
- PING消息
- PONG消息
- FAIL消息
- PUBLISH消息
其中,在节点之间发送MEET、PING和PONG消息时,发送者都会从自己的已知节点中随机选出两个节点,并将这两个节点的信息发送出去,因此,接收者可以更新对于这些节点的认知,比如如果是新节点则记录下来,已知节点则更新信息,比如是否为疑似下线等。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: Redis 单机数据库
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论