共计 1812 个字符,预计需要花费 5 分钟才能阅读完成。
前言
又是redis,又是mysql的,还有双写,一致性,乍一看,好像很高级的样子,这也是大厂比较热门的面试题。当然不仅是面试吧,实际开发中,这也是一个非常常见的场景。这道题抛开redis和mysql,其实就是在问缓存和数据库在双写的场景下,是如何保证数据一致性的?表急,跟着我的思路一起来捋一捋这个问题。
说方案之前,先说说一致性和几种经典的缓存模式
一致性
一致性一般指分布式系统中,多个节点的数据的值是一致的。
- 强一致性:这种一致性是最符合用户直觉的,往系统写入什么,读出来的就是什么,但是实现起来往往对系统的性能影响很大
- 弱一致性:这种一致性在系统写入成功后,并不承诺立刻可以读到写入的值,也不承偌多久之后数据能达到一致,但是会尽可能快的在某个时间级别(比如秒级)后,数据会达到一致。
- 最终一致性:这个是弱一致性的一个特例。之所以单独拿出来写,是因为这是业界目前比较推崇的一种做法。表现为系统写入数据后,会保证在一定时间内,达到数据一致的状态。
三种缓存模式
尽量用最简洁的话来总结这三种模式
一、Cache-Aside Pattern (旁路缓存模式)
读流程如下:
- 读的时候,先读缓存,缓存命中,直接返回数据
- 缓存没有命中,就去读数据库,然后用数据库的数据更新缓存,再返回数据
写流程如下:
- 更新数据的时候,先更新数据库,再删除缓存
思考问题:为什么是删除缓存,而不是更新缓存?
二、Read-Through/Write-Through(读写穿透)
这种模式跟旁路缓存模式最明显的差别在于应用程序和缓存/数据库之间多了一层 缓存抽象层(Cache Provider)

读流程和旁路缓存模式一样,只不过用户程序是跟缓存抽象层做交互
写流程不大一样,跟缓存抽象层做交互,更新数据时,先更新数据库,再更新缓存。

三、Write-behind(异步缓存写入)
这个模式也有 缓存抽象层(Cache Provider),跟读写穿透模式不同的地方在于,更新数据时,只更新缓存,不直接更新数据库,而是通过批量异步去更新数据库。
这种方式数据一致性不强,适合频繁写的场景,比如Mysql的InnoDB Buffer Pool机制。
思考问题
一、操作缓存的时候,到底是删除缓存,还是更新缓存?
实际场景中,一般使用的是上述的第一种Cache-Aside Pattern (旁路缓存模式),但是这种模式在写入数据的时候,为什么是删除缓存而不是更新缓存呢?
试想一下,如果有两个线程都发起了写操作,线程A先发起写操作,线程B后发起写操作,但是由于网络原因,线程B先更新了缓存,然后线程A再更新缓存,所以这个时候数据库的数据是线程2最后写进去的,这个时候脏数据就出现,缓存和数据库就不一致了。而如果删除缓存,而不直接更新缓存,就不会有这个问题。

另外更新缓存相对删除缓存,还有一个点需要考虑:
- 如果缓存的值,是经过复杂计算,可能是多个字段计算才得到。如果更新频繁,就很浪费性能,因为更新了也不一定马上就用到
二、双写的场景下,先操作数据库,还是先操作缓存?
- 线程A发起一个写操作,第一步先删除缓存
- 此时线程B发起一个读操作,结果缓存没有了,所以开始读DB,读出来一个老数据
- 然后线程B就把这个老数据写入了缓存
- 线程A再把新的数据写入DB

酱紫不就又出现数据不一致的问题了吗,所以Cache-Aside模式,要先操作数据库,再操作缓存。
那肯定很多小伙伴会问,先操作数据库,在操作缓存,不是原子性不还是会出现数据不一致吗?
答案是会的,但是只有出现在删除缓存失败的情况才会出现,这个可以不考虑。
三、有必要做数据库和缓存的强一致性吗?
数据库缓存强一致性?拜托,那你还用缓存干嘛,这不是没事找事嘛
三种解决方案
① 缓存延时双删

- 先删除缓存
- 更新数据库
- 休眠一会,再删除缓存
只有休眠那一会,可能出现脏数据,所以可以设置在业务可以容忍的范围内,但是这个方案也不是特别好,因为要容忍一段时间数据不一致
② 删除缓存重试机制
不管是延时双删,还是先操作数据库,后删除缓存,都可能出现最后一次删除缓存失败,导致数据不一致的问题。
所以可以使用重试机制来优化。将删除失败的key写入队列,消费队列,获取要删除的key,重试删除操作
③ 读取binlog异步删除缓存
重试删除缓存会造成很多代码入侵。所以,还可以考虑使用数据库的 binlog 来异步淘汰key。
比如,使用阿里提供的canal将binlog日志采集发送到MQ中,然后通过ACK机制确认处理这条更新消息,删除缓存