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

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