引言:今年4月份听过一次关于“库和缓存一致性”的线下分享,收获很大。基于这次分享,加上之前的项目经验,总结下库和缓存的一致性问题。
缓存是计算机中加快访问速度,缩短RT的通用解决方案,它无处不再。浏览器中有缓存、CDN上有缓存、应用服务器有缓存、数据库本身把热点数据放在内存中也可以认为是一种缓存,甚至包括CPU也有多级Cache来缓存数据,加快数据访问速率。总结起来就是,我们为了加速数据访问,缩短访问链路,减少数据获取成本都可以采用缓存这种技术方案,以空间换时间。话题扯的有点远,回归正题,本文我们主要讨论应用服务器上,数据库和缓存之间的一致性问题。
读请求
写请求
这里很重要的一点在写请求中,要删除缓存而不是更新缓存。缓存的更新会发生在下一次读请求时。这里为什么会选择删除缓存,而没有更新缓存呢。因为如果更新缓存的话,存在并发写操作时,无法保证多个进程的执行顺序,有可能旧数据会覆盖新数据。Cache Aside Pattern方案虽然使用范围比较广,但它本身也存在一些问题。 问题一
如上图,进程A在T1时刻数据写入库中,T2时刻删除了缓存。在高并发场景下,T1和T2时刻之间的读请求从缓存中读到的数据和库中的数据会出现不一致。 问题二
如上图,进程A在T1时刻把数据写入库中,T2时刻删除缓存失败。失败的原因暂不详谈。这种情况下会导致库和缓存数据长时间不一致。
问题三
如上图,进程A是读请求,进程B是写请求。
此时库中数据是B,缓存中是A,出现了数据不一致。
方案二是在方案一的基础上发展而来的。
读请求 同方案一
写请求
此方案的写请求,与Cache Aside Pattern方法相比,最前面多了一次删除缓存,这样就可以避免T1~T2时间差的数据不一致。 在最后删除缓存前有一个短暂的等待,这样就避免了方案一中的问题三:防止因为上下文切换等原因导致的数据不一致问题。
读请求
写请求
加锁之后,数据一致性能得到很好的保证,但是数据的访问效率会受到较大的影响。所以,很多时候加锁未必是理想方案。不过在写少、读多的业务场景中也可以考虑。
读请求
写请求 只写数据库
对于缓存的更新,我们采用订阅数据库日志的方式实现。比如采用阿里开源的Canal,订阅MySQL的Binlog,然后放入MQ,消费端消费数据,然后把数据和缓存中的数据进行比较,把不一致的数据从缓存中删除。删除失败可以尝试多次删除。
这种方案,可以把缓存删除逻辑从业务代码中剥离,业务开发专注于业务;但是需要引入额外的组件,花费更高的维护成本。
以上是处理库和缓存数据一致性问题的常用方案。他们都有一个共同点删除缓存,而不是更新缓存。不管是哪一种方案,都很难做到库和缓存数据完全一致。所以在各方案中,都可以加异步的对账逻辑,定期检查库和缓存中的数据是否一致,出现不一致时,删除缓存数据即可。