缓存与数据库一致性问题

本文探讨了在数据更新时如何处理数据库与缓存一致性问题,包括先更新缓存还是数据库的选择及其并发挑战。总结了两种删除缓存的策略及其可能导致的不一致情况,并提出引入消息队列和订阅数据库变更日志作为解决方案。最后强调了在追求性能和一致性之间的权衡,以及掌握缓存和数据库一致性问题的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、同时更新数据库与缓存

当数据发生更新时,我们不仅要操作数据库,还要一并操作缓存。具体操作就是,修改一条数据时,不仅要更新数据库,也要连带缓存一起更新。

在这个过程中不能保证 数据库 与 缓存 中的数据都能成功更新!因此会出现数据不一致的问题。

有两种更新方式。

1. 先更新缓存再更新数据库

如果缓存更新成功了,但数据库更新失败,那么此时缓存中是最新值,但数据库中是旧值

虽然此时读请求可以命中缓存,拿到正确的值,但是,一旦缓存失效,就会从数据库中读取到旧值,重建缓存也是这个旧值。

2. 先更新数据库再更新缓存

如果数据库更新成功了,但缓存更新失败,那么此时数据库中是最新值,缓存中是旧值

之后的读请求读到的都是旧数据,只有当缓存失效后,才能从数据库中得到正确的值。

可见,无论谁先谁后,但凡后者发生异常,就会出现数据不一致的情况

并发问题

假设我们采用先更新数据库,再更新缓存的方案,并且两步都可以成功执行的前提下,如果存在并发,情况会是怎样的呢?

缓存数据库线程操作
oaA修改数据库
obB修改数据库
bbB修改缓存
abA修改缓存

最终在缓存中是 a,在数据库中是 b,发生不一致。

也就是说,A 虽然先于 B 发生,但 B 操作数据库和缓存的时间,却要比 A 的时间短,执行时序发生错乱,最终这条数据结果是不符合预期的。

除此之外,我们从缓存利用率的角度来看也是不太推荐的。
这是因为每次数据发生变更,都更新缓存,但是缓存中的数据不一定会被马上读取,这就会导致缓存中可能存放了很多不常访问的数据,浪费缓存资源。
而且很多情况下,写到缓存中的值,并不是与数据库中的值一一对应的,很有可能是先查询数据库,再经过一系列计算得出一个值,才把这个值才写到缓存中。
由此可见,这种更新数据库 + 更新缓存的方案,不仅缓存利用率不高,还会造成机器性能的浪费。

二、删除缓存

1. 先删除缓存,后更新数据库

A 写,B 读

缓存数据库线程操作
-oA删除缓存
-oB读取缓存为空
-oB读取数据库为o
-aA修改数据库
oaB写入缓存o

可见并发下,存在读写数据不一致情况

2. 先更新数据库,后删除缓存

A 读,B 写

缓存数据库线程操作
-o-缓存过期
-oA读取数据库为o
-oB读取缓存为o
-bB修改数据库
bbB写入缓存
obA写入缓存o

可见并发下,也会出现读写数据不一致的情况

在实际情况下,写的速度是远慢于读的,因此这种不一致出现的概率非常低。可以考虑延迟双删(删除缓存)方式来尝试解决。

如果数据库更新成功,缓存删除失败如何解决?

缓存删除重试,引入消息队列,将重试请求写到消息队列中,然后由专门的消费者来重试,直到成功。

因为消息队列的特性,正好符合我们的需求:

  • 消息队列保证可靠性:写到队列中的消息,成功消费之前不会丢失(重启项目也不担心)
  • 消息队列保证消息成功投递:下游从队列拉取消息,成功消费后才会删除消息,否则还会继续投递消息给消费者(符合我们重试的场景)

至于写队列失败和消息队列的维护成本问题:

  • 写队列失败:操作缓存和写消息队列,「同时失败」的概率其实是很小的
  • 维护成本:我们项目中一般都会用到消息队列,维护成本并没有新增很多
    所以,引入消息队列来解决这个问题,是比较合适的。
    在这里插入图片描述

现有的流行解决方案:订阅数据库变更日志,再操作缓存。

我们的业务应用在修改数据时,只需修改数据库,无需操作缓存。
通过订阅数据库的变更日志(Binlog),拿到具体操作的数据然后再根据这条数据去删除对应的缓存

在这里插入图片描述

订阅变更日志,目前也有了比较成熟的开源中间件,例如阿里的 canal,使用这种方案的优点在于:

  • 无需考虑写消息队列失败情况:只要写 MySQL 成功,Binlog 肯定会有
  • 自动投递到下游队列:canal 自动把数据库变更日志投递给下游的消息队列

我们可以得出结论,想要保证数据库和缓存一致性,推荐采用先更新数据库,再删除缓存方案,并配合消息队列订阅变更日志的方式来做。应对高并发可能出现的旧数据回写缓存问题可以通过延迟双删来优化。

总结

  1. 性能和一致性不能同时满足,为了性能考虑,通常会采用最终一致性的方案

  2. 掌握缓存和数据库一致性问题,核心问题有 3 点:缓存利用率、并发、缓存 + 数据库一起成功问题

  3. 失败场景下要保证一致性,常见手段就是重试,同步重试会影响吞吐量,所以通常会采用异步重试的方案

  4. 订阅变更日志的思想,本质是把权威数据源(例如 MySQL)当做 leader 副本,让其它异质系统(例如 Redis)成为它的 follower 副本,通过同步变更日志的方式,保证 leader 和 follower 之间保持一致

如果需要强一致性则需要引入分布式锁等机制,这样将会大大影响性能,让缓存丢失了本该拥有的高性能

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值