第三部分 多机数据库的实现
复制
1.旧版复制功能的实现
旧版复制分为两个阶段 :
同步
和命令传播
-
同步过程的执行步骤
- 从服务器向主服务器发送SYNC命令
- 收到SYNC命令后,主服务器开始执行BGSAVE操作生成RDB文件,并使用一个缓冲区记录现在开始执行的所有写命令(用于命令传播阶段保持数据库一致性)。
- 当主服务器的BGSAVE操作执行完时,主服务器会将BGSAVE命令生成的RDB文件发送给从服务器,从服务器接收并载入这个RDB文件,将自己的数据库状态更新至主服务器执行BGSAVE命令时的数据库状态。
- 主服务器将记录在缓冲区里面的所有写命令发送给从服务器,从服务器执行这些写命令。将自己数据库状态更新至主服务器数据库当前状态。
-
命令传播的过程
- 主服务器将执行的写命令,发送给从服务器执行,当从服务器执行了相同写命令后,主从服务器将再次回到一致性状态
-
SYNC命令流程图
2.旧版复制功能的缺陷
- 在从服务器A
断线
的情况(此时保存了A1 - A99键值对),当服务器重连上主服务器时 (此时主服务器保存到了A120), 从服务器会再次向主服务器发送SYNC命令,此时主服务器又需要将数据库的所有键值对保存到RDB文件中(A1-A120键值对),然而从服务器A其实只需要掉线时期产生的A100-A120数据就能恢复成一致性状态。 所以为了让从服务器补足一小部分缺失的数据,而让主服务器重新执行一次BGSAVE操作,造成断线重连后的效率会很低。
3.新版复制功能的实现
- 如何提高断线重连后同步的效率呢? 实际上我们只需要保存断线后的那部分写命令,重连后让主服务器再发送过来给从服务器执行就好了。
- 新版复制解决了旧版复制在处理断线重连时的低效情况。使用PSYNC命令代替SYNC命令执行复制时的同步操作。
- PSYNC命令具有
完整重同步
和部分重同步
两种模式- 完整重同步用于处理初次复制的情况:与SYNC命令的执行步骤基本一致 , 都是通过让主服务器创建并发送RDB文件,以及向从服务器发送保存在缓冲区里面的写命令来进行同步。
- 而部分重同步用于处理断线后重复制的情况:当从服务器在断线后重新连接主服务器时,如果条件允许,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些写命令,就可以将数据库更新至主服务器当前所在状态了。
- 由于部分重同步只需要将服务器缺少的写命令发送给从服务器执行旧可以了,因此所需资源更少,速度更快。
4.部分重同步的实现
-
复制偏移量
主服务器和从服务器都维护一个复制偏移量。 主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量的值加上N。
从服务器每次收到主服务器传播来的N个字节的数据时,就将自己的复制偏移量加上N
-
可以很容易
通过判断主从服务器的复制偏移量来判断是否处于一致的状态
如果发现偏移量不一致时,如何判断接下来要进行完整重同步 or 部分重同步。主服务器又如何补偿掉线期间丢失的数据呢?与复制积压缓冲区有关。
-
复制积压缓冲区
-
复制积压缓冲区维护一个固定长度的先进先出队列
,默认大小为1MB -
当服务器进行命令传播时,不仅将命令发送给所有从服务器,还将写命令入队到复制积压缓冲区中
当从服务器重连主服务器时,从服务器通过PSYNC命令将复制偏移量offset也发送给主服务器,如果offset偏移量之后的数据仍存在复制积压缓冲区中,则执行部分重同步操作 ; 而如果offset偏移量之后的数据已不存在复制积压缓冲区中(掉线太久),则执行完整重同步操作。
复制积压缓冲区的最小大小应为 平均重连时间 * 主服务器每秒产生的写命令数量 ; 为了保证大部分掉线情况都能使用部分重同步, 应将复制积压缓冲区设为 2*平均重连时间 * 主服务器每秒产生的写命令数量
-
-
服务器运行ID
- 用于记录判断 断线重连后的服务器是否为 原先连接的服务器
- 当从服务器对主服务器进行初次复制时,主服务器会将自己的运行ID传送给从服务器,而从服务器则会将这个ID保存起来。 当从服务器重新连接时,从服务器向主服务器发送之前保存的服务器运行ID
- 如果从服务器保存的ID和 当前连接的主服务器运行ID相同,则说明断线前复制的就是当前的主服务器,则再根据复制积压缓冲区判断进行部分重同步 还是 完整重同步
- 相反,则说明线前复制的不是当前的主服务器,则需要进行完整重同步操作。
5.PSYNC命令的实现
- PSYNC <runid> <offset> : runid表示上一次复制的主服务器运行ID , offset表示当前复制偏移量
- 主服务器返回 FULLRESYNC <runid> <offset> : 表示执行完整重同步操作,offset作为从服务器的初始偏移量
- 主服务器返回 CONTINUE : 表示执行部分重同步操作。 只需等待主服务器将缺失数据重发即可。
6.复制的实现
SLAVEOF <master_ip> <master_port>
- 设置主服务器的地址和端口号
- 建立套接字连接(让从服务器成为主服务器客户端)
- 发送PING命令 (检测主服务器连接状况)
- 身份验证
- 发送端口信息(让主服务器保存从服务器的相关信息)
- 同步(发送PSYNC命令,执行同步操作。 并且由单工通信编程双工通信,即主从服务器互相成为对方的客户端)
- 命令传播
- 心跳检测(从服务器会每秒1次的频率,向主服务器发送命令
REPLCONF ACK <从服务器偏移量>
)- 检测主从服务器网络连接状态
- 复制实现min-slaves功能 (当主服务器的从服务器数量小于min-slaves时,则认为集群挂了)
- 检测命令丢失(主服务器进行部分重同步时由于网络问题而丢失,则通过心跳检测到后进行补发)
总结
- 当客户端向从服务器发送SLAVEOF命令,要求从服务器复制主服务器时,从服务器首先需要执行同步操作。将从服务器的数据库丛台更新至主服务器当前所处的数据库状态。 在同步操作完成后,这种一致性的状态不是一成不变的,当主服务器执行客户端的写命令时,主从服务器的状态旧不再一致, 因此主服务器需要对从服务器执行命令传播操作, 即主服务器将执行的写命令,发送给从服务器执行。
- Redis 2.8以前的复制功能不能高效地处理断线后重复制情况,但Redis2.8新添加的部分重同步功能可以解决这个问题。
- 部分重同步通过
复制偏移量
、复制积压缓冲区
、服务器运行
ID三个部分来实现。 - 在复制操作刚开始的时候,从服务器会成为主服务器的客户端,并通过向主服务器发送命令请求来执行复制步骤。 而在复制的后期(同步阶段),主从服务器会互相成为对方的客户端 (主服务器发送命令(命令传播,部分重同步)给从服务器来维护数据一致性)
- 主服务器通过向从服务器传播命令来更新从服务器的状态,保证主从服务器一致,而从服务器则通过向主服务器发送命令来进行心跳检测,以及命令丢失检测。
Sentinel哨兵机制
Sentinel是Redis的高可用性解决方案:由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及这些主服务器下的从服务器,并在被监视的主服务器进行下线状态时,自动将被下线主服务器下的某个重服务器升级为主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求(失效转移)
Sentinel的构成
-
Setinel启动时的执行步骤
- 初始化服务器(Sentinel本质上只是运行在特殊模式下的Redis服务器, 部分初始化过程不相同)
- 将普通Redis服务器使用的代码替换成Sentinel专用代码(如没有get,set 而有ping,INFO)
- 初始化Sentinel状态
- 初始化Sentinel的监视主服务器列表
- 创建连向主服务器的网络连接 (创建命令连接和订阅连接; 前者用于向主服务器发送命令 。 而后者是订阅主服务器的 _sentinel_hello频道,用于接收其他主服务器信息的变更的频道信息)
Setineal通过读取配置文件,使用字典记录所有的Master; 并通过给主服务器发送INFO命令,得出主服务器下的所有从服务器信息。
如何获取主服务器信息
- Sentinel默认会以每10秒一次的频率,通过命令连接向被监视的主服务器发送INFO命令,并通过分析INFO命令的回复来获取主服务器的当前信息(状态,从服务器slaves信息)
如何获取从服务器信息
-
当Sentinel发现主服务器有新的从服务器出现时,Sentinel除了会为这个新的从服务器创建相应实例结构外,Sentinel还会创建连接到从服务器的命令连接和订阅连接
-
创建命令连接后,每10秒向从服务器发送INFO命令
- 获取从服务器的运行ID,角色,优先级,偏移量, 该从服务器对应的主服务器连接信息,状态
-
向主从服务器发送消息
在默认情况下,Sentinel会以每两秒一次的频率,通过命令连接向所有被监视的主服务器和从服务器发送订阅消息
PUBLISH sentinel_:hello <sentinel的信息,主服务器的信息>
接收来自主服务器和从服务器的频道信息
当Sentinel与一个主服务器或者从服务器建立建立起订阅连接之后,Sentinel就会通过订阅连接,向服务器发送以下命令。一直持续到Sentinel与服务器的连接断开为止。
对于监视同一个服务器的多个Sentinel来说,一个Sentinel发送的信息会被其他Sentinel接收到,这些信息会被用于更新其他Sentinel对发送信息Sentinel的认知,也会被用于更新其他Sentinel对被监视服务器的认知
检测主观下线状态
- 在默认情况下,Sentinel会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器,从服务器,其他Sentinel在内)发送PING命令,并通过实例返回的PING命令回复来判断实例是否在线。
- Sentinel配置文件中的
down-after-milliseconds
指定了判断实例主观下线所需的时间长度。如果在down-after-milliseconds时间内连续发送了无效回复,则Sentinel将判定该实例主观下线,并将对应的实例结构的flags设置为 SRI_S_DOWN。 (Subjective 主观)
检测客观下线状态
- 当Sentinel将一个主服务器判断为
主观下线后
,为了确认这个主服务器是否真的下线了,它会向同样监视这一主服务器的其他Sentinel进行询问,看它们是否也认为主服务器已经进入了下线状态。 当Sentinel从其他Sentinel那里接收到足够数量的已下线判断后,Sentinel就会将从服务器判断为客观下线
,并将主服务器执行故障转移
操作。 - 当判断为客观下线后,将flags设置为 SRI_O_DOWN (Objective 客观)
如何选举领头Setinel
- 当一个主服务器被判断为客观下线时,监视这个下线主服务器的各个Sentinel会进行协商,选举出一个领头Sentinel,并由领头Sentinel对下线主服务器执行故障转移操作。
##请求格式
Sentinel is-master-down-by-addr <master的ip地址> <端口号> <配置纪元>(标志是否同一轮选举) <SentinelId>
##响应格式
<上下线状态> <局部领头id> <当前纪元>
故障转移
- 在已下线主服务器属下的所有从服务器里面,挑选出一个从服务器,并将其转换为主服务器(升级)
- 让已下线主服务器属下的所有从服务器改为复制新的主服务器
- 将已下线主服务器设置为新的主服务器的从服务器,当这个旧的服务器重新上线时,它就会成为新的主服务器的从服务器(降级)
集群
Redis集群是Redis提供的分布式数据库方案,集群通过分片来进行数据共享,并提供复制和故障转移功能。
主要内容有集群中的节点,槽指派,命令执行,重新分片,转向,故障转移,消息等方面
节点通过握手来将其他节点添加到自己所处的集群当中 (clustermeet ip port)
集群中的16384个槽可以分别指派给集群中的各个节点,每个节点都会记录哪些槽指派给了自己,哪些槽又被指派给了其他节点。
clusterState.slots数组记录了集群中所有槽的指派信息,使得能以O(1)时间判断某个槽指派给了哪个节点;而clusterNode.slots数组只记录了clusterNode结构所代表的节点的槽指派信息。使得交换槽信息时只需发送clusterNode.slots数组,让接收信息的节点更新字典表中对应的clusterNode即可。而不用遍历clusterState中所有元素,标志当前节点所指派的槽,再将其信息进行传送,时间复杂度为O(n)。下图为节点7000创建的clusterState结构。
节点在接收到一个命令请求时,会先检查这个命令请求要处理的键所在的槽是否由自己负责(查看clusterState.slots数组),如果不是的话,节点将向客户端返回一个MOVED操作, MOVED错误携带的信息可以指引客户端转向至正在负责相关槽的节点。
节点还会使用跳跃表来保存槽与键之间的关系 (方便比如 查看 属于某个slot的数据库键由哪些的命令)
-
对Redis集群的重新分片工作是由redis-trib负责的,重新分片的关键是将属于某个槽的所有键值对从一个节点转移到另一个节点。
如果节点A正在迁移槽i到节点B,那么当节点A没能在自己的数据库中找到命令指定的数据库键时,节点A会向客户端返回一个ASK错误,指引客户端到节点B继续查找指定的数据库键。
MOVED错误表示槽的负责权已经从一个节点转移到了另一个节点,而ASK错误只是两个节点在迁移槽的过程中使用的一种临时措施。
-
集群里的从节点用于复制主节点,并在主节点下线时,代替主节点继续处理请求。
集群中的节点通过发送和接收消息来通信,常见的消息包括MEET,PING,PONG,PUBLISH,FAIL五种。