主从之间的数据同步

Redis提供了主从模式, 主从库之间采用的是读写分离的方式.

  • 读操作:主库、从库都可以接收;
  • 写操作:首先到主库执行,然后,主库将写操作同步给从库。

基础篇(下) - 图1

第一次同步

在实例2上执行如下命令, 即可让实例2成为实例1的从库

  1. replicaof {实例1ip} {端口}

然后开始三阶段同步:

  1. 主从库建立连接, 从库发送psync ? -1给主库, 主库收到后, 通过FULLRESYNC命令回应

    psync {runId} {offset}, 命令中runId是redis实例启动时自动生成的随机id, 用来标识实例, runId=?表示主库runId未知, offset表示复制进度, offset=-1表示第一次复制 FULLRESYNC表示全量复制

  2. 主库执行bgsave生成RDB文件发给从库, 从库先清空当前数据库, 然后加载RDB

  3. 主库同步给从库过程中, 并不会被阻塞, 新写操作会记录在专门的replication buffer中, 当RDB文件发送完后, 会将replication buffer发给从库

基础篇(下) - 图2

通过主-从-从来分担主库全量复制的压力

主库fork子进程生成RDB文件这个操作是阻塞的, 此外传输RDB文件也会占用主库的网络带宽, 可以通过主-从-从模式来分担压力
基础篇(下) - 图3

主从网络中断

主从库之间是基于长连接的命令传播, 发生网络中断恢复后, 主从库会采用增量复制的方式继续同步.(redis2.8以后)

新写操作命令记录在replication buffer,
同时会记录在repl_backlog_buffer的环形缓冲区, 在这里, 主库会记录写到的位置, 从库会记录已读到的位置
基础篇(下) - 图4
主从库的连接恢复后, 从库会给主库发送psync命令将当前的slave_repl_offset发给主库, 主库只用把master_repl_offset和salve_repl_offset之前的操作命令同步给从库即可
基础篇(下) - 图5

但是环形缓冲区存在写满后覆盖的问题, 会导致主从库数据不一致, 一般而言, 可以调整repl_backlog_size进行调整

