「Redis缓存」Redis 缓存数据库一致性手撕面答
🎨本篇脑图速览
🎯为什么是删除缓存而不是更新缓存?
🎐懒加载
一种懒加载的思想,因为每次更改数据之后,不一定立马就有人来用。 若更新的次数远大于读取的次数,此时会频繁更新缓存,但一直没人使用,若缓存更新的成本很高的话,此时会非常浪费性能资源。
🎈并发更新的情况
ABBA 【A的操作过程中,穿插了B的完整过程】
- A更新数据库为1
- B更新数据库为2
- B更新缓存为2
- A更新缓存为1
最后导致数据库最终是2,但缓存是1,也就是B的缓存更新丢失了
🎯为什么要先更新数据库,再删除缓存
若先删除缓存,再更新数据库的话:
- A线程删除缓存
- B线程查询数据,缓存中没有了,会去计算数据库,设置缓存【此时计算出来的缓存是旧数据,是数据库更新之前的数据】
- A线程再更新数据库
接下来的查询,一直走的是缓存,也就是旧数据,这样就出现了数据库和缓存中数据不一致的情况
若先更新数据库,再删除缓存的话
- A线程更新数据库
- B线程查询数据,此时还未删除缓存,缓存中还有,得到的就是旧的缓存数据【此时就出现了数据库和缓存中数据不一致的情况】
- A线程再删除缓存
A读B写【先更后删可能会有数据不一致的情况,但很少见】
- A读取,发现缓存刚好过期了
- A查询出数据库的旧值,在设置缓存之前
- B更新数据库,并删除了缓存
- A用旧值,设置了缓存【缓存中是旧的数据,数据库是新的】
但由于缓存的写入要快于MySQL的写入,一般不可能在2跟4之间,穿插一个3操作【3还操作了数据库】
- 如果顺序是【2,4,3】的话,A设置了缓存之后也没关系,因为后续B还会再次删除缓存
- 之后的查询,发现缓存过期会去数据库中查询得到最新的数据
🎯🎈如何解决A读B写时先更后删的极端情况?
延时双删
- A读取,发现缓存失效了,此时去查询数据库【查出旧值】
- B更新数据库
- B设置新缓存
- A设置缓存为旧值
到这一步,如果之后的查询,查到的都会是旧的缓存,所以我们可以
- 延时500毫秒
延时500毫秒是为了让B能够更新完数据库,我们再次查询数据库才能获得最新的值,来设置缓存
- 删除缓存
🎯先删除缓存,后更新数据库如果也出现类似上边的问题怎么办?
假设一个场景:
- 请求A进行写操作,删除缓存
- 请求B查询发现缓存不存在
- 请求B去数据库查询得到旧值
- 请求B将旧值写入缓存
- 请求A将新值写入数据库
🎯从后者操作可能失败的角度来看,选择哪种策略更好一点?
先更新后删除缓存的话,后者失败,情况是这样的:
- A更新数据库,然后缓存没有删除成功
- B查询,直接走了缓存【旧数据】
这就出现了缓存跟数据库不一致的情况
先删除缓存后更新数据库的话,后者失败,情况是这样的:
- A删除缓存,然后没能更新数据库
- B查询,发现缓存没有,则去查询数据库,设置到缓存里边
此时并不会出现缓存跟数据库不一致的情况,因为A还没来得及更新,这样数据库跟缓存中,一直都是旧的数据,但至少不会出现数据不一致的问题
- 所以如果从这个角度来看的话,先删除缓存,后更新数据库才是更优解。
🎯🎈如何解决后者失败的情况呢?
使用消息队列重试
- 请求 A 先对数据库进行更新操作,同时吧
- 在对 Redis 进行删除操作的时候发现报错,删除失败
- 此时将Redis 的 key 作为消息体发送到消息队列中
-
系统接收到消息队列发送的消息后再次对 Redis 进行删除操作
但是这个方案会有一个缺点就是会对业务代码造成大量的侵入,深深的耦合在一起。
有没有必要更新成功后就投递到mq呢?而不是等到redis删除失败了再投递到mq
如果考虑Java项目进程在更新数据库之后就宕机了的话,那无论哪种都没法避免缓存删除的失败 如果硬抠的话,更新成功后,到删除redis还有一段间隙,这个间隙先投递到mq,会更有保障一点,但相应的编码规则也更麻烦了点
- 更新数据库成功,投递到mq表示要删除 某个key
- 业务继续执行,删除redis:
- 删除成功的话,需要删除掉mq里边的消息,防止重复消费
- 删除失败的话,就需要消费者拉取mq,再次删除缓存,实现重试机制,当然,如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。后续的处理可能就先丢到死信队列里边了。
可以发现编码确实麻烦了些,因为要考虑重复消费的问题。那么如何解决呢?Java崩了,咱下游还有 mysql 和 mq 没崩呢!也就因此引出了下文的订阅binlog日志的方法
订阅binlog日志
原理:更新数据库成功,就会产生一条变更日志,记录在 binlog 里。
于是我们就可以通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除,阿里巴巴开源的 Canal 中间件就是基于这个实现的。
具体流程:
- canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议;
- mysql master收到dump请求,开始推送binary log给slave(也就是canal);
- canal解析binary log对象(原始为byte流);
-
canal将解析后的对象,根据业务场景,分发到比如 MySQL 、RocketMQ 或者 ES 中。