缓存一致性问题全场景及其解决方案

Posted by 听雨coding on Sunday, January 5, 2025

缓存双写一致性

缓存双写一致性是分布式系统中处理缓存(如Redis、Memcached)与数据库(如MySQL)数据同步问题的核心挑战,指在对同一数据进行并发读写时,如何保证缓存与数据库中的数据最终一致

为什么需要缓存双写一致性?

在引入缓存的系统中,数据存在两份副本:

  1. 数据库:数据的持久化存储,保证可靠性。
  2. 缓存:临时存储热点数据,加速读性能。

当数据发生更新时,若缓存与数据库的写操作顺序或时机不当,可能导致二者数据不一致,最终引发业务逻辑错误(如超卖、脏数据展示)。


解决方案及其问题场景

一、先更新数据库再删除缓存

不一致场景

1. 数据库主从延迟

在数据库读写分离的场景下,当一个写线程更新了数据库中的数据并删除了缓存,此是又有一个读线程没有读到缓存,从而去数据库从库读,然而此时数据库主库的数据还没有同步到该从库,此线程读到了旧数据,然后将它添加到缓存,从而后续读请求读到的都是旧数据。

解决方案

  1. 延迟双删
  2. 强制读主库

2. 删除失败

当有一个线程更新了数据库中的数据,下一步即将删除缓存,但是此时本地服务宕机(例如JVM宕机),就导致缓存删除失败;或者此时缓存服务不可用(例如网络中断、服务宕机等),此时删除缓存也失败。并且删除缓存失败后,产生数据不一致。

解决方案

  1. 重试补偿机制:异步重试删除操作
  2. 分布式事务

二、先更新数据库再更新缓存

不一致场景

1. 并发更新

当有一个线程更新了数据库后要更新缓存,此时由于网络延迟或者CPU时间片耗尽等原因,没有及时更新,但是此时又有一个线程更新了数据库,又更新了缓存,此时第一个线程恢复执行,此时缓存中的数据就是旧数据,发生了不一致问题。

解决方案

  1. 分布式锁

2. 更新失败

当有一个线程更新了数据库中的数据,下一步即将更新缓存,但是此时本地服务宕机(例如JVM宕机),就导致缓存更新失败;或者此时缓存服务不可用(例如网络中断、服务宕机等),此时更新缓存也失败。并且更新缓存失败后,产生数据不一致。

解决方案

  1. 重试补偿机制:异步重试删除操作
  2. 分布式事务

三、先删除缓存再更新数据库

不一致场景

1. 读请求过快

当有一个写线程删除了缓存,下一步即将更新数据库,但由于网络延迟或CPU时间片耗尽等原因未及时执行,此时有一个读线程未命中缓存,则去查数据库,然后将数据放入缓存,此时写线程继续更新数据库,最后缓存中的依然是旧数据,发生不一致问题。

解决方案

  1. 分布式锁
  2. 延迟双删

四、先更新缓存再更新数据库

不一致场景

1. 事务回滚

当先更新缓存成功后,更新数据库时发生了异常,导致没有对数据库更新或者导致数据库事务回滚,发送不一致。

解决方案

  1. 重试补偿机制:异步重试删除操作
  2. 分布式事务

2. 并发更新

当有一个线程更新缓存后,由于网络延迟或者CPU时间片耗尽没有及时执行,此时另一个线程更新了缓存,又更新了数据库,此时第一个线程恢复执行,更新数据库,此时缓存中的数据和数据库中的不一致。

解决方案

  1. 分布式锁

五、延迟双删

操作流程

先删除缓存,再更新数据库,延迟一定时间后再删除缓存。

不一致场景

1. 延迟时间太短

当更新数据库后延迟的时间过短,比数据库主从复制延迟更短的话,此时写线程删除缓存,读线程读从库的旧数据然后放入缓存,发生不一致。

解决方案

  1. 设置更合理的延迟时间
  2. 强制读主库
  3. 分布式锁

2. 删除失败

当有一个线程更新了数据库中的数据,下一步即将删除缓存,但是此时本地服务宕机(例如JVM宕机),就导致缓存删除失败;或者此时缓存服务不可用(例如网络中断、服务宕机等),此时删除缓存也失败。并且删除缓存失败后,产生数据不一致。

解决方案

  1. 重试补偿机制:异步重试删除操作
  2. 分布式事务

六、分布式锁

操作流程

写请求在写缓存和写数据库之前加分布式锁,完成后再释放锁。 读请求在缓存未命中后加锁,将读到的数据库数据放入缓存后释放锁

使用分布式锁不管是删除还是更新,并发问题都得到解决。

分布式锁解决了上面几种方案的部分缺陷

  1. 先更新数据库再更新缓存的并发更新问题
  2. 先删除数据库再更新缓存的读请求过快问题
  3. 先更新缓存再更新数据库的并发更新问题

不一致问题

然而使用了分布式锁的方案后依然会有不一致问题

  1. 先更新数据库再删除缓存的数据库主从延迟问题
  2. 以上所有方案中的删除失败问题,也是分布式事务问题

七、基于数据库监听删除缓存

操作流程

以MySQL为例,可以通过监听MySQL的binlog,异步删除缓存

一致性问题

此方案实现最终一致性,在异步删除有延迟的情况下,会读到旧数据。

1. 删除失败

在异步删除缓存时也会发生删除失败问题,最终导致缓存不一致问题。

解决方案

我们监听binlog通常会以消息队列MQ异步消费实现,删除失败我们可以用MQ的消费者重试机制,对删除操作进行重试。


八、基于数据库监听更新缓存

操作流程

以MySQL为例,可以通过监听MySQL的binlog,异步更新缓存

一致性问题

此方案实现最终一致性,在异步更新有延迟的情况下,会读到旧数据。

1. 更新失败

在异步更新缓存时也会发生删除失败问题,最终导致缓存不一致问题。

解决方案

我们监听binlog通常会以消息队列MQ异步消费实现,更新失败我们可以用MQ的消费者重试机制,对更新操作进行重试。

2. 消息顺序

监听binlog向MQ发送消息时由于网络延迟等因素,可能会导致消息的顺序无法保证,从而在更新缓存时发生错误的更新顺序导致数据不一致。

解决方案

实现MQ顺序消息以及顺序消费


总结

缓存双写一致性无“银弹”,需遵循以下原则:

  1. 写优先保数据库:任何写操作以数据库成功为第一优先级,缓存通过异步机制兜底。
  2. 读容忍短暂不一致:通过TTL、本地缓存、降级页面降低不一致影响范围。
  3. 监控重于预防:实时监控+自动化修复比“完美方案”更实际有效。

comments powered by Disqus