操作模拟
1、先更新数据库还是先更新缓存?
1.1先更新缓存,再更新数据库
按并发的角度来说,有两个线程A、B,操作同一个数据,线程A先更新缓存为1,在线程A更新数据库之前,这时候线程B进来,更新缓存为2,更新数据库为2,这时候线程A更新数据库为1.
此时,缓存中数据为2,但数据库为1,缓存和数据库不一致
因此先更新缓存,再更新数据库会有数据不一致的情况
1.2 先更新数据库,再更新缓存
我们再来进行分析。
线程A先更新数据库为1,还未更新缓存,这时候线程B更新数据库为2,缓存更新为2,最后线程A把缓存更新为1。
最后情况:缓存中为1,数据库中为2
:::info
仍然会出现数据库和缓存不一致问题
:::
小结
【先更新缓存,再更新数据库】和【先更新数据库,再更新缓存】都有可能导致数据库和缓存不一致问题。既然这两者不一致,那么我们能否考虑去掉其中一个呢?当然不能删数据库,那就只能删缓存了。也就是后面的方法。在删缓存的顺序也有讲究。
2、先删缓存,还是先更新数据库?
2.1、先删除缓存,再更新数据库
同样使用多线程来分析,这时候使用读写进行分析。
写线程A进入,删除缓存,这时候°线程B进入,读取数据库值为1,更新缓存为1(由于更新缓存的操作是非常快的,因此出现这种情况的概率会比较大),最后线程A更新数据库为2,就会出现数据库和缓存不一致的情况。
2.2、先更新数据库,再删除缓存
同样使多线程分析。
- 线程A进入,先更新数据库为2,
- 后面读线程B进入,读取数据为1
- 然后线程A删除缓存
- 线程B写入缓存为1
此时仍然会出现数据库与缓存不一致的情况,但是请注意,读取数据和写入缓存数据通常要快得多,因此出现这种情况的概率是比【先删缓存,再更新数据库】的概率低得多的,所以我们只需要再给缓存增加一个过期时间即可。
删除缓存的操作可能会失败
使用消息队列发送请求
:::color2
- 这时候还可以引入消息队列(利用消息重试机制)来进行操作,将删除缓存放到消费者当中去。但是此方法会导致代码入侵,需要修改原来删缓存的业务代码,然后添加消息队列重试机制。
:::
订阅 MySQL binlog,再操作缓存
我们知道,mysql是有一个二进制日志文件的,当数据库产生更新操作时,就会计入该二进制文件中。
【先更新数据库,再删缓存】的策略的第一步是更新数据库,那么更新数据库成功,就会产生一条
变更日志,记录在 binlog里。
于是我们就可以通过订阅binlog日志,拿到具体要操作的数据,然后再执行缓存删除,阿里巴巴开
源的**Canal中间件**就是基于这个实现的。
Canal模拟MySQL主从复制的交互协议,把自己伪装成一个MySQL的从节点,向MySQL主节点
发送dump请求,MySQL收到请求后,就会开始推送Binlog给Canal,Canal解析 Binlog字节流
之后,转换为便于读取的结构化数据,供下游程序订阅使用。
Canal工作原理
前面我们说到直接用消息队列重试机制方案的话,会对代码造成入侵,那么Canal方案就能很好的
规避这个问题,因为它是直接订阅binlog日志的,和业务代码没有藕合关系,因此我们可以通过
Canal+消息队列的方案来保证数据缓存的一致性。
具体的做法是:
:::color1
将binlog日志采集发送到MQ队列里面,然后编写一个简单的缓存删除消息者订阅
binlog日志,根据更新log删除缓存,并且通过ACK机制确认处理这条更新log,保证数据缓存一致
性。这样,我们其实是在业务代码之外对缓存进行删除的,通过直接订阅mysql的binlog文件,而不是在业务代码中手动发送删除消息。
:::
总结
:::info
数据库和缓存保持一致性的问题,最简单快捷的方式就是【先更新数据库,再删除缓存】,但是这会导致缓存命中率变低,如果对缓存命中率有要求的系统,可以考虑【先更新缓存,再更新数据库】或者【先更新数据库,再更新缓存】的方式,但是需要加上锁机制进行线程互斥(把更新缓存和更新数据库的操作放在同步代码块内),这样才能避免多线程同时操作,导致数据库与缓存不一致的情况,但是这会导致写入性能下降。
:::