redis面试:怎么保证缓存和数据库数据的一致性?

旧城等待, 2022-08-28 08:42 271阅读 0赞

你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?

一般来讲,有四种方案

  • 先更新数据库,然后更新缓存
  • 先更新缓存,后更新数据库
  • 先删除缓存,后更新数据库
  • 先更新数据库,后删除缓存

第一种和第二种方案,没有人使用的,因为第一种方案存在问题是:并发更新数据库场景下,会将脏数据刷到缓存。

第二种方案存在的问题是:如果先更新缓存成功,但是数据库更新失败,则肯定会造成数据不一致。

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

最经典的缓存+数据库读写的模式

  • 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存
  • 更新的时候,先更新数据库,然后再删除缓存

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

原因很简单,很多时候,在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值

比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据并进行计算,才能计算出缓存最新的值。

另外更新缓存的代价有时候很高的。是不是说,每次修改数据库的时候,都一定要将其对应的缓存更新一份?也许有的场景是这样,但是对于比较复杂的缓存数据计算的场景,就不是这样了。如果你频繁修改一个缓存涉及的多个表,缓存也频繁更新。但是问题在于,这个缓存到底会不会频繁被访问到

举个栗子,一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;但是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。实际上,如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。用到缓存才去算缓存。

其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。像 mybatis,hibernate,都有懒加载思想。查询一个部门,部门带了一个员工的 list,没有必要说每次查询部门,都里面的 1000 个员工的数据也同时查出来啊。80% 的情况,查这个部门,就只是要访问这个部门的信息就可以了。先查部门,同时要访问里面的员工,那么这个时候只有在你要访问里面的员工的时候,才会去数据库里面查询 1000 个员工。

问题:先更新数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。

在这里插入图片描述

解决思路一:先删除缓存,然后更新数据库

如果数据库更新失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,所以去读了数据库中的旧数据,然后更新到缓存中。

解决思路二: 利用消息队列进行删除的补偿

在这里插入图片描述

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

但这个方案有一个缺点就是会对业务代码造成到来的侵入,深深的耦合在一起,所以这时会有一个优化的方案,我们知道mysql数据库更新操作后再binlg日志中我们都能够找到相应的操作,那么我们可以订阅 Mysql 数据库的 binlog 日志对缓存进行操作。
在这里插入图片描述

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

问题:数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改。完了,数据库和缓存中的数据不一样了…

请求 A(更新操作) 和请求 B(查询操作)

  • 请求A进行写操作,删除缓存
  • 请求B查询发现缓存不存在
  • 请求B去数据库查询得到旧值
  • 请求B将旧值写入缓存
  • 请求A将新值写入数据库。

只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题。其实如果说你的并发量很低的话,特别是读并发很低,每天访问量就 1 万次,那么很少的情况下,会出现刚才描述的那种不一致的场景。但是问题是,如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的情况。

解决方法一:延迟双删

最简单的解决方法是延迟双删

  • 先淘汰缓存
  • 再写数据库
  • 休眠一秒,再次淘汰缓存,这样做,可以将1s内所造成的缓存1脏数据,再次删除。确保读请求结束。写请求可以删除读请求造成的缓存脏数据。自行评估自己的项目的读数据业务逻辑的耗时,写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。

如果使用的是 Mysql 的读写分离的架构的话,那么其实主从同步之间也会有时间差。

在这里插入图片描述
此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作)

  • 请求 A 更新操作,删除了 Redis
  • 请求主库进行更新操作,主库与从库进行同步数据的操作
  • 请 B 查询操作,发现 Redis 中没有数据
  • 去从库中拿去数据
  • 此时同步数据还未完成,拿到的数据是旧数据

此时的解决方法就是如果是对redis进行填充数据的查询数据库操作,那么就强制将其指向主库进行查询

在这里插入图片描述

解决方法二:更新与读取操作进行异步串行化

怎么做:

(1)异步串行化

  • 在系统内部维护n个内存队列,更新数据的时候,根据数据的唯一标识,将该操作路由之后,发送到其中一个jvm内部的内存队列中(对同一数据的请求发送到同一个队列)。读取数据的时候,如果发现数据不在缓存中,并且此时队列里有更新库存的操作,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也将发送到同一个jvm内部的内存队列中。然后每个队列对应一个工作线程,每个工作线程串行地拿到对应的操作,然后一条一条的执行。
  • 这样的话,一个数据变更的操作,先执行删除缓存,然后再去更新数据库,但是还没完成更新的时候,如果此时一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,排在刚才更新库的操作之后,然后同步等待缓存更新完成,再读库。

(2)读操作去重

  • 多个读库更新缓存的请求串在同一个队列中是没意义的,因此可以做过滤,如果发现队列中已经有了该数据的更新缓存的请求了,那么就不用再放进去了,直接等待前面的更新操作请求完成即可,待那个队列对应的工作线程完成了上一个操作(数据库的修改)之后,才会去执行下一个操作(读库更新缓存),此时会从数据库中读取最新的值,然后写入缓存中。
  • 如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回;如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。(返回旧值不是又导致缓存和数据库不一致了么?那至少可以减少这个情况发生,因为等待超时也不是每次都是,几率很小吧。这里我想的是,如果超时了就直接读旧值,这时候仅仅是读库后返回而不放缓存)

  • 如何保证缓存与数据库的双写一致性?

发表评论

表情:
评论列表 (有 0 条评论,271人围观)

还没有评论,来说两句吧...

相关阅读