文章目录
目标
保证缓存和数据库数据一致性
有缓存的查询一般是这样的:先查询缓存,缓存不存在,去查询数据库,然后将数据库的值放入缓存。
先操作缓存再操作数据库
1. 先删缓存后更新数据库
先删缓存后更新数据库
问题:先删除缓存后,数据库没有更新成功之前,此时读取缓存,缓存不存在,然后去读取数据库,数据库还是旧值,将旧值更新到缓存,后面查询缓存查到的是不符合预期的旧值,缓存不一致发生。
2. 普通双删
对于第1种方案,先删缓存后更新数据库,会造成其他事务读取到数据库旧值并更新缓存的问题,能直接想到的方法是再把这个缓存删掉,这就是普通双删缓存。
问题:第二次删除缓存的时机不好控制,普通双删是更新完数据库后马上再次删除缓存,但是可能由于二次删除缓存速度太快,其他事务可能读数据库才刚完成,还没来得及添加缓存,就被二次删除缓存了,导致这次缓存删除无用,依旧会造成缓存不一致。
3. 延时双删
对方案2优化下,既然更新数据库后马上删除缓存可能无效,那就等另一个事务更新缓存后,再二次删除,这个加入"等"这个动作,就是延时双删了,容易得出,等的目的是,确保 修改数据库 -> 清空缓存前,其他事务的更改缓存操作已经执行完,因此,"等"的时间大于读写缓存的时间即可。
针对相同条件,查询数据库的时间一般比更新数据库快,因此延时双删可以发挥较好的作用,但是不能完全避免缓存不一致的问题。
具体流程:
- 线程1删除缓存,然后去更新数据库
- 线程2来读缓存,发现缓存已经被删除,所以直接从数据库中读取,这时候由于线程1还没有更新完成,所以读到的是旧值,然后把旧值写入缓存
- 线程1,根据估算的时间,sleep,由于sleep的时间大于线程2读数据+写缓存的时间,所以缓存被再次删除
- 如果还有其他线程来读取缓存的话,就会再次从数据库中读取到最新值
先操作数据库再操作缓存
4. 先更新数据库后删缓存
问题:先更新数据库后,还没有删除缓存,此时读取缓存都是旧值,缓存不一致。
5. 消息队列
先更新数据库,成功后往消息队列发消息,消费到消息后再删除缓存,借助消息队列的重试机制来实现,达到最终一致性的效果。
问题:
- 引入消息中间件之后,问题更复杂了,怎么保证消息不丢失更麻烦
- 就算更新数据库和删除缓存都没有发生问题,消息的延迟也会带来短暂的不一致性,不过这个延迟相对来说还是可以接受的
6. 监听binlog的消费队列
先更新数据库,成功后借助监听binlog的消息队列来做删除缓存的操作。
好处:
- 不用自己引入,侵入到业务代码中,中间件做了解耦;
- 中间件的这个东西本身就保证了高可用。
问题:
- 消息延迟的问题依然存在,但是相比单纯引入消息队列的做法更好一点。
而且,如果并发不是特别高的话,这种做法的实时性和一致性都还算可以接受的。
设置缓存过期时间
每次放入缓存的时候,设置一个过期时间,比如5分钟,以后的操作只修改数据库,不操作缓存,等待缓存超时后从数据库重新读取。
如果对于一致性要求不是很高的情况,可以采用这种方案。
这个方案还会有另外一个问题,就是如果数据更新的特别频繁,不一致性的问题就很大了。
在实际生产中,我们有一些活动的缓存数据是使用这种方式处理的。
因为活动并不频繁发生改变,而且对于活动来说,短暂的不一致性并不会有什么大的问题。
为什么是删除缓存而不是更新缓存
- 对于缓存数据可能是比较复杂的,更新需要有复杂的业务逻辑,增加复杂度;
- 删除缓存,等查询时再增加缓存,当多写少读场景,更新缓存没必要
总结
- 缓存是删除,而不是更新;
- 先删后更新db,使用延迟双删;
- 先更新db后删,使用消息队列 or binlog同步;
- 数据一致性要求不那么高的场景,设置缓存有效期即可;
参考: