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

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

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

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

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

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

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

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

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

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

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

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

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

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

第二次删除失败怎么办?

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

相关推荐
小小李程序员7 小时前
LRU缓存
java·spring·缓存
Clown958 小时前
go-zero(十) 数据缓存和Redis使用
redis·缓存·golang
CopyLower12 小时前
深入理解 MyBatis 的缓存机制:一级缓存与二级缓存
spring·缓存·mybatis
nbsaas-boot16 小时前
秒杀系统三层架构设计:缓存、消息队列与数据库
数据库·缓存
NiNg_1_23419 小时前
Redis中的zset底层实现
数据库·redis·缓存
冧轩在努力1 天前
【redis 】string类型详解
数据库·redis·缓存
feilieren1 天前
DataGrip 连接 Redis、TongRDS
数据库·redis·缓存
孙克旭_1 天前
第三章 分布式缓存Redis
redis·分布式·缓存
Allen Bright1 天前
Jedis存储一个-以String的形式的对象到Redis
数据库·redis·缓存
Allen Bright1 天前
Jedis存储一个以byte[]的形式的对象到Redis
数据库·redis·缓存