前言

又是redis,又是mysql的,还有双写,一致性,乍一看,好像很高级的样子,这也是大厂比较热门的面试题。当然不仅是面试吧,实际开发中,这也是一个非常常见的场景。这道题抛开redis和mysql,其实就是在问缓存和数据库在双写的场景下,是如何保证数据一致性的?表急,跟着我的思路一起来捋一捋这个问题。

说方案之前,先说说一致性和几种经典的缓存模式

一致性

一致性一般指分布式系统中,多个节点的数据的值是一致的。

  • 强一致性:这种一致性是最符合用户直觉的,往系统写入什么,读出来的就是什么,但是实现起来往往对系统的性能影响很大
  • 弱一致性:这种一致性在系统写入成功后,并不承诺立刻可以读到写入的值,也不承偌多久之后数据能达到一致,但是会尽可能快的在某个时间级别(比如秒级)后,数据会达到一致
  • 最终一致性:这个是弱一致性的一个特例。之所以单独拿出来写,是因为这是业界目前比较推崇的一种做法。表现为系统写入数据后,会保证在一定时间内,达到数据一致的状态。

三种缓存模式

尽量用最简洁的话来总结这三种模式

一、Cache-Aside Pattern (旁路缓存模式)

读流程如下:

  1. 读的时候,先读缓存,缓存命中,直接返回数据
  2. 缓存没有命中,就去读数据库,然后用数据库的数据更新缓存,再返回数据

写流程如下:

  1. 更新数据的时候,先更新数据库,再删除缓存

思考问题:为什么是删除缓存,而不是更新缓存?

二、Read-Through/Write-Through(读写穿透)

这种模式跟旁路缓存模式最明显的差别在于应用程序和缓存/数据库之间多了一层 缓存抽象层(Cache Provider)

读流程和旁路缓存模式一样,只不过用户程序是跟缓存抽象层做交互

写流程不大一样,跟缓存抽象层做交互,更新数据时,先更新数据库,再更新缓存

三、Write-behind(异步缓存写入)

这个模式也有 缓存抽象层(Cache Provider),跟读写穿透模式不同的地方在于,更新数据时,只更新缓存,不直接更新数据库,而是通过批量异步去更新数据库。

这种方式数据一致性不强,适合频繁写的场景,比如Mysql的InnoDB Buffer Pool机制。

思考问题

一、操作缓存的时候,到底是删除缓存,还是更新缓存?

实际场景中,一般使用的是上述的第一种Cache-Aside Pattern (旁路缓存模式),但是这种模式在写入数据的时候,为什么是删除缓存而不是更新缓存呢?

试想一下,如果有两个线程都发起了写操作,线程A先发起写操作,线程B后发起写操作,但是由于网络原因,线程B先更新了缓存,然后线程A再更新缓存,所以这个时候数据库的数据是线程2最后写进去的,这个时候脏数据就出现,缓存和数据库就不一致了。而如果删除缓存,而不直接更新缓存,就不会有这个问题。

另外更新缓存相对删除缓存,还有一个点需要考虑:

  1. 如果缓存的值,是经过复杂计算,可能是多个字段计算才得到。如果更新频繁,就很浪费性能,因为更新了也不一定马上就用到

二、双写的场景下,先操作数据库,还是先操作缓存?

  1. 线程A发起一个写操作,第一步先删除缓存
  2. 此时线程B发起一个读操作,结果缓存没有了,所以开始读DB,读出来一个老数据
  3. 然后线程B就把这个老数据写入了缓存
  4. 线程A再把新的数据写入DB

酱紫不就又出现数据不一致的问题了吗,所以Cache-Aside模式,要先操作数据库,再操作缓存

那肯定很多小伙伴会问,先操作数据库,在操作缓存,不是原子性不还是会出现数据不一致吗?

答案是会的,但是只有出现在删除缓存失败的情况才会出现,这个可以不考虑。

三、有必要做数据库和缓存的强一致性吗?

数据库缓存强一致性?拜托,那你还用缓存干嘛,这不是没事找事嘛

三种解决方案

① 缓存延时双删

  1. 先删除缓存
  2. 更新数据库
  3. 休眠一会,再删除缓存

只有休眠那一会,可能出现脏数据,所以可以设置在业务可以容忍的范围内,但是这个方案也不是特别好,因为要容忍一段时间数据不一致

② 删除缓存重试机制

不管是延时双删,还是先操作数据库,后删除缓存,都可能出现最后一次删除缓存失败,导致数据不一致的问题。

所以可以使用重试机制来优化。将删除失败的key写入队列,消费队列,获取要删除的key,重试删除操作

③ 读取binlog异步删除缓存

重试删除缓存会造成很多代码入侵。所以,还可以考虑使用数据库的 binlog 来异步淘汰key

比如,使用阿里提供的canal将binlog日志采集发送到MQ中,然后通过ACK机制确认处理这条更新消息,删除缓存