Skip to content

Redis基础(二)

主从之间的数据同步

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

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

第一次同步

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

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

然后开始三阶段同步:

  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发给从库 1744964433020

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

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

主从网络中断

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

新写操作命令记录在replication buffer, 同时会记录在repl_backlog_buffer的环形缓冲区, 在这里, 主库会记录写到的位置, 从库会记录已读到的位置 1744964466139

主从库的连接恢复后, 从库会给主库发送psync命令将当前的slave_repl_offset发给主库, 主库只用把master_repl_offset和salve_repl_offset之前的操作命令同步给从库即可 1744964507659

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

这里细锁一下:

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

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

总结: 主从库同步有三种方式:

  • 全量复制
  • 基于长连接的命令传播
  • 增量复制 建议:一个 Redis 实例的数据库不要太大,一个实例大小在几 GB 级别比较合适,这样可以减少 RDB 文件生成、传输和重新加载的开销

哨兵机制

在redis主从库集群模式下, 若主库发生故障, 无法服务写操作, 此时需要选举出新的主库; 涉及到3个问题:

  1. 如何判断主库挂掉?
  2. 怎么选新主库?
  3. 如何将新主库通知给从库以及客户端?

redis的哨兵机制解决了上面3个问题, 换言之, 哨兵机制的职责在于: 监控, 选主和通知 1744964892085

监控

哨兵进程会使用 PING 命令检测它自己和主、从库的网络连接情况,用来判断实例的状态。如果哨兵发现主库或从库对 PING 命令的响应超时了,那么,哨兵就会先把它标记为“主观下线”.

为了避免单个哨兵因为自身网络状况不好,而误判主库下线的情况, 引入了哨兵集群, 只有大多数的哨兵实例,都判断主库已经“主观下线”了,主库才会被标记为“客观下线”.

客观下线”的标准就是,当有 N 个哨兵实例时,最好要有 N/2 + 1 个实例判断主库为“主观下线” 1744964912711

选主

先筛选后打分, 最后将得分最高的从库作为主库 1744964933415 筛选 检查当前在线状态 判断之前的网络连接状态, 如使用配置项 down-after-milliseconds * 10, 如果发生断连的次数超过了 10 次,就说明这个从库的网络状况不好

打分

  1. 优先级高的从库得分高 可以通过 slave-priority 配置项,给不同的从库设置不同优先级, 参数越小, 优先级越高.
  2. 和旧主库同步程度最接近的从库得分高 选slave_repl_offset最大的从库
  3. ID 号小的从库得分高 每个实例都会有一个 ID,这个 ID 就类似于这里的从库的编号

哨兵集群

配置哨兵

sentinel monitor <master-name> <ip> <redis-port> <quorum>

哨兵之间的相互发现

哨兵之间的相互发现是基于Redis 提供的 pub/sub 机制,也就是发布 / 订阅机制。 1744965043217

哨兵获取从库信息

由哨兵向主库发送 INFO 命令来完成的 1744965055085

哨兵提供的事件通知

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

哨兵选举

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

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

经验

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

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

切片集群

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

  • 纵向拓展: 增加实例的资源配置, 优点是简单直接, 缺点是数据量过大, RDB快照在fork子进程会长时间阻塞, 且会受到硬件以及成本的限制
  • 横向拓展: 采用多实例分散存储数据经验 需要注意的是,如果哨兵集群只有 2 个实例,此时,一个哨兵要想成为 Leader,必须获得 2 票,而不是 1 票。所以,如果有个哨兵挂掉了,那么,此时的集群是无法进行主从库切换的。因此,通常我们至少会配置 3 个哨兵实例。这一点很重要,你在实际应用时可不能忽略了。

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

切片集群

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

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

多实例的数据分布

从 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

1744965176241

客户端定位数据

  1. redis实例之间建立连接后, 会将自己的哈希槽信息共享, 因此每个实例都拥有所有哈希槽的映射关系
  2. 客户端与集群实例建立连接后, 实例会将哈希槽信息发给客户端, 客户端因此知道所有哈希槽信息 经过以上两步, 客户端会将哈希槽信息缓存在本地, 当客户端请求键值时, 会先计算键所在的哈希槽, 然后再给相应的实例发送请求.

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

  • 集群中的实例新增或删除, redis需要重新分配哈希槽
  • 为了负载均衡, 需要重新分布 对此, 实例之间可以通过互相通信获取最新的哈希槽分配信息

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

GET hello:key
(error) MOVED 13320 172.16.19.5:6379

1744965217079

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

GET hello:key
(error) ASK 13320 172.16.19.5:6379

1744965240975

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

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