一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情。
要保证缓存和数据库的强一致性,最好的方法就是加分布式锁,但是引入缓存的目的就是提高性能,而加分布式锁付出的的代价很可能超过引入缓存带来的性能提升
现在 业内常用的是 Cache Aside Pattern + 延时双删 这种无锁的方案,只能在保证并发的前提下尽可能减少不一致的可能
Cache Aside Pattern(边缘缓存模式)
Cache Aside Pattern(边缘缓存模式),是最经典的缓存一致性处理模式
查询操作:程序先从缓存中读取数据,如果命中,则直接返回,如果没有命中,则从数据库中读取,成功之后将数据放到缓存中
更新操作:先更新数据库,再删缓存
对于 更新操作有四种方案:
- 先更新数据库,再更新缓存
- 先更新缓存,再更新数据库
- 先删除缓存,再更新数据库
- 先更新数据库,再删除缓存
下面来分析,每种方案的优劣性以及为什么选择 先更新数据库,再删除缓存 这种方案。
如何保证两步操作都成功?
影响数据一致性有两方面,一是其中一步操作失败,如第一个方案中,更新缓存成功,但更新数据库失败,就会对业务造成影响。
解决方法:
异步重试
当操作失败后,将重试的请求写到 【消息队列】中去,然后由专门的消费者来重试,直到成功。
订阅数据库变更日志,再操作缓存
具体来讲就是,我们的业务应用在修改数据时,「只需」修改数据库,无需操作缓存。
拿 MySQL 举例,当一条数据发生修改时,MySQL 就会产生一条变更日志(Binlog),我们可以订阅这个日志,拿到具体操作的数据,然后再根据这条数据,去删除对应的缓存。订阅变更日志的中间件比如阿里的canal
并发引发的一致性问题
并发场景也会引发数据库缓存一致性问题。
- 先更新数据库,再更新缓存 和 先更新缓存,再更新数据库
采用「先更新数据库,再更新缓存」的方案,并且两步都可以 成功执行 的前提下,存在并发问题会怎么样呢?
有线程 A 和线程 B 两个线程,需要更新「同一条」数据,会发生这样的场景:
- 线程 A 更新数据库(X = 1)
- 线程 B 更新数据库(X = 2)
- 线程 B 更新缓存(X = 2)
- 线程 A 更新缓存(X = 1)
最终 X 的值在缓存中是 1,在数据库中是 2,发生不一致。
也就是说,A 虽然先于 B 发生,但 B 操作数据库和缓存的时间,却要比 A 的时间短,执行时序发生「错乱」,最终这条数据结果是不符合预期的。
同样的,采用 先更新缓存,再更新数据库 也会有这样类似的问题,所以我们接下来考虑 删除缓存
删除缓存 对应2种方案:
- 先删除缓存,后更新数据库
- 先更新数据库,后删除缓存
- 先删除缓存,后更新数据库
如果两个线程并发读写数据,可能会出现以下情况:
可以看到,先删除缓存,后更新数据库,当发生「读+写」并发时,还是存在数据库和缓存数据不一致的情况。这种不一致的情况会一直持续,直到 缓存过期 或者 下一次删除缓存 。可见这个时间窗口是不可预知的,有可能很长。
- 先更新数据库,后删除缓存
我们再来看 先更新数据库,后删除缓存 出现并发问题的情况:
可以看到这种情况还是有可能会发生的,但其实他概率很低,因为它必须满足3个条件:
- 缓存刚好失效
- 读写请求并发
- 更新数据库+删除缓存时间要比 读数据库 + 写缓存时间短
条件3发生概率非常低,因为一般写数据库一般会先「加锁」,所以写数据库,通常是要比读数据库的时间更长的。所以,我们应该采用 先更新数据库,后删除缓存 这种方案,来操作数据库和缓存。
主从库延迟和延迟双删问题
- 【读写分离 + 主从复制延迟】导致不一致
先更新数据库,后删除缓存 这种方案在 【读写分离 + 主从复制延迟】还是会导致不一致。
- 线程 A 更新主库 X = 2(原值 X = 1)
- 线程 A 删除缓存
- 线程 B 查询缓存,没有命中,查询「从库」得到旧值(从库 X = 1)
- 从库「同步」完成(主从库 X = 2)
- 线程 B 将「旧值」写入缓存(X = 1)
最终 X 的值在缓存中是 1(旧值),在主从库中是 2(新值),发生不一致。
- 先删除缓存,后更新数据库
同时上面提到的 先删除缓存,后更新数据库 这种方案也会出现不一致场景
想解决以上两种情况,可以再更新完数据库后,一段时间后 【延迟删】,这就是 延迟双删策略 ,目的就是把缓存清掉,这样一来,下次就可以从数据库读取到最新值,写入缓存。
但是延迟时间要设置多久呢?
对于第一种问题:延迟时间要大于「主从复制」的延迟时间
第二种问题:延迟时间要大于线程 B 查询数据库 + 写入缓存的时间
可以看出这个时间很难评估,所以在实际使用中,还是建议采用「先更新数据库,再删除缓存」的方案,同时,要尽可能地保证「主从复制」不要有太大延迟,降低出问题的概率。