序
在使用缓存时,我们必须要考虑的是缓存与数据库的双写一致性,是先删缓存还是先更新数据库?是需要强一致性还是最终一致性?延迟双删策略真的就万无一失了吗?虽然网上已经有很多文章分析了,但都比较零散,所以本篇根据自己的经验及网上的文章做了个归纳整理。
正篇
我们先来看看缓存的更新策略,到底是先删缓存还是先更新数据库?(为什么不更新缓存?因为更新缓存更麻烦,一致性更难保证,所以一般都是采用删除策略,简单、粗暴。)
先删缓存再更新数据库
如图,如果第一步删除缓存失败,那么事务直接回滚,数据库和缓存是一致的;如果更新数据库失败,事务回滚,数据库仍是旧数据,其它线程来查的时候,也是将旧数据放入缓存,所以也是一致的。看起来很不错是不?但是再加入一个线程并发读取呢?
可以看到线程A删除缓存后还没来得及更新数据库,或者更新了数据库还没提交事务,若有其它线程来查询,此时缓存没有,则去数据库查询到旧数据放入到缓存,那么数据库和缓存就不是一致的了。所以先删除缓存在原子性被破坏的情形下表现良好,但是在并发场景下就很容易出现不一致。
先更新数据库再删除缓存
再来看看先更新数据库再删除缓存又有什么问题。
如果第一步更新数据库失败,事务直接回滚,不会有什么影响;同样更新数据库成功,删除缓存失败,也不会有什么问题;但若是删除缓存是放在提交事务之后,那么当删除缓存失败时(如链接超时、异常断开),缓存和数据库就会出现不一致问题。
另外这个操作在并发场景下也是有问题的,下面分析两个场景:
第一个场景如上图,在更新数据库的事务未提交前,缓存刚好过期,这时其它线程来查数据库并且卡住了,等到线程A删除缓存操作完成后线程B才返回,那么也造成不一致情况,不过这个场景比较极限,触发条件有两个,一是更新数据库时缓存刚好过期,二是查询数据库慢于删除缓存,所以一般可以不考虑。
第二个场景如上图,当线程A删除完缓存后还有其它操作,导致事务未提交,那么其它线程这时也会查询到旧数据放入缓存。
除此之外,把删除缓存放到事务之外的并发场景,读者可自行分析看看。
延迟双删
延迟双删实际上是基于先删除缓存再更新数据库的改进方案,前面说到先删除缓存再更新数据库的主要问题是在高并发场景下很容易造成不一致,那么只要更新完数据库后再删一次缓存就可以了,延迟一段时间是为了避免其它查询到旧数据的线程比删除缓存更晚返回。
看起来很完美,这也是其它文章中常推荐的方式,但仔细想想还是有问题的。高并发场景下第一个删除有什么作用?需要延迟多久?第二次删除缓存失败了怎么办?
第一个问题,高并发场景下第一个删除其实是没啥作用的,还是会有一大堆查询到旧数据的线程。
第二个问题,在提交事务和删除缓存的这个时间段,且第一个删除缓存不起作用的情况下,其它线程都会查询到旧数据。
第三个问题,第二次缓存如果删除失败了,那么也就是和第一个方案是一样的了,所以主要考虑如何避免删除失败。这个我们只需要一个重试机制就可以避免,比如放入mq,确保删除成功,但这样对业务代码侵入比较大,所以考虑将第二次删除操作交由其它组件完成,比如使用canal监听binlog,异步删除缓存,也就是Cache Aside Pattern。
综上所述,很明显这种方案适合的是并发量不高,业务实时性和一致性要求不高的场景。那要如何保证实时的强一致性呢?
加锁串行化实现强一致性
如图,我们只需要在更新事务开启前以及查询线程查数据库前加锁,那么就可以保证实时的强一致性。但这里需要注意,加锁不能在更新事务内,否则还是可能会出现不一致问题。
虽然这个方案可以很好的保证数据一致,但缺点也很明显,在读写比较频繁的情况下,会造成大量的锁竞争,导致性能降低,不过这样的业务一般是可以考虑最终一致性或是直接写缓存,再异步写入库,具体情况还是需要根据业务来分析。
总结
综上所述,在大部分情况下我们使用延迟双删保证最终一致性即可,但小部分业务可能需要实时强一致性,这时就不得不串行化操作来实现。
文中错误或您有更好的方案,欢迎指出。