前言
随着互联网业务的发展,其中越来越多场景使用了缓存来提升服务质量。从系统角度而言,缓存的主要目标是减轻数据库压力(特别是读取压力)并提高服务响应速度。引入缓存就不可避免会涉及到缓存与业务数据库数据一致性的问题,而不同的业务场景对数据一致性的要求也有所不同。本文将针对常见的数据库+缓存双写模式进行数据一致性分析。
数据库+缓存双写
数据库+缓存双写模式是一种常见的数据处理模式,它结合了数据库和缓存的优势。所以,说白了,数据库+缓存双写就是值,在数据处理过程下,写操作会同时更新数据库和缓存。
当数据需要被写入时,首先会将更新操作应用于数据库,确保数据的持久性和一致性。接下来,更新的数据也会被写入缓存,以提高后续读取请求的响应速度。
这种双写模式确保了数据的可靠性和一致性(其实在高并发情况下,并会有数据不一致的情况,后面将详细分析)。读取数据时,系统首先检查缓存,如果缓存中存在所需数据,则直接从缓存中获取,减轻了对数据库的负载。如果缓存中不存在所需数据,系统会从数据库中获取,并将获取到的数据写入缓存,以便将来的读取请求能够从缓存中获得更快的响应。
通过使用数据库+缓存双写模式,系统能够在保持数据一致性的同时,提高读取请求的效率和响应速度。然而,这种模式也增加了系统复杂性,因为需要确保数据库和缓存之间的数据一致性,并处理双写可能带来的同步和并发问题。
更新缓存还是删除缓存?
答案是:删除缓存
原因如下:
- 在实际业务中,缓存的数据可能不是直接来自数据库表,也许来自多张底层数据表的聚合。比如上面提到的商品详情信息,在底层可能会关联商品表、价格表、库存表等,如果更新了一个价格字段,那么就要更新整个数据库,还要关联的去查询和汇总各个周边业务系统的数据,这个操作会非常耗时。
- 从另外一个角度,不是所有的缓存数据都是频繁访问的,更新后的缓存可能会长时间不被访问(利用大量计算和时间去做更新,最后却没有被访问,不一定有价值),所以说,从计算资源和整体性能的考虑,更新的时候删除缓存,等到下次查询命中再填充缓存,是一个更好的方案。
- 系统设计中有一个思想叫 Lazy Loading,适用于那些加载代价大的操作,删除缓存而不是更新缓存,就是懒加载思想的一个应用。
参考:40 经典问题:先更新数据库,还是先更新缓存?
缓存更新(删除)策略
情况1:先删除缓存,后更新数据库 —— 更大概率出现数据不一致
举个例子:
线程1 刚刚 删除了 k = 1的缓存,并且打算去数据库更新k 1 -> 2(注意:数据库更新是一个慢操作);在这个时候,线程2 发现 缓存中 k 不存在,那么线程2 就去数据库中查k,由于线程1更新操作很慢,因此在线程2查询k的时候 线程1并未完成对 k = 2的更新,此时线程2就读到了旧数据k=1,并且线程1还要去更新缓存k = 1(这个过程很快)。过了一会儿,线程1才完成数据库更新k=2,然而此时缓存中k=1,所以数据不一致发生了。
结合下面的图来看(蓝色的条表示操作需要的时间,虚线代表此时DB和Cache的情况)
可以发现,在更新DB k = 2的时候,存在很长时间的空窗让其他线程读到旧的数据并且更新旧的数据到缓存,因此这种情况,有很大概率出现数据不一致。
情况2 : 先更新数据库,再删除缓存——减少不一致的发生时间
情况A:读缓在更新完数据库且删缓存之前读到的
线程1查不到缓存,就查数据库k = 2,并且将缓存写入 k = 2,但是此时线程2更新数据库 k = 1,并且删除缓存(还没完成),而假设此时有第三个线程3是在线程2更新数据库完成但删除缓存未完成读到了缓存 k = 2,此时数据就不一致了。但是,由于缓存删除其实很快,这种情况很少。
情况B:写缓存是在删缓存之后造成的不一致
线程1查缓存不存在,所以去查DB 得到 k =1 ,然后打算写缓存 k = 1(但是还没完成),此时,线程2更新完数据库 k = 2,打算删除缓存,然而此时线程1完成写缓存k = 1,此时数据不一致了。
解决方案 —— 延迟双删策略
针对这种情况,采用延迟双删策略
刚才的情况是因为写缓存是在删缓存之后造成的不一致,所以我们可以在删缓存之后设置一个定时任务或者延时队列MQ,比如在发送删除缓存命令延迟1秒(由业务决定)之后才真正执行,让写缓存先完成。
异步串行化:解决“先删缓存,后更新DB”情况下的数据不一致问题。
参考:高并发场景下的缓存 + 数据库双写不一致问题分析与解决方案设计
刚才的情况是,还没更新数据库的就查数据库读到旧数据吗?不就是因为读在更新前面了吗?那我就让你排队执行呗。
我们考虑在系统内部维护n个内存队列,更新数据的时候,根据数据的唯一标识,将该操作路由之后,发送到其中一个jvm内部的内存队列中(对同一数据的请求发送到同一个队列)。读取数据的时候,如果发现数据不在缓存中,并且此时队列里有更新库存的操作,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也将发送到同一个jvm内部的内存队列中。然后每个队列对应一个工作线程,每个工作线程串行地拿到对应的操作,然后一条一条的执行。
这样的话,一个数据变更的操作,先执行删除缓存,然后再去更新数据库,但是还没完成更新的时候,如果此时一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,排在刚才更新库的操作之后,然后同步等待缓存更新完成,再读库。
参考:干货 | 携程最终一致和强一致性缓存实践