主从之间的数据同步
Redis提供了主从模式, 主从库之间采用的是读写分离的方式.
- 读操作:主库、从库都可以接收;
- 写操作:首先到主库执行,然后,主库将写操作同步给从库。
第一次同步
在实例2上执行如下命令, 即可让实例2成为实例1的从库
replicaof {实例1的ip} {端口}
然后开始三阶段同步:
主从库建立连接, 从库发送
psync ? -1
给主库, 主库收到后, 通过FULLRESYNC
命令回应psync {runId} {offset}
, 命令中runId
是redis实例启动时自动生成的随机id, 用来标识实例,runId=?
表示主库runId
未知,offset
表示复制进度,offset=-1
表示第一次复制FULLRESYNC
表示全量复制主库执行bgsave生成RDB文件发给从库, 从库先清空当前数据库, 然后加载RDB
- 主库同步给从库过程中, 并不会被阻塞, 新写操作会记录在专门的replication buffer中, 当RDB文件发送完后, 会将replication buffer发给从库
通过主-从-从来分担主库全量复制的压力
主库fork子进程生成RDB文件这个操作是阻塞的, 此外传输RDB文件也会占用主库的网络带宽, 可以通过主-从-从模式来分担压力
主从网络中断
主从库之间是基于长连接的命令传播, 发生网络中断恢复后, 主从库会采用增量复制的方式继续同步.(redis2.8以后)
新写操作命令记录在replication buffer,
同时会记录在repl_backlog_buffer的环形缓冲区, 在这里, 主库会记录写到的位置, 从库会记录已读到的位置
主从库的连接恢复后, 从库会给主库发送psync命令将当前的slave_repl_offset发给主库, 主库只用把master_repl_offset和salve_repl_offset之前的操作命令同步给从库即可
但是环形缓冲区存在写满后覆盖的问题, 会导致主从库数据不一致, 一般而言, 可以调整repl_backlog_size进行调整
这里细锁一下:
- 断开后, 主库一直写, ```bash repl_backlog_size=缓冲空间2 缓冲空间=(主库写入命令速度-主从网络传输命令速度)操作大小
示例: 主库每秒写2000个操作, 每个操作为2KB, 网络每秒能传输1000个操作 那么缓冲空间=(2000-1000)*2KB=2MB, repl_backlog_size设为4MB
总结:<br />主从库同步有三种方式:
- 全量复制
- 基于长连接的命令传播
- 增量复制
建议:一个 Redis 实例的数据库不要太大,一个实例大小在几 GB 级别比较合适,这样可以减少 RDB 文件生成、传输和重新加载的开销
---
<a name="v3Qcg"></a>
## 哨兵机制
在redis主从库集群模式下, 若主库发生故障, 无法服务写操作, 此时需要选举出新的主库; 涉及到3个问题:
1. 如何判断主库挂掉?
1. 怎么选新主库?
1. 如何将新主库通知给从库以及客户端?
redis的哨兵机制解决了上面3个问题, 换言之, 哨兵机制的职责在于: 监控, 选主和通知<br />
<a name="xpspX"></a>
### 监控
哨兵进程会使用 PING 命令检测它自己和主、从库的网络连接情况,用来判断实例的状态。如果哨兵发现主库或从库对 PING 命令的响应超时了,那么,哨兵就会先把它标记为“主观下线”.
为了避免单个哨兵因为自身网络状况不好,而误判主库下线的情况, 引入了哨兵集群, 只有大多数的哨兵实例,都判断主库已经“主观下线”了,主库才会被标记为“客观下线”.
> 客观下线”的标准就是,当有 N 个哨兵实例时,最好要有 N/2 + 1 个实例判断主库为“主观下线”

<a name="P96KZ"></a>
### 选主
先筛选后打分, 最后将得分最高的从库作为主库<br />
筛选
- 检查当前在线状态
- 判断之前的网络连接状态, 如使用配置项 down-after-milliseconds * 10, 如果发生断连的次数超过了 10 次,就说明这个从库的网络状况不好
打分
1. 优先级高的从库得分高
可以通过 slave-priority 配置项,给不同的从库设置不同优先级, 参数越小, 优先级越高.
2. 和旧主库同步程度最接近的从库得分高
选slave_repl_offset最大的从库
3. ID 号小的从库得分高
每个实例都会有一个 ID,这个 ID 就类似于这里的从库的编号
---
<a name="GHUuD"></a>
## 哨兵集群
配置哨兵
```json
sentinel monitor <master-name> <ip> <redis-port> <quorum>
哨兵之间的相互发现
哨兵之间的相互发现是基于Redis 提供的 pub/sub 机制,也就是发布 / 订阅机制。
哨兵获取从库信息
由哨兵向主库发送 INFO 命令来完成的
哨兵提供的事件通知
从本质上说,哨兵就是一个运行在特定模式下的 Redis 实例,只不过它并不服务请求操作,只是完成监控、选主和通知的任务。所以,每个哨兵实例也提供 pub/sub 机制,客户端可以从哨兵订阅消息。哨兵提供的消息订阅频道有很多,不同频道包含了主从库切换过程中的不同关键事件。
哨兵选举
确定由哪个哨兵执行主从切换的过程,和主库“客观下线”的判断过程类似,也是一个“投票仲裁”的过程。
在投票过程中,任何一个想成为 Leader 的哨兵,要满足两个条件:第一,拿到半数以上的赞成票;第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。
经验
需要注意的是,如果哨兵集群只有 2 个实例,此时,一个哨兵要想成为 Leader,必须获得 2 票,而不是 1 票。所以,如果有个哨兵挂掉了,那么,此时的集群是无法进行主从库切换的。因此,通常我们至少会配置 3 个哨兵实例。这一点很重要,你在实际应用时可不能忽略了。
要保证所有哨兵实例的配置是一致的,尤其是主观下线的判断值 down-after-milliseconds。我们曾经就踩过一个“坑”。当时,在我们的项目中,因为这个值在不同的哨兵实例上配置不一致,导致哨兵集群一直没有对有故障的主库形成共识,也就没有及时切换主库,最终的结果就是集群服务不稳定。所以,你一定不要忽略这条看似简单的经验。
切片集群
当需要保存的数据量非常大时, redis有纵向拓展和横向拓展两种方案
- 纵向拓展: 增加实例的资源配置, 优点是简单直接, 缺点是数据量过大, RDB快照在fork子进程会长时间阻塞, 且会受到硬件以及成本的限制
- 横向拓展: 采用多实例分散存储数据
多实例的数据分布
从 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 集群无法正常工作。
redis-cli -h 172.16.19.3 –p 6379 cluster addslots 0,1
redis-cli -h 172.16.19.4 –p 6379 cluster addslots 2,3
redis-cli -h 172.16.19.5 –p 6379 cluster addslots 4
客户端定位数据
- redis实例之间建立连接后, 会将自己的哈希槽信息共享, 因此每个实例都拥有所有哈希槽的映射关系
- 客户端与集群实例建立连接后, 实例会将哈希槽信息发给客户端, 客户端因此知道所有哈希槽信息
经过以上两步, 客户端会将哈希槽信息缓存在本地, 当客户端请求键值时, 会先计算键所在的哈希槽, 然后再给相应的实例发送请求.
实例与哈希槽的对应关系存在变更的情况, 如:
- 集群中的实例新增或删除, redis需要重新分配哈希槽
- 为了负载均衡, 需要重新分布
对此, 实例之间可以通过互相通信获取最新的哈希槽分配信息
对于客户端, Redis Cluster 方案提供了一种重定向机制
情况1: slot2已由实例2迁移至实例3, 实例2会返回MOVED命令, 客户端重新发送请求到实例3, 并更新本地缓存
GET hello:key
(error) MOVED 13320 172.16.19.5:6379
情况2: slot2正由实例2迁移至实例3, 其中key2已迁移, 实例2会返回ASK命令, 客户端先发送ASKING命令到实例3, 然后再发送操作命令, 这时并不更新客户端本地哈希槽缓存
GET hello:key
(error) ASK 13320 172.16.19.5:6379
补充:
哈希槽可以将数据和节点解耦, 数据只需要关系映射到哪个槽, 再通过槽与节点的映射表找到节点, 不但使数据分布更加均匀, 而且使映射表变得很小(如果是数据与节点的映射将会非常大), 利于映射关系的保存以及网络传输. 此外, 也简化了节点扩容, 缩容的难度.
补充
从操作系统的角度来看,进程一般是指资源分配单元,例如一个进程拥有自己的堆、栈、虚存空间(页表)、文件描述符等;而线程一般是指 CPU 进行调度和执行的实体。