这里细锁一下:

  1. 断开后, 主库一直写, ```bash repl_backlog_size=缓冲空间2 缓冲空间=(主库写入命令速度-主从网络传输命令速度)操作大小

示例: 主库每秒写2000个操作, 每个操作为2KB, 网络每秒能传输1000个操作 那么缓冲空间=(2000-1000)*2KB=2MB, repl_backlog_size设为4MB

  1. 总结:<br />主从库同步有三种方式:
  2. - 全量复制
  3. - 基于长连接的命令传播
  4. - 增量复制
  5. 建议:一个 Redis 实例的数据库不要太大,一个实例大小在几 GB 级别比较合适,这样可以减少 RDB 文件生成、传输和重新加载的开销
  6. ---
  7. <a name="v3Qcg"></a>
  8. ## 哨兵机制
  9. redis主从库集群模式下, 若主库发生故障, 无法服务写操作, 此时需要选举出新的主库; 涉及到3个问题:
  10. 1. 如何判断主库挂掉?
  11. 1. 怎么选新主库?
  12. 1. 如何将新主库通知给从库以及客户端?
  13. redis的哨兵机制解决了上面3个问题, 换言之, 哨兵机制的职责在于: 监控, 选主和通知<br />![](https://cdn.nlark.com/yuque/0/2021/jpeg/281275/1620566267627-0978c25c-3ff9-44eb-9c05-201b6c1c9f14.jpeg#clientId=u322ba026-2831-4&from=paste&height=1018&id=u7febb5ef&margin=%5Bobject%20Object%5D&originHeight=1018&originWidth=2890&originalType=url&status=done&style=none&taskId=u11883105-3b40-41df-acd7-0a671eb2c7d&width=2890)
  14. <a name="xpspX"></a>
  15. ### 监控
  16. 哨兵进程会使用 PING 命令检测它自己和主、从库的网络连接情况,用来判断实例的状态。如果哨兵发现主库或从库对 PING 命令的响应超时了,那么,哨兵就会先把它标记为“主观下线”.
  17. 为了避免单个哨兵因为自身网络状况不好,而误判主库下线的情况, 引入了哨兵集群, 只有大多数的哨兵实例,都判断主库已经“主观下线”了,主库才会被标记为“客观下线”.
  18. > 客观下线”的标准就是,当有 N 个哨兵实例时,最好要有 N/2 + 1 个实例判断主库为“主观下线”
  19. ![](https://cdn.nlark.com/yuque/0/2021/jpeg/281275/1620566372784-4adfb115-03eb-4258-8bdd-d87595333c4f.jpeg#clientId=u322ba026-2831-4&from=paste&height=1416&id=u1dff11a6&margin=%5Bobject%20Object%5D&originHeight=1416&originWidth=3807&originalType=url&status=done&style=none&taskId=u5754f916-af95-47da-86f3-d6c714c062e&width=3807)
  20. <a name="P96KZ"></a>
  21. ### 选主
  22. 先筛选后打分, 最后将得分最高的从库作为主库<br />![](https://cdn.nlark.com/yuque/0/2021/jpeg/281275/1620566419306-e1d44d65-55ed-4ad2-96f7-14a1983ac0af.jpeg#clientId=u322ba026-2831-4&from=paste&height=1743&id=ube03b16e&margin=%5Bobject%20Object%5D&originHeight=1743&originWidth=3671&originalType=url&status=done&style=none&taskId=u58d12d94-cd16-4fb9-91b9-90f9ec02163&width=3671)
  23. 筛选
  24. - 检查当前在线状态
  25. - 判断之前的网络连接状态, 如使用配置项 down-after-milliseconds * 10, 如果发生断连的次数超过了 10 次,就说明这个从库的网络状况不好
  26. 打分
  27. 1. 优先级高的从库得分高
  28. 可以通过 slave-priority 配置项,给不同的从库设置不同优先级, 参数越小, 优先级越高.
  29. 2. 和旧主库同步程度最接近的从库得分高
  30. slave_repl_offset最大的从库
  31. 3. ID 号小的从库得分高
  32. 每个实例都会有一个 ID,这个 ID 就类似于这里的从库的编号
  33. ---
  34. <a name="GHUuD"></a>
  35. ## 哨兵集群
  36. 配置哨兵
  37. ```json
  38. sentinel monitor <master-name> <ip> <redis-port> <quorum>

哨兵之间的相互发现

哨兵之间的相互发现是基于Redis 提供的 pub/sub 机制,也就是发布 / 订阅机制。基础篇(下) - 图6

哨兵获取从库信息

由哨兵向主库发送 INFO 命令来完成的
基础篇(下) - 图7

哨兵提供的事件通知

从本质上说,哨兵就是一个运行在特定模式下的 Redis 实例,只不过它并不服务请求操作,只是完成监控、选主和通知的任务。所以,每个哨兵实例也提供 pub/sub 机制,客户端可以从哨兵订阅消息。哨兵提供的消息订阅频道有很多,不同频道包含了主从库切换过程中的不同关键事件。基础篇(下) - 图8

哨兵选举

确定由哪个哨兵执行主从切换的过程,和主库“客观下线”的判断过程类似,也是一个“投票仲裁”的过程。

在投票过程中,任何一个想成为 Leader 的哨兵,要满足两个条件:第一,拿到半数以上的赞成票;第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。
基础篇(下) - 图9
基础篇(下) - 图10

经验

需要注意的是,如果哨兵集群只有 2 个实例,此时,一个哨兵要想成为 Leader,必须获得 2 票,而不是 1 票。所以,如果有个哨兵挂掉了,那么,此时的集群是无法进行主从库切换的。因此,通常我们至少会配置 3 个哨兵实例。这一点很重要,你在实际应用时可不能忽略了。

