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 留足时间,降低全量同步失败的概率