「Redis缓存」Redis 缓存数据库一致性手撕面答

🎨本篇脑图速览


🎯为什么是删除缓存而不是更新缓存?

🎐懒加载

一种懒加载的思想,因为每次更改数据之后,不一定立马就有人来用。 若更新的次数远大于读取的次数,此时会频繁更新缓存,但一直没人使用,若缓存更新的成本很高的话,此时会非常浪费性能资源。

🎈并发更新的情况

ABBA 【A的操作过程中,穿插了B的完整过程】

  1. A更新数据库为1
  2. B更新数据库为2
  3. B更新缓存为2
  4. A更新缓存为1

最后导致数据库最终是2,但缓存是1,也就是B的缓存更新丢失了



🎯为什么要先更新数据库,再删除缓存

若先删除缓存,再更新数据库的话:

  1. A线程删除缓存
  2. B线程查询数据,缓存中没有了,会去计算数据库,设置缓存【此时计算出来的缓存是旧数据,是数据库更新之前的数据】
  3. A线程再更新数据库

接下来的查询,一直走的是缓存,也就是旧数据,这样就出现了数据库和缓存中数据不一致的情况

若先更新数据库,再删除缓存的话

  1. A线程更新数据库
  2. B线程查询数据,此时还未删除缓存,缓存中还有,得到的就是旧的缓存数据【此时就出现了数据库和缓存中数据不一致的情况】
  3. A线程再删除缓存

A读B写【先更后删可能会有数据不一致的情况,但很少见】

  1. A读取,发现缓存刚好过期了
  2. A查询出数据库的旧值,在设置缓存之前
  3. B更新数据库,并删除了缓存
  4. A用旧值,设置了缓存【缓存中是旧的数据,数据库是新的】

但由于缓存的写入要快于MySQL的写入,一般不可能在2跟4之间,穿插一个3操作【3还操作了数据库】

  • 如果顺序是【2,4,3】的话,A设置了缓存之后也没关系,因为后续B还会再次删除缓存
  • 之后的查询,发现缓存过期会去数据库中查询得到最新的数据

🎯🎈如何解决A读B写时先更后删的极端情况?

延时双删

  1. A读取,发现缓存失效了,此时去查询数据库【查出旧值】
  2. B更新数据库
  3. B设置新缓存
  4. A设置缓存为旧值

到这一步,如果之后的查询,查到的都会是旧的缓存,所以我们可以

  1. 延时500毫秒

延时500毫秒是为了让B能够更新完数据库,我们再次查询数据库才能获得最新的值,来设置缓存

  1. 删除缓存

🎯先删除缓存,后更新数据库如果也出现类似上边的问题怎么办?

假设一个场景:

  • 请求A进行写操作,删除缓存
  • 请求B查询发现缓存不存在
  • 请求B去数据库查询得到旧值
  • 请求B将旧值写入缓存
  • 请求A将新值写入数据库
这时候缓存中的数据就是脏数据,如果缓存没有设置过期时间的话,就导致这个数据在下一次修改之前返回给用户的都是脏数据。

🎯从后者操作可能失败的角度来看,选择哪种策略更好一点?

先更新后删除缓存的话,后者失败,情况是这样的:

  1. A更新数据库,然后缓存没有删除成功
  2. B查询,直接走了缓存【旧数据】

这就出现了缓存跟数据库不一致的情况

先删除缓存后更新数据库的话,后者失败,情况是这样的:

  1. A删除缓存,然后没能更新数据库
  2. B查询,发现缓存没有,则去查询数据库,设置到缓存里边

此时并不会出现缓存跟数据库不一致的情况,因为A还没来得及更新,这样数据库跟缓存中,一直都是旧的数据,但至少不会出现数据不一致的问题

  • 所以如果从这个角度来看的话,先删除缓存,后更新数据库才是更优解。

🎯🎈如何解决后者失败的情况呢?

使用消息队列重试

  1. 请求 A 先对数据库进行更新操作,同时吧
  2. 在对 Redis 进行删除操作的时候发现报错,删除失败
  3. 此时将Redis 的 key 作为消息体发送到消息队列中
  4. 系统接收到消息队列发送的消息后再次对 Redis 进行删除操作



