25 | 缓存异常(上):如何解决缓存和数据库的数据不一致问题?
蒋德钧
该思维导图由 AI 生成,仅供参考
你好,我是蒋德钧。
在实际应用 Redis 缓存时,我们经常会遇到一些异常问题,概括来说有 4 个方面:缓存中的数据和数据库中的不一致;缓存雪崩;缓存击穿和缓存穿透。
只要我们使用 Redis 缓存,就必然会面对缓存和数据库间的一致性保证问题,这也算是 Redis 缓存应用中的“必答题”了。最重要的是,如果数据不一致,那么业务应用从缓存中读取的数据就不是最新数据,这会导致严重的错误。比如说,我们把电商商品的库存信息缓存在 Redis 中,如果库存信息不对,那么业务层下单操作就可能出错,这当然是不能接受的。所以,这节课我就重点和你聊聊这个问题。关于缓存雪崩、穿透和击穿等问题,我会在下一节课向你介绍。
接下来,我们就来看看,缓存和数据库之间的数据不一致是怎么引起的。
缓存和数据库的数据不一致是如何发生的?
首先,我们得清楚“数据的一致性”具体是啥意思。其实,这里的“一致性”包含了两种情况:
缓存中有数据,那么,缓存的数据值需要和数据库中的值相同;
缓存中本身没有数据,那么,数据库中的值必须是最新值。
不符合这两种情况的,就属于缓存和数据库的数据不一致问题了。不过,当缓存的读写模式不同时,缓存数据不一致的发生情况不一样,我们的应对方法也会有所不同,所以,我们先按照缓存读写模式,来分别了解下不同模式下的缓存不一致情况。我在第 23 讲中讲过,根据是否接收写请求,我们可以把缓存分成读写缓存和只读缓存。
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
- 深入了解
- 翻译
- 解释
- 总结
本文介绍了在实际应用Redis缓存时经常遇到的缓存和数据库数据不一致问题,并提供了解决方案。首先介绍了数据一致性的两种情况,即缓存中有数据时需要与数据库中的值相同,缓存中没有数据时需要从数据库中获取最新值。针对不同的缓存读写模式,文章分别讨论了读写缓存和只读缓存的数据不一致情况及解决方法。对于读写缓存,需要采用同步直写策略来保证缓存和数据库的数据一致,并在业务应用中使用事务机制来保证原子性更新。而对于只读缓存,新增数据直接写入数据库,而删改数据需要标记缓存中的数据为无效,以保证后续访问时能够从数据库中读取最新值。此外,文章还详细分析了在删改数据过程中可能出现的数据不一致情况及解决方法。总的来说,本文提供了清晰的思路和解决方案,帮助读者解决了缓存和数据库数据不一致问题。
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Redis 核心技术与实战》,新⼈⾸单¥68
《Redis 核心技术与实战》,新⼈⾸单¥68
立即购买
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
登录 后留言
全部留言(93)
- 最新
- 精选
- 曾轼麟老是Redis的LFU是做了优化的,访问次数会随着时间而递减,在updateLFU() ->LFUDecrAndReturn()里面
作者回复: 是的,如果短时间内有频繁访问造成了过高的访问次数,这些数据也容易被滞留在缓存中,LFU会衰减访问次数,避免这些数据造成缓存污染。
2020-10-302 - 我是小妖怪🇨🇳加油!坚持就是胜利
作者回复: 加油!
2020-10-251 - Kaito数据在删改操作时,如果不是删除缓存值,而是直接更新缓存的值,你觉得和删除缓存值相比,有什么好处和不足? 这种情况相当于把Redis当做读写缓存使用,删改操作同时操作数据库和缓存。 1、先更新数据库,再更新缓存:如果更新数据库成功,但缓存更新失败,此时数据库中是最新值,但缓存中是旧值,后续的读请求会直接命中缓存,得到的是旧值。 2、先更新缓存,再更新数据库:如果更新缓存成功,但数据库更新失败,此时缓存中是最新值,数据库中是旧值,后续读请求会直接命中缓存,但得到的是最新值,短期对业务影响不大。但是,一旦缓存过期或者满容后被淘汰,读请求就会从数据库中重新加载旧值到缓存中,之后的读请求会从缓存中得到旧值,对业务产生影响。 同样地,针对这种其中一个操作可能失败的情况,也可以使用重试机制解决,把第二步操作放入到消息队列中,消费者从消息队列取出消息,再更新缓存或数据库,成功后把消息从消息队列删除,否则进行重试,以此达到数据库和缓存的最终一致。 以上是没有并发请求的情况。如果存在并发读写,也会产生不一致,分为以下4种场景。 1、先更新数据库,再更新缓存,写+读并发:线程A先更新数据库,之后线程B读取数据,此时线程B会命中缓存,读取到旧值,之后线程A更新缓存成功,后续的读请求会命中缓存得到最新值。这种场景下,线程A未更新完缓存之前,在这期间的读请求会短暂读到旧值,对业务短暂影响。 2、先更新缓存,再更新数据库,写+读并发:线程A先更新缓存成功,之后线程B读取数据,此时线程B命中缓存,读取到最新值后返回,之后线程A更新数据库成功。这种场景下,虽然线程A还未更新完数据库,数据库会与缓存存在短暂不一致,但在这之前进来的读请求都能直接命中缓存,获取到最新值,所以对业务没影响。 3、先更新数据库,再更新缓存,写+写并发:线程A和线程B同时更新同一条数据,更新数据库的顺序是先A后B,但更新缓存时顺序是先B后A,这会导致数据库和缓存的不一致。 4、先更新缓存,再更新数据库,写+写并发:与场景3类似,线程A和线程B同时更新同一条数据,更新缓存的顺序是先A后B,但是更新数据库的顺序是先B后A,这也会导致数据库和缓存的不一致。 场景1和2对业务影响较小,场景3和4会造成数据库和缓存不一致,影响较大。也就是说,在读写缓存模式下,写+读并发对业务的影响较小,而写+写并发时,会造成数据库和缓存的不一致。 针对场景3和4的解决方案是,对于写请求,需要配合分布式锁使用。写请求进来时,针对同一个资源的修改操作,先加分布式锁,这样同一时间只允许一个线程去更新数据库和缓存,没有拿到锁的线程把操作放入到队列中,延时处理。用这种方式保证多个线程操作同一资源的顺序性,以此保证一致性。 综上,使用读写缓存同时操作数据库和缓存时,因为其中一个操作失败导致不一致的问题,同样可以通过消息队列重试来解决。而在并发的场景下,读+写并发对业务没有影响或者影响较小,而写+写并发时需要配合分布式锁的使用,才能保证缓存和数据库的一致性。 另外,读写缓存模式由于会同时更新数据库和缓存,优点是,缓存中一直会有数据,如果更新操作后会立即再次访问,可以直接命中缓存,能够降低读请求对于数据库的压力(没有了只读缓存的删除缓存导致缓存缺失和再加载的过程)。缺点是,如果更新后的数据,之后很少再被访问到,会导致缓存中保留的不是最热的数据,缓存利用率不高(只读缓存中保留的都是热数据),所以读写缓存比较适合用于读写相当的业务场景。2020-10-1442362
- Alex_QY延时双删根本解决不了一致性问题,高迸发场景线程A根本不知道线程B,线程C的执行开始和结束的时间。 所以sleep跟没sleep没啥区别。。。 感觉唯一的办法就是要让整个事务方法由并行变成串行。 感觉串行呢?要么借助分布式锁,要么借助MYSQL本身的update独占锁2020-10-2929117
- ctang先删除缓存后更新数据库,数据库更新失败了,何来的旧值。只读缓存不是以数据库为准吗?2020-10-171636
- 约书亚完全没必要双删,双删比起先DB后删Redis,无非就是防止删除Redis操作失败。但高并发下依旧可能在第一次删期间混进来读操作。 还有基于消息队列的方案,凭啥Redis操作能会失败,DB操作会失败,消息队列就不会了呢?就算能用事务消息,难道不怕旧值覆盖新值的情况发生么? 对于课后题,这种被称为直接缓存,除了大家说的分布式锁方案保证并发下的正确,还可以考虑基于lua实现cas,有一定性能下降但大多数场景都还能接受。2021-02-01631
- wjunt928看了一堆评论,其实会发现讨论的时候,说着说着大家关于一致性的概念理解就发生变化了; 有时说的一致性指的是强一致性,有时说的一致性又是最终一致性,所以大家互觉得对方有问题; (1)只要缓存和数据库更新有先后,就一定达不到强一致性(达到数据库和缓存在同一瞬间都更新的效果); (2)即便使用最终一致性,也有方法实现: a.尽量降低出现长时间才能达到最终一致性的概率; b.尽量减少达到最终一致性的时间; 所以才会有人认为延迟双删解决不了一致性,因为这个一致性指的是强一致性; 而老师专栏说延迟双删可以保证一致性,也仅仅是指"减少数据库和缓存达到最终一致的时间";2022-03-16411
- Reborn 2.0不用第二种延迟双删, 直接用先DB后删redis不就好了, 延迟双删还要麻烦一点, 感觉没有什么优点啊2020-11-16310
- 不诉离殇老师分析问题的思路很赞,但是我对于结论有一个很大的疑问,按照分析思路 ,既然对于读写缓存同步写回策略可以采用(分布式)事务来保证原子性,那么只读缓存的更新数据库和删除缓存的操作为什么不能采用事务来保证原子性?二者的思路差异在哪里?2020-10-19610
- 范闲更新步骤: 1.先更新缓存,再更新数据库 2.先更新数据库,再更新缓存 并发: 1.读+写 2.写+写 3.读+读 在并发情况下,3是肯定没有影响的。1和2肯定会对数据一致性有影响。这个时候可以利用分布式锁来处理。同一时刻一个key只有一把锁持有。2020-10-208
收起评论