面试老是问到,看了些文章写了个总结。注意为啥某个方法会不一致这种问题也会考,要能说出来两个线程具体怎么执行导致的不一致。
如果是更换缓存+更新数据库的方案。两个写线程并发可能会有不一致的问题(一个线程A先执行一步,中间被另一个线程B执行完了,A再执行第二步)。
比如:
先更新缓存再更新数据库
A线程:更新缓存X=1
B线程:更新缓存X=2
B线程:更新数据库X=2
A线程:更新数据库X=1
此时缓存中X=2,数据库中X=1
先更新数据库再更新缓存
A线程:更新数据库X=1
B线程:更新数据库X=2
B线程:更新缓存X=2
A线程:更新缓存X=1
此时缓存中X=1,数据库中X=2
并且缓存采用更新的方式有可能更新之后很久也没有用到,造成资源浪费。所以用删除缓存的方式更合适。
如果是删除缓存+更新数据库的方式。读写线程并发可能会有问题(核心问题读线程是先把老的值读出来,然后写线程删除完缓存之后,读线程再把老的值写进去。所以造成这种顺序的情况是先更新数据库的话就先读,先删缓存的话就先删)
先更新数据库再删除缓存
A线程:读缓存发现为空,去读数据库X=1
B线程:更新数据X=2
B线程:删除缓存
A线程:将缓存更新为X=1
此时数据库X=2,缓存X=1
先删除缓存再更新数据库
A线程:删除缓存
B线程:读缓存发现为空,去读数据库X=1
A线程:更新数据库X=2
B线程:将缓存更新为X=1
此时数据库X=2,缓存X=1
虽然这两个模式都有问题。但是先更新数据库再删除缓存的情况更优秀。因为更新数据库这个操作要加锁,一般比读数据库+更新缓存这又要慢的。所以这个导致不一致的执行顺序不常发生。而且比较常用的方式就是先更新数据库再删除缓存。
同时,这两个方式还可以打一下补丁,用延迟双删的策略
先删除缓存再更新数据库再睡一会再删除缓存
这个方式对于先删除缓存再更新数据库的补丁在于:B线程将缓存更新为旧值了,A线程睡一会之后将这个旧值再次删掉了。
这个方式对于先更新数据库再删除缓存的补丁在于:如果是数据库是读写分离主从复制的架构的话,写线程写的是主库,读线程读的是从库,而主从复制是需要时间的。假设读写都是主库的情况下一致了,这时候引入读写分离又是不一致了。比如
正常情况下:
先更新数据库再删除缓存
A线程:更新数据库X=1
B线程:缓存为空,读数据库X=1
A线程:删除缓存
B线程:写回缓存X=1
此时数据库为X=1,缓存X=1。是正常情况。
如果引入数据库主从复制读写分离呢
A线程:更新主库X=1,次数从库X=2,且还没有同步
B线程:缓存为空,读从库X=2
A线程:删除缓存
B线程:写回缓存X=2
后序同步成功从库X=1
此时数据库X=1,缓存X=2.又不一致了
如果引入延迟双删的话
在上面的情况A线程延迟一段时间,把旧缓存删掉就一致了。
但是延迟双删也会有不一致的情况,其实和先删缓存再更新数据库是一样的。引入这个机制是为了把缓存中出现的旧值给再一次删除,但是如果线程休眠时间不够,第二次删除是起不了作用的。以之前的先删缓存再更新数据库为例,也就是第二次删除在写回缓存之前。
A线程:删除缓存
B线程:读缓存为空,查数据库X=1
A线程:更新数据库X=2
A线程:等待一段时间
A线程:再次删除缓存
B线程:写回缓存X=1
所以说,等待这段时间要稍微长一点,但又不能太长,所以这段时间是不好估算的。太短了不起作用,太长了又一直阻塞线程。基于这种情况,最后还是只使用先更新数据库再删缓存的方式用的多。
还有一个问题就是删缓存如果失败了怎么办?又不一致了。如果要增加这一步的可靠性的话,需要引入重试机制。一般是两个策略。1.用消息队列重试,消费者去做删除缓存操作,可以多次消费也就是删除失败了可以多次重试。 2. 引入canal监听mysql的binlog,发生数据库更新了就将消息发送到消息队列,也是消费者去删缓存,重试。 canal和消息队列都有失败补偿机制。
同时如果这里还想引入延迟双删的策略的话,消息队列选用rabbitmq(装延迟插件)或者rocketmq(支持延迟队列),kafka没有延迟队列机制。
上面的这些都是实现最终一致性的手段。也就是说尽量降低了缓存和数据库不一致的窗口期。
关于强一致性(即任何时刻读到的数据都和 DB 一致),面试被问到了,我也不知道怎么回答。这是AI给我的回答:
在缓存和数据库场景下,严格的 强一致性(任何时刻读到的数据都与数据库一致)很难实现。原因是 Redis 和数据库是两个独立系统,无法保证跨系统的原子操作,中间总会存在一定的不一致窗口。
如果理论上要实现强一致性,可以通过 分布式锁 把读写操作串行化:
- 写操作:先获取锁 → 更新数据库 → 删除缓存 → 释放锁
- 读操作:先获取锁 → 读缓存 → 缓存不存在再读数据库并写入缓存 → 释放锁
这个方案虽然接近强一致,但有几个明显缺点:
- 性能差:所有读写都串行化,吞吐量大幅下降
- 锁可靠性:锁超时、节点宕机、网络波动可能导致问题
- 操作非原子:数据库更新与缓存删除仍可能失败,导致脏数据
因此工程上通常选择 最终一致性:
- 通过延迟双删、消息队列同步缓存、或者 binlog + Canal 异步更新缓存
- 对于一致性要求极高的业务(如金融交易),直接放弃缓存,让所有操作走数据库事务