但是这个方案会有一个缺点就是会对业务代码造成大量的侵入,深深的耦合在一起。

有没有必要更新成功后就投递到mq呢?而不是等到redis删除失败了再投递到mq

如果考虑Java项目进程在更新数据库之后就宕机了的话,那无论哪种都没法避免缓存删除的失败 如果硬抠的话,更新成功后,到删除redis还有一段间隙,这个间隙先投递到mq,会更有保障一点,但相应的编码规则也更麻烦了点

  1. 更新数据库成功,投递到mq表示要删除 某个key
  2. 业务继续执行,删除redis:
    1. 删除成功的话,需要删除掉mq里边的消息,防止重复消费
    2. 删除失败的话,就需要消费者拉取mq,再次删除缓存,实现重试机制,当然,如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。后续的处理可能就先丢到死信队列里边了。

可以发现编码确实麻烦了些,因为要考虑重复消费的问题。那么如何解决呢?Java崩了,咱下游还有 mysql 和 mq 没崩呢!也就因此引出了下文的订阅binlog日志的方法

订阅binlog日志

原理:更新数据库成功,就会产生一条变更日志,记录在 binlog 里。

于是我们就可以通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除,阿里巴巴开源的 Canal 中间件就是基于这个实现的。

具体流程:

  1. canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议;
  2. mysql master收到dump请求,开始推送binary log给slave(也就是canal);
  3. canal解析binary log对象(原始为byte流);
  4. canal将解析后的对象,根据业务场景,分发到比如 MySQL 、RocketMQ 或者 ES 中。



总结

#Java##程序员#
全部评论

相关推荐

09-24 20:31
已编辑
西安电子科技大学 Java
9.14笔试A了1.8感觉没戏了结果9.23约9.24下午3.一面,面试难度较大(对我来说)1)上来先来了一道手撕,实现LRU缓存(力扣上那道原题)2)什么场景是高并发场景,有什么解决方案,问我哪里知道的这些?redis为什么这么快,说完又问数据结构为什么高效,String怎么实现的,SDS是什么?3)分布式事务,Seata是怎么解决的,AT模式说着说着他说我知道这个你很熟(直接打断),直接问遇到数据不一致怎么处理,8个数据有一个出错怎么知道哪些数据需要回滚(不会瞎扯,他说不对,让我再考虑,后边又瞎扯都不对直接不问了,说你没实操过)4)redis分布式锁怎么实现的,他质疑我,然后解释了一下。5)数据库隔离级别、MVCC,锁有哪些,特意问了记录锁属于哪个,B+树是什么?回表发生在什么场景下?索引存储什么地方6)跳表的原理是什么(不会),他又问了优势是什么?7)RocketMQ和Kafka优势是什么?怎么实现的,啥的,引出一大堆8)HTTP1.1劣势有哪些?HTTP2.0做了哪些改进?多路复用有哪些实现方式?9)tcp和udp协议能否使用同一个端口号(可能说错了)10)策略模式是什么?并举例说明一下11)组合模式是什么?举例说明12)还问了一些关于自己实验室项目后边问的都有点忘记了反正答的不太行说着说着他直接让我停,问我有什么想问的。我比较菜,答的太差了,以为无了就随便问了两个问题想结束这场羞辱。1)业务是做什么的(他说是什么up主激励计划,说了一些)2)问下来的流程,他说你的HR没告诉你吗?后边又告诉我了。然后我沉默,他也沉默。再就是他问我面的实习生还是应届生(直接无语),我说应届生,是不是我投错了,他看了看说你没投错,上一个面的是实习生,这个安排的应届生啊啥的。后边又问我你想不想来我们这边啊,有没有用过B站啊。总共面了一个小时。四点面完,我一直给朋友吐槽B站这个面试,结果过了一会5点收到明天下午二面,希望能对我好点
点赞 评论 收藏
分享
1 14 评论
分享
牛客网
牛客企业服务