缓存双写一致性
缓存双写一致性是分布式系统中处理缓存(如Redis、Memcached)与数据库(如MySQL)数据同步问题的核心挑战,指在对同一数据进行并发读写时,如何保证缓存与数据库中的数据最终一致
为什么需要缓存双写一致性?
在引入缓存的系统中,数据存在两份副本:
- 数据库:数据的持久化存储,保证可靠性。
- 缓存:临时存储热点数据,加速读性能。
当数据发生更新时,若缓存与数据库的写操作顺序或时机不当,可能导致二者数据不一致,最终引发业务逻辑错误(如超卖、脏数据展示)。
解决方案及其问题场景
一、先更新数据库再删除缓存
不一致场景
1. 数据库主从延迟
在数据库读写分离的场景下,当一个写线程更新了数据库中的数据并删除了缓存,此是又有一个读线程没有读到缓存,从而去数据库从库读,然而此时数据库主库的数据还没有同步到该从库,此线程读到了旧数据,然后将它添加到缓存,从而后续读请求读到的都是旧数据。
解决方案
- 延迟双删
- 强制读主库
2. 删除失败
当有一个线程更新了数据库中的数据,下一步即将删除缓存,但是此时本地服务宕机(例如JVM宕机),就导致缓存删除失败;或者此时缓存服务不可用(例如网络中断、服务宕机等),此时删除缓存也失败。并且删除缓存失败后,产生数据不一致。
解决方案
- 重试补偿机制:异步重试删除操作
- 分布式事务
二、先更新数据库再更新缓存
不一致场景
1. 并发更新
当有一个线程更新了数据库后要更新缓存,此时由于网络延迟或者CPU时间片耗尽等原因,没有及时更新,但是此时又有一个线程更新了数据库,又更新了缓存,此时第一个线程恢复执行,此时缓存中的数据就是旧数据,发生了不一致问题。
解决方案
- 分布式锁
2. 更新失败
当有一个线程更新了数据库中的数据,下一步即将更新缓存,但是此时本地服务宕机(例如JVM宕机),就导致缓存更新失败;或者此时缓存服务不可用(例如网络中断、服务宕机等),此时更新缓存也失败。并且更新缓存失败后,产生数据不一致。
解决方案
- 重试补偿机制:异步重试删除操作
- 分布式事务
三、先删除缓存再更新数据库
不一致场景
1. 读请求过快
当有一个写线程删除了缓存,下一步即将更新数据库,但由于网络延迟或CPU时间片耗尽等原因未及时执行,此时有一个读线程未命中缓存,则去查数据库,然后将数据放入缓存,此时写线程继续更新数据库,最后缓存中的依然是旧数据,发生不一致问题。
解决方案
- 分布式锁
- 延迟双删
四、先更新缓存再更新数据库
不一致场景
1. 事务回滚
当先更新缓存成功后,更新数据库时发生了异常,导致没有对数据库更新或者导致数据库事务回滚,发送不一致。
解决方案
- 重试补偿机制:异步重试删除操作
- 分布式事务
2. 并发更新
当有一个线程更新缓存后,由于网络延迟或者CPU时间片耗尽没有及时执行,此时另一个线程更新了缓存,又更新了数据库,此时第一个线程恢复执行,更新数据库,此时缓存中的数据和数据库中的不一致。
解决方案
- 分布式锁
五、延迟双删
操作流程
先删除缓存,再更新数据库,延迟一定时间后再删除缓存。
不一致场景
1. 延迟时间太短
当更新数据库后延迟的时间过短,比数据库主从复制延迟更短的话,此时写线程删除缓存,读线程读从库的旧数据然后放入缓存,发生不一致。
解决方案
- 设置更合理的延迟时间
- 强制读主库
- 分布式锁
2. 删除失败
当有一个线程更新了数据库中的数据,下一步即将删除缓存,但是此时本地服务宕机(例如JVM宕机),就导致缓存删除失败;或者此时缓存服务不可用(例如网络中断、服务宕机等),此时删除缓存也失败。并且删除缓存失败后,产生数据不一致。
解决方案
- 重试补偿机制:异步重试删除操作
- 分布式事务
六、分布式锁
操作流程
写请求在写缓存和写数据库之前加分布式锁,完成后再释放锁。 读请求在缓存未命中后加锁,将读到的数据库数据放入缓存后释放锁
使用分布式锁不管是删除还是更新,并发问题都得到解决。
分布式锁解决了上面几种方案的部分缺陷:
- 先更新数据库再更新缓存的并发更新问题
- 先删除数据库再更新缓存的读请求过快问题
- 先更新缓存再更新数据库的并发更新问题
不一致问题
然而使用了分布式锁的方案后依然会有不一致问题
- 先更新数据库再删除缓存的数据库主从延迟问题
- 以上所有方案中的删除失败问题,也是分布式事务问题
七、基于数据库监听删除缓存
操作流程
以MySQL为例,可以通过监听MySQL的binlog,异步删除缓存
一致性问题
此方案实现最终一致性,在异步删除有延迟的情况下,会读到旧数据。
1. 删除失败
在异步删除缓存时也会发生删除失败问题,最终导致缓存不一致问题。
解决方案
我们监听binlog通常会以消息队列MQ异步消费实现,删除失败我们可以用MQ的消费者重试机制,对删除操作进行重试。
八、基于数据库监听更新缓存
操作流程
以MySQL为例,可以通过监听MySQL的binlog,异步更新缓存
一致性问题
此方案实现最终一致性,在异步更新有延迟的情况下,会读到旧数据。
1. 更新失败
在异步更新缓存时也会发生删除失败问题,最终导致缓存不一致问题。
解决方案
我们监听binlog通常会以消息队列MQ异步消费实现,更新失败我们可以用MQ的消费者重试机制,对更新操作进行重试。
2. 消息顺序
监听binlog向MQ发送消息时由于网络延迟等因素,可能会导致消息的顺序无法保证,从而在更新缓存时发生错误的更新顺序导致数据不一致。
解决方案
实现MQ顺序消息以及顺序消费
总结
缓存双写一致性无“银弹”,需遵循以下原则:
- 写优先保数据库:任何写操作以数据库成功为第一优先级,缓存通过异步机制兜底。
- 读容忍短暂不一致:通过TTL、本地缓存、降级页面降低不一致影响范围。
- 监控重于预防:实时监控+自动化修复比“完美方案”更实际有效。
comments powered by Disqus