要保证所有哨兵实例的配置是一致的,尤其是主观下线的判断值 down-after-milliseconds。我们曾经就踩过一个“坑”。当时,在我们的项目中,因为这个值在不同的哨兵实例上配置不一致,导致哨兵集群一直没有对有故障的主库形成共识,也就没有及时切换主库,最终的结果就是集群服务不稳定。所以,你一定不要忽略这条看似简单的经验。


切片集群

当需要保存的数据量非常大时, redis有纵向拓展和横向拓展两种方案

  • 纵向拓展: 增加实例的资源配置, 优点是简单直接, 缺点是数据量过大, RDB快照在fork子进程会长时间阻塞, 且会受到硬件以及成本的限制
  • 横向拓展: 采用多实例分散存储数据

基础篇(下) - 图11

多实例的数据分布

从 3.0 开始,官方提供了一个名为 Redis Cluster 的方案,用于实现切片集群.
Redis Cluster 方案采用哈希槽(Hash Slot,接下来我会直接称之为 Slot),来处理数据和实例之间的映射关系。

在 Redis Cluster 方案中,一个切片集群共有 16384 个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中。

具体的映射过程分为两大步:首先根据键值对的 key,按照CRC16 算法计算一个 16 bit 的值;然后,再用这个 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽

在部署 Redis Cluster 方案时,可以使用 cluster create 命令创建集群,此时,Redis 会自动把这些槽平均分布在集群实例上。例如,如果集群中有 N 个实例,那么,每个实例上的槽个数为 16384/N 个。

也可以根据不同实例的资源配置情况, 手动使用cluster addslots命令分配哈希槽,
需要注意的是, 在手动分配哈希槽时,需要把 16384 个槽都分配完,否则 Redis 集群无法正常工作。

  1. redis-cli -h 172.16.19.3 p 6379 cluster addslots 0,1
  2. redis-cli -h 172.16.19.4 p 6379 cluster addslots 2,3
  3. redis-cli -h 172.16.19.5 p 6379 cluster addslots 4

基础篇(下) - 图12

客户端定位数据

  1. redis实例之间建立连接后, 会将自己的哈希槽信息共享, 因此每个实例都拥有所有哈希槽的映射关系
  2. 客户端与集群实例建立连接后, 实例会将哈希槽信息发给客户端, 客户端因此知道所有哈希槽信息

经过以上两步, 客户端会将哈希槽信息缓存在本地, 当客户端请求键值时, 会先计算键所在的哈希槽, 然后再给相应的实例发送请求.

实例与哈希槽的对应关系存在变更的情况, 如:

  • 集群中的实例新增或删除, redis需要重新分配哈希槽
  • 为了负载均衡, 需要重新分布

对此, 实例之间可以通过互相通信获取最新的哈希槽分配信息

对于客户端, Redis Cluster 方案提供了一种重定向机制
情况1: slot2已由实例2迁移至实例3, 实例2会返回MOVED命令, 客户端重新发送请求到实例3, 并更新本地缓存

  1. GET hello:key
  2. (error) MOVED 13320 172.16.19.5:6379

基础篇(下) - 图13

情况2: slot2正由实例2迁移至实例3, 其中key2已迁移, 实例2会返回ASK命令, 客户端先发送ASKING命令到实例3, 然后再发送操作命令, 这时并不更新客户端本地哈希槽缓存

  1. GET hello:key
  2. (error) ASK 13320 172.16.19.5:6379

基础篇(下) - 图14

补充:
哈希槽可以将数据和节点解耦, 数据只需要关系映射到哪个槽, 再通过槽与节点的映射表找到节点, 不但使数据分布更加均匀, 而且使映射表变得很小(如果是数据与节点的映射将会非常大), 利于映射关系的保存以及网络传输. 此外, 也简化了节点扩容, 缩容的难度.


补充

从操作系统的角度来看,进程一般是指资源分配单元,例如一个进程拥有自己的堆、栈、虚存空间(页表)、文件描述符等;而线程一般是指 CPU 进行调度和执行的实体。