面试:如何保证Redis缓存和Mysql数据库一致性的问题

前言

为什么会有缓存和数据一致性的问题呢?

对于热点数据(经常查询,但不经常修改的数据),我们可以放入redis缓存中,因为如果我们使用Mysql的话,DB是扛不住的。因此采用缓存中间件来增加查询效率,但需要保证Redis中读取数据与数据库存储数据是一致的。

客户端对于数据库主要是读写两个操作。针对放入redis中缓存的热点数据,当客户端想读取数据的时候就在缓存中直接返回数据,即缓存命中,当读数据不在缓存内,就需要从数据库中将数据读入缓存,即缓存未命中。我们可以看到读操作不会导致缓存与数据库的数据不一致

正文:

1. 常见方案

通常情况下,我们使用缓存的主要目的是为了提升查询的性能。大多数情况下,我们是这样使用缓存的:
在这里插入图片描述

  • 这是缓存最常用的用法,看上去没问题,但你忽视了一个非常重要的细节:**如果数据库的某条数据,放入缓存后,又立马更新了,那么如何更新缓存呢?**因为不更新缓存,下一次读取的时候命中缓存读取的就是旧数据。
  • 目前更新缓存主要有以下四种方案:
  1. 先写缓存,在更新数据库
  2. 先更新数据库,在写缓存(双写)

建议缓存一致的处理:
3. 先删除缓存,在更新数据库
4. 先写数据库,在删除缓存

先写缓存,在写数据库

在这里插入图片描述
我们想一下,如果我们每次写操作后,刚写完缓存,突然网络不好导致数据库写入失败。
在这里插入图片描述
**缓存更新成了最新数据,但数据库没有,这样缓存中的数据不就变成脏数据了?**如果此时该用户的查询请求,正好读取到该数据,就会出现问题,因为该数据在数据库中根本不存在,这个问题非常严重。
我们都知道,缓存的主要目的是把数据库的数据临时保存在内存,便于后续的查询,提升查询速度。
但如果某条数据,在数据库中都不存在,你缓存这种“假数据”又有啥意义呢?

因此,先写缓存,再写数据库的方案是不可取的,在实际工作中用得不多

先更新数据库在更新缓存

在这里插入图片描述
用户的写操作,先写数据库,再写缓存,可以避免之前“假数据”的问题。但它却带来了新的问题
什么问题呢?

  • 在高并发业务场景下,写数据库和写缓存都属于远程操作,为了防止出现大事物造成死锁问题,通常==建议写数据库和写缓存不要放在一个事务中。==也就是说该方案中,如果数据库成功了但是写缓存失败了,数据库中已写入的数据不会进行回滚。
  • 会出现数据库新数据,缓存是旧数据的问题
    在这里插入图片描述
  1. 请求a先过来,刚写完了数据库。但由于网络原因,卡顿了一下,还没来得及写缓存。
  2. 这时候请求b过来了,先写了数据库。
  3. 接下来,请求b顺利写了缓存。
  4. 此时,请求a卡顿结束,也写了缓存。
    很显然,在这个过程当中,请求b在缓存中的新数据,被请求a的旧数据覆盖了。
    也就是说:在高并发场景中,如果多个线程同时执行先写数据库,再写缓存的操作,可能会出现数据库是新值,而缓存中是旧值,两边数据不一致的情况。

从上我们可以看到先写数据库在写缓存是比较浪费系统资源的,不建议使用

更新数据库更新缓存使用的场景

如果我们的业务对缓存命中率有很高的要求,我们可以采用「更新数据库 + 更新缓存」的方案,因为更新缓存并不会出现缓存未命中的情况。

解决方案

  • 在更新缓存前先加个分布式锁,保证同一时间只运行一个请求更新缓存,就会不会产生并发问题了,当然引入了锁后,对于写入的性能就会带来影响。
  • 在更新完缓存时,给缓存加上较短的过期时间,这样即时出现缓存不一致的情况,缓存的数据也会很快过期,对业务还是能接受的
  • 通过上述两个双写我们可以知道,如果直接更新缓存的问题是很多的,因此我们换一种思路,从更新缓存->删除缓存

先删除缓存,在更新数据库

在这里插入图片描述

扫描二维码关注公众号,回复: 15492481 查看本文章
  • 高并发下
    在这里插入图片描述

  • A线程删除缓存,但是此时更新数据库的操作还未完成,此时B线程来读取缓存发现缓存没有数据,就去读取数据库的旧值,更新到缓存中,此时A线程更新完了,将新值写入数据库。这种场景下的数据不一致性问题怎么解决呢?。

解决方案,延迟双删

A线程删除缓存在更新数据库,此时A的更新操作还未完成,而B线程来读取缓存发现缓存没有,去读取数据库,读取的是旧值,然后把旧值写入缓存。A线程Sleep到B线程写入缓存后,在执行删除缓存操作。当其他线程来读取时,数据库就是最新值。
在这里插入图片描述

如果第二次删除删除缓存失败,那么可以采用消息队列的重试机制。

如果第二次删除失败,采用重试机制

  • 重试机制原理图
    在这里插入图片描述

先更新数据库,在删除缓存

在这里插入图片描述

  • 这个更明显,如果A线程数据库更新成功,而缓存失败的话,或者还未来得及删除,那么此时B线程来读取就是旧值,还是会不一致。
    在这里插入图片描述

解决措施(重试和binlog):

  • 消息队列
    我们可以引入消息队列加粗样式将第二个操作(删除缓存)要操作的数据加入到消息队列,由消费者来操作数据

如果应用删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存,这个就是重试机制。当然,如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。
如果删除缓存成功,就要把数据从消息队列中移除,避免重复操作,否则就继续重试。

  • 订阅Mysql binLog,在操作缓存

「先更新数据库,再删缓存」的策略的第一步是更新数据库,那么更新数据库成功,就会产生一条变更日志,记录在 binlog 里。

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

**Canal 模拟 MySQL 主从复制的交互协议,把自己伪装成一个 MySQL 的从节点,**向 MySQL 主节点发送 dump 请求,MySQL 收到请求后,就会开始推送 Binlog 给 Canal,Canal 解析 Binlog 字节流之后,转换为便于读取的结构化数据,供下游程序订阅使用。
在这里插入图片描述
所以,如果要想保证「先更新数据库,再删缓存」策略第二个操作能执行成功,我们可以使用「消息队列来重试缓存的删除」,或者「订阅 MySQL binlog 再操作缓存」,这两种方法有一个共同的特点,都是采用异步操作缓存

面试版本回答

1.更新数据库,在手动清除Redis缓存,在重新查询最新的数据同步到Redis中。
2.更新Mysql数据库,在采用MQ异步的形式同步数据到Redis中,优点是解耦,缺点是延迟的概率大。
3.更新数据库,在基于订阅数据库中的binlog日志采用mq异步的形式同步到Redis中。
4.订阅mysql中的Binlog文件,异步的形式同步到Redis中(canal框架)

参考文章链接

阿里云开发和社区:如何保证数据库和缓存双写一致性?
小林coding:数据库和缓存如何保证一致性

猜你喜欢

转载自blog.csdn.net/weixin_59823583/article/details/129072734