Skip to content

主从复制有那些坑? ​

Redis 为了保证高可用,提供了主从复制的方式,这样就可以保证 Redis 有多个「副本」,当主库宕机后,我们依旧有从库可以使用。

1、 主从复制会丢数据吗? ​

Redis 的主从复制是采用「异步」的方式进行的。

这就意味着,如果 master 突然宕机,可能存在有部分数据还未同步到 slave 的情况发生。

这会导致什么问题呢?

如果你把 Redis 当做纯缓存来使用,那对业务来说没有什么影响。

master 未同步到 slave 的数据,业务应用可以从后端数据库中重新查询到。

但是,对于把 Redis 当做数据库,或是当做分布式锁来使用的业务,有可能因为异步复制的问题,导致数据丢失 / 锁丢失。

2、同样命令查询一个 key,主从库却返回不同的结果? ​

如果一个 key 已过期,但这个 key 还未被 master 清理,此时在 slave 上查询这个 key,会返回什么结果呢?

  1. slave 正常返回 key 的值
  2. slave 返回 NULL

其实,返回什么结果,这要取决于以下 3 个因素:

  1. Redis 的版本
  2. 具体执行的命令
  3. 机器时钟

如果你使用的是 Redis 3.2 以下版本,只要这个 key 还未被 master 清理,那么,在 slave 上查询这个 key,它会永远返回 value 给你。

也就是说,即使这个 key 已过期,在 slave 上依旧可以查询到这个 key。

其实这是 Redis 的一个 Bug:3.2 以下版本的 Redis,在 slave 上查询一个 key 时,并不会判断这个 key 是否已过期,而是直接无脑返回给客户端结果。

slave 查询过期 key,经历了 3 个阶段:

  1. 3.2 以下版本,key 过期未被清理,无论哪个命令,查询 slave,均正常返回 value
  2. 3.2 - 4.0.11 版本,查询数据返回 NULL,但 EXISTS 依旧返回 true
  3. 4.0.11 以上版本,所有命令均已修复,过期 key 在 slave 上查询,均返回「不存在」

最后,我们来看影响查询结果的第 3 个因素:「机器时钟」。

无论是 master 还是 slave,在判断一个 key 是否过期时,都是基于「本机时钟」来判断的。

如果 slave 的机器时钟比 master 走得「快」,那就会导致,即使这个 key 还未过期,但以 slave 上视角来看,这个 key 其实已经过期了,那客户端在 slave 上查询时,就会返回 NULL。

3、主从切换会导致缓存雪崩? ​

我们假设,slave 的机器时钟比 master 走得「快」,而且是「快很多」。

此时,从 slave 角度来看,Redis 中的数据存在「大量过期」。

如果此时操作「主从切换」,把 slave 提升为新的 master。

它成为 master 后,就会开始大量清理过期 key,此时就会导致以下结果:

  1. master 大量清理过期 key,主线程发生阻塞,无法及时处理客户端请求
  2. Redis 中数据大量过期,引发缓存雪崩

一定要保证主从库的机器时钟一致性,避免发生这些问题。

4、master / slave 大量数据不一致? ​

还有一种场景,会导致 master / slave 的数据存在大量不一致。

这就涉及到 Redis 的 maxmemory 配置了。

Redis 的 maxmemory 可以控制整个实例的内存使用上限,超过这个上限,并且配置了淘汰策略,那么实例就开始淘汰数据。

但这里有个问题:假设 master / slave 配置的 maxmemory 不一样,那此时就会发生数据不一致。

例如,master 配置的 maxmemory 为 5G,而 slave 的 maxmemory 为 3G,当 Redis 中的数据超过 3G 时,slave 就会「提前」开始淘汰数据,此时主从库数据发生不一致。

另外,尽管 master / slave 设置的 maxmemory 相同,如果你要调整它们的上限,也要格外注意,否则也会导致 slave 淘汰数据:

  • 调大 maxmemory 时,先调整 slave,再调整 master
  • 调小 maxmemory 时,先调整 master,再调整 slave

以此方式操作,就避免了 slave 提前超过 maxmemory 的问题。

其根本原因在于,slave 超过 maxmemory 后,会「自行」淘汰数据。

Redis 5.0 增加了一个配置项:replica-ignore-maxmemory,默认 yes。这个参数表示,尽管 slave 内存超过了 maxmemory,也不会自行淘汰数据了!

这样一来,slave 永远会向 master 看齐,只会老老实实地复制 master 发送过来的数据,不会自己再搞「小动作」。

5、slave 竟然会有内存泄露问题? ​

当你在使用 Redis 时,符合以下场景,就会触发 slave 内存泄露:

  • Redis 使用的是 4.0 以下版本
  • slave 配置项为 read-only=no(从库可写)
  • 向 slave 写入了有过期时间的 key

这时的 slave 就会发生内存泄露:slave 中的 key,即使到了过期时间,也不会自动清理。

这其实也是 Redis 的一个 Bug,Redis 4.0 才修复了这个问题。

解决方案是,在可写的 slave 上,写入带有过期时间 key 时,slave 会「记录」下来这些 key。

然后 slave 会定时扫描这些 key,如果到达过期时间,则清理之。

如果你的业务需要在 slave 上临时存储数据,而且这些 key 也都设置了过期时间,那么就要注意这个问题了。

最好的方案是,制定一个 Redis 使用规范,slave 必须强制设置为 read-only,不允许写,这样不仅可以保证 master / slave 的数据一致性,还避免了 slave 内存泄露问题。

6、为什么主从全量同步一直失败? ​

在主从全量同步时,你可能会遇到同步失败的问题,具体场景如下:

slave 向 master 发起全量同步请求,master 生成 RDB 后发给 slave,slave 加载 RDB。

由于 RDB 数据太大,slave 加载耗时也会变得很长。

此时你会发现,slave 加载 RDB 还未完成,master 和 slave 的连接却断开了,数据同步也失败了。

之后你又会发现,slave 又发起了全量同步,master 又生成 RDB 发送给 slave。

同样地,slave 在加载 RDB 时,master / slave 同步又失败了,以此往复。

其实,这就是 Redis 的「复制风暴」问题。

什么是复制风暴?

就像刚才描述的:主从全量同步失败,又重新开始同步,之后又同步失败,以此往复,恶性循环,持续浪费机器资源。

如果你的 Redis 有以下特点,就有可能发生这种问题:

  • master 的实例数据过大,slave 在加载 RDB 时耗时太长
  • 复制缓冲区(slave client-output-buffer-limit)配置过小
  • master 写请求量很大

主从在全量同步数据时,master 接收到的写请求,会先写到主从「复制缓冲区」中,这个缓冲区的「上限」是配置决定的。

当 slave 加载 RDB 太慢时,就会导致 slave 无法及时读取「复制缓冲区」的数据,这就引发了复制缓冲区「溢出」。

为了避免内存持续增长,此时的 master 会「强制」断开 slave 的连接,这时全量同步就会失败。

之后,同步失败的 slave 又会「重新」发起全量同步,进而又陷入上面描述的问题中,以此往复,恶性循环,这就是所谓的「复制风暴」。

如何解决这个问题:

  1. Redis 实例不要太大,避免过大的 RDB
  2. 复制缓冲区配置的尽量大一些,给 slave 加载 RDB 留足时间,降低全量同步失败的概率