缓存与数据库数据一致性问题

在用了redis缓存的系统中,正常情况下,一个读操作会先查缓存,如果在缓存中查到了,则直接返回,如果缓存中没有,则会查数据库,再将查到的数据写到redis中,然后返回。如下图:

这样的流程没有啥问题,但是当数据变更时,怎么把缓存变到最新,这就是缓存一致性要考虑的问题。

首先最容易想到的就是在更新数据库的时候同时更新缓存。这就涉及到先更新缓存还是先更新数据库的问题。其实无论是先操作哪一个都可能会出现缓存不一致问题。

首先就是两个操作不是原子性的。就可能出现一个成功一个失败的情况。先更新数据库,然后缓存更新失败,这样就会导致缓存中存在的是一个错误的旧值。先更新缓存,数据库更新失败时,这样读取到的缓存依然是不正确的。这还是没有并发的情况。

其次考虑在并发场景下,两个线程同时进行先更新缓存后更新数据库的操作:

先更新数据库后更新缓存会导致一样的问题:

然后还有一个发生概率比较低的并发场景,那就是读写并发。读的过程就是一开始说的先查缓存,缓存中有直接返回,没有则查数据库,查完再写到缓存然后返回。如果读写次序是:读线程先读缓存,缓存没有,查数据库值假如为10,这时候更新缓存和数据库的写操作执行了写数据库和缓存为20,然后读线程接着又更新缓存它从数据库查到的10。数据库中为20,缓存中为10,又导致了不一致,如图:

所以更新缓存就不太行。就考虑直接删除缓存和更新数据库,由于是直接删除缓存,所以上面写写并发情况出现的不一致问题也就不存在了。而且删缓存相比于更新缓存更简单,也更不容易出错。因为很多情况下缓存中的数据可能并不是一个简单的字符串,而是一个比较大的JSON串,更新缓存要从缓存中反序列化得到对象然后更新,然后又要序列化成JSON再写到redis。而删缓存直接了当删掉,就很简单不容易出错。

但是删缓存依然存在两个操作无法保证原子性的问题:

假如先更新数据库后删缓存,删缓存失败了,就会导致缓存是旧值,数据不一致。

如果先删除缓存后更新数据库,即使更新数据库失败了也不会有脏数据,没什么影响,只要重试一次就好了。但是这种方案会无形中放大前面说的读写并发时的问题,因为先删缓存会增大缓存miss的概率(这种缓存Miss还可能会导致缓存击穿,可以通过加锁来解决)。还是前面的例子,一个读线程从缓存中没有查到值,然后查数据库查到10,这时候恰好一个写线程删缓存更新数据库为20,然后读线程更新缓存为10。这就又导致了不一致。

这种情况就要延迟双删来解决了,写线程先删缓存,写数据库后,延迟一会再删一次,这样就能把最后读线程写到缓存的脏数据给删掉了。延迟一般1~2秒。

总结一下延迟双删,第一次删除的目的是解决两个操作无法保证原子而导致不一致问题,所以选择先删缓存再更新数据库。第二次删除的目的是为了降低第一次删除而导致放大读写并发导致数据不一致的概率。

第二次删除失败怎么办?

也会出现,但第二次删除本来就是补偿性的。在读写并发这个小概率不一致事件中再失败,概率就更低。

相关推荐
虫小宝10 分钟前
返利软件的分布式缓存架构:Redis集群在高并发场景下的优化策略
分布式·缓存·架构
lifallen15 分钟前
字节跳动Redis变种Abase:无主多写架构如何解决高可用难题
数据结构·redis·分布式·算法·缓存
C++_girl2 小时前
缓存未命中
c++·缓存
虫小宝2 小时前
返利app排行榜的缓存更新策略:基于过期时间与主动更新的混合方案
java·spring·缓存
Light602 小时前
领码SPARK融合平台 · TS × Java 双向契约 —— 性能与治理篇|缓存分段与版本秩序
低代码·缓存·spark
Java烘焙师3 小时前
架构师必备:缓存更新模式总结
mysql·缓存
无敌的神原秋人12 小时前
关于Redis不同序列化压缩性能的对比
java·redis·缓存
百思可瑞教育15 小时前
Vue中使用keep-alive实现页面前进刷新、后退缓存的完整方案
前端·javascript·vue.js·缓存·uni-app·北京百思可瑞教育
论迹16 小时前
【Redis】-- 持久化
数据库·redis·缓存
七夜zippoe19 小时前
多级缓存架构实战手册:Caffeine+Redis 从设计到落地的全链路解决方案
redis·缓存·架构