Redis实践篇(主从不一致)

309 阅读6分钟

Redis主从不一致

Redis主从数据不一致的原因,主要是主从数据的复制是异步的。

  1. 网络延迟
  2. 从库接收了主库发来的命令后,由于正在执行其他的阻塞性命令,导致同步命令还未来得及执行。

具体来说,在主从库命令传播阶段,主库收到新的写命令后,会发送给从库。但是,主库并不会等到从库实际执行完命令后,再把结果返回给客户端,而是主库自己在本地执行完命令后,就会向客户端返回结果了。如果从库还没有执行主库同步过来的命令,主从库间的数据就不一致了。

应对方案

  1. 提供更好的硬件环境,比如主从间的网络要保障好,必要时可以配置专有网络通道;
  2. 开发一个外部程序来监控主从库间的复制进度。

因为 Redis 的 INFO replication 命令可以查看主库接收写命令的进度信息 (master_repl_offset)和从库复制写命令的进度信息(slave_repl_offset),所以,我们 就可以开发一个监控程序,先用 INFO replication 命令查到主、从库的进度,然后,我们 用 master_repl_offset 减去 slave_repl_offset,这样就能得到从库和主库间的复制进度差 值了。

如果某个从库的进度差值大于我们预设的阈值,我们可以让客户端不再和这个从库连接进 行数据读取,这样就可以减少读到不一致数据的情况。不过,为了避免出现客户端和所有 从库都不能连接的情况,我们需要把复制进度差值的阈值设置得大一些。

image.png

当然,监控程序可以一直监控着从库的复制进度,当从库的复制进度又赶上主库时,我们 就允许客户端再次跟这些从库连接。

读取到过期数据

我们在使用 Redis 主从集群时,有时会读到过期数据。例如,数据 X 的过期时间是 202010240900,但是客户端在 202010240910 时,仍然可以从从库中读到数据 X。一个 数据过期后,应该是被删除的,客户端不能再读取到该数据,但是,Redis 为什么还能在 从库中读到过期的数据呢?

这跟 Redis 用于设置过期时间的命令有关系,有些命令给数据设置的过期时间在从库上可能会被延后,导致应该过期的数据又在从库上被读取到了。

设置数据过期时间的命令一共有 4 个,我们可以把它们分成两类:

  • EXPIRE 和 PEXPIRE:它们给数据设置的是从命令执行时开始计算的存活时间;

  • EXPIREAT 和 PEXPIREAT:它们会直接把数据的过期时间设置为具体的一个时间点。

image.png

当主从库全量同步时,如果主库接收到了一条 EXPIRE 命令,那么,主库会直接执行这条命令。这条命令会在全量同步完成后,发给从库执行。而从库在执行时,就会在当前时间的基础上加上数据的存活时间,这样一来,从库上数据的过期时间就会比主库上延后了。

所以,使用 EXPIREAT/PEXPIREAT 命令设置过期时间,避免从库上的数据过期时间滞后。不过,这里有个地方需要注意下,因为 EXPIREAT/PEXPIREAT 设置的是时间点,所以,主从节点上的时钟要保持一致,具体的做法是,让主从节点和相同的NTP 服务器(时间服务器)进行时钟同步。

脑裂问题

脑裂是指在主从集群中,同时有两个主库都能接收写请求。在 Redis 的主从切换过程中,如果发生了脑裂,客户端数据就会写入到原主库,如果原主库被降为从库,这些新写入的数据就丢失了。

为什么脑裂会导致数据丢失?

主从切换后,从库一旦升级为新主库,哨兵就会让原主库执行 slave of 命令,和新主库重新进行全量同步。而在全量同步执行的最后阶段,原主库需要清空本地的数据,加载新主库发送的 RDB 文件,这样一来,原主库在主从切换期间保存的新写数据就丢失了。

脑裂发生的原因主要是原主库发生了假故障,一般有如下几个原因:

  1. 主库所在的服务器,部署了其他的程序临时占用了大量的资源(CPU、网络),导致主库资源受限,短时间无法响应心跳。
  2. 主库自身遇到的阻塞,如执行阻塞时间过长的命令,bigkey处理等,短时间内无法响应心跳。

为了应对脑裂,可以在主从集群部署时,通过合理地配置参数 min-slaves-to-write 和min-slaves-max-lag,来预防脑裂的发生。

  • min-slaves-to-write:这个配置项设置了主库能进行数据同步的最少从库数量;

  • min-slaves-max-lag:这个配置项设置了主从库间进行数据复制时,从库给主库发送ACK 消息的最大延迟(以秒为单位)。

把 min-slaves-to-write 和 min-slaves-max-lag 这两个配置项搭配起来使用,分别给它们设置一定的阈值,假设为 N 和 T。这两个配置项组合后的要求是,主库连接的从库中至少有 N 个从库,和主库进行数据复制时的 ACK 消息延迟不能超过 T 秒,否则,主库就不会再接收客户端的请求了。

即使原主库是假故障,它在假故障期间也无法响应哨兵心跳,也不能和从库进行同步,自然也就无法和从库进行 ACK 确认了。这样一来,min-slaves-to-write 和 min-slavesmax-lag 的组合要求就无法得到满足,原主库就会被限制接收客户端请求,客户端也就不能在原主库中写入新数据了。

等到新主库上线时,就只有新主库能接收和处理客户端请求,此时,新写的数据会被直接写到新主库中。而原主库会被哨兵降为从库,即使它的数据被清空了,也不会有新数据丢失。

假设我们将 min-slaves-to-write 设置为 1,把 min-slaves-max-lag 设置为 12s,把哨兵的 down-after-milliseconds 设置为 10s,主库因为某些原因卡住了 15s,导致哨兵判断主库客观下线,开始进行主从切换。同时,因为原主库卡住了 15s,没有一个从库能和原主库在 12s 内进行数据复制,原主库也无法接收客户端请求了。这样一来,主从切换完成后,也只有新主库能接收请求,不会发生脑裂,也就不会发生数据丢失的问题了。