双写一致性

Cache-Aside

Cache-Aside 意为旁路缓存模式,是应用最为广泛的一种缓存策略。下面的图示展示了它的读写流程,来看看它是如何保证最终一致性的。在读请求中,首先请求缓存,若缓存命中( cache hit ),则直接返回缓存中的数据;若缓存未命中( cache miss ),则查询数据库并将查询结果更新至缓存,然后返回查询出的数据( demand-filled look-aside )。在写请求中,先更新数据库,再删除缓存(write-invalidate)。

为什么是删除缓存而不是更新缓存?

性能考虑:

  1. 当该缓存对应的结果需要消耗大量的计算过程才能得到时,比如需要访问多张数据库表并联合计算,那么在写操作中更新缓存的动作将会是一笔不小的开销,这会导致写操作变得很慢。

  2. 当写操作较多时,可能也会存在刚更新的缓存还没有被读取到,又再次被更新的情况(这常被称为缓存扰动),显然,这样的更新是白白消耗机器性能的,会导致缓存利用率不高。而等到读请求未命中缓存时再去更新,也符合懒加载的思路,需要时再进行计算。

  3. 删除缓存的操作不仅是幂等的,可以在发生异常时重试,而且写-删除和读-更新在语义上更加对称。

安全考虑:

  1. 在并发场景下,在写请求中更新缓存可能会引发数据的不一致问题。

  2. 若存在两个来自不同线程的写请求,首先来自线程 1 的写请求更新了数据库,接着来自线程 2 的写请求再次更新了数据库,但由于网络延迟等原因,线程 1 可能会晚于线程 2 更新缓存,那么这样便会导致最终写入数据库的结果是来自线程 2 的新值,写入缓存的结果是来自线程 1 的旧值,即缓存落后于数据库,此时再有读请求命中缓存,读取到的便是旧值。

如果选择先删除缓存,再更新数据库,那如何解决一致性问题呢?

为了避免“先删除缓存,再更新数据库”这一方案在读写并发时可能带来的缓存脏数据,业界又提出了延时双删的策略,即在更新数据库之后,延迟一段时间再次删除缓存,为了保证第二次删除缓存的时间点在读请求更新缓存之后,这个延迟时间的经验值通常应稍大于业务中读请求的耗时。延迟的实现可以在代码中 sleep 或采用延迟队列。显而易见的是,无论这个值如何预估,都很难和读请求的完成时间点准确衔接,这也是延时双删被诟病的主要原因。

最后更新于