前言

如果你开发了一个 Web 网站,前期业务逻辑比较简单,就是查数据库然后呈现到页面上,但是随着业务的发展,用户数量和 qps 越来越多,这时候你会发现网站访问越来越慢,于是你定位到是数据库负载太高,越来越多的查询落到数据库,里面不乏一些慢查询。这时你能想到的优化方法是加个索引,但是随着业务的不断发展,落到数据库的查询越来越多,除了给数据库实例加资源配置,还有其他方法吗?你想到了可以给一些慢查询和频繁访问的查询加上 Redis 缓存,这样大部分的数据库查询就会转到 Redis,减轻数据库的压力,而 Redis 的特点就是快,这样你的网站速度有了显著的提升。然而很快你又发现新的问题,如何保证缓存的数据和数据库数据的一致性?

数据一致性的概念

在缓存与数据库的协同场景中,缓存一致性是确保缓存数据与数据库数据保持一致的关键问题。当数据发生更新(修改、删除等操作)时,若处理不当,易出现缓存中数据与数据库数据不匹配的情况。

不一致的根源

  • 操作部分失败
  • 并发操作

操作部分失败

数据库和缓存都更新,又存在先后问题,那对应的方案就有 2 个:

  1. 先更新缓存,后更新数据库
  2. 先更新数据库,后更新缓存

因为操作分为两步,那么就很有可能存在「第一步成功、第二步失败」的情况发生,而且这是两个不同的中间件,很难做到原子性操作。

先更新缓存,后更新数据库

如果缓存更新成功了,但数据库更新失败,那么此时缓存中是最新值,但数据库中是「旧值」。虽然此时读请求可以命中缓存,拿到正确的值,但是,一旦缓存「失效」,就会从数据库中读取到「旧值」,重建缓存也是这个旧值。这时用户会发现自己之前修改的数据又「变回去」了,对业务造成影响。

先更新数据库,后更新缓存

如果数据库更新成功了,但缓存更新失败,那么此时数据库中是最新值,缓存中是「旧值」。之后的读请求读到的都是旧数据,只有当缓存「失效」后,才能从数据库中得到正确的值。这时用户会发现,自己刚刚修改了数据,但却看不到变更,一段时间过后,数据才变更过来,对业务也会有影响。

并发操作

假设我们采用「先更新数据库,再更新缓存」的方案,并且两步都可以「成功执行」的前提下,如果存在并发,情况会是怎样的呢?

有线程 A 和线程 B 两个线程,需要更新「同一条」数据,会发生这样的场景:

  1. 线程 A 更新数据库(X = 1)
  2. 线程 B 更新数据库(X = 2)
  3. 线程 B 更新缓存(X = 2)
  4. 线程 A 更新缓存(X = 1)

最终 X 的值在缓存中是 1,在数据库中是 2,发生不一致。同样地,采用「先更新缓存,再更新数据库」的方案,也会有类似问题。

删除缓存

除缓存对应的方案也有 2 种:

  1. 先删除缓存,后更新数据库
  2. 先更新数据库,后删除缓存

同样地,先来看「第二步」操作失败的情况。

先删除缓存,后更新数据库,第二步操作失败,数据库没有更新成功,那下次读缓存发现不存在,则从数据库中读取,并重建缓存,此时数据库和缓存依旧保持一致。

但如果是先更新数据库,后删除缓存,第二步操作失败,数据库是最新值,缓存中是旧值,发生不一致。所以,这个方案依旧存在问题。

策略

Cache-Aside(缓存旁路)

Read-Through(读穿透)

Write-Through(写贯穿)

解决方案

延迟双删策略

延迟双删的操作流程可概括为:“先删缓存→再更新数据库→延迟一段时间后,再删一次缓存”。具体步骤如下:

  1. 第一次删缓存:先删除缓存中的目标数据,使后续读取请求暂时无法从缓存获取数据(需去数据库查询)。
  2. 更新数据库:执行数据库中目标数据的更新操作(修改、新增、删除等)。
  3. 延迟一段时间:等待一个短时间(如几百毫秒、几秒,根据业务场景调整)。
  4. 第二次删缓存:再次删除缓存中的目标数据(若此时缓存中因并发请求已写入旧数据,第二次删除可将其清除)。

为什么?

  • 第一次删缓存:目的是 “清空旧缓存”,避免更新数据库前,有请求从缓存读取旧数据并依赖旧数据做操作。
  • 延迟一段时间的意义:等待 “在数据库更新期间可能进入的并发读请求” 完成。例如:第一次删缓存后,若有请求读取数据,会因缓存为空去查数据库(此时数据库未更新,读到旧数据),并可能将旧数据写入缓存;延迟一段时间后,数据库已完成更新,且该并发请求大概率已完成 “读旧数据→写缓存” 的操作,此时第二次删缓存可将刚写入的旧数据清除。
  • 第二次删缓存:兜底操作,确保即使并发请求在数据库更新期间写入了旧数据到缓存,也能被再次删除,后续请求读取时会从数据库获取新数据并写入正确的缓存。

延迟时间的长短直接影响策略效果,需根据业务场景(如数据库更新耗时、并发量、网络延迟等)灵活调整,核心原则是:“确保延迟时间足够覆盖‘数据库更新期间可能发生的并发读请求写入缓存的时间’”

一般参考因素:

  • 数据库更新的耗时:若数据库操作(如事务、复杂更新)耗时较长,需适当延长延迟。
  • 业务并发量:高并发场景下,并发读请求可能更密集,需预留更多时间让 “读旧数据→写缓存” 的操作完成。
  • 经验值:多数场景下,可先设置几百毫秒(如 300ms、500ms),再通过监控缓存命中率、数据一致性情况逐步优化。

缺点

  • 无法完全解决极端并发问题:若延迟时间设置过短,仍可能有并发请求在第二次删缓存后才写入旧数据;若设置过长,会增加缓存空窗期,导致数据库压力上升。
  • 增加系统复杂度:需额外处理 “延迟任务”(如通过线程池、消息队列实现延迟删除),若延迟任务失败(如第二次删缓存未执行),仍可能出现不一致。
  • 对写操作性能有影响:两次删缓存 + 延迟等待会增加写操作的耗时,不适合 “高并发写” 场景(如秒杀商品库存更新,可能因延迟导致写操作堆积)。

总结

缓存都是有「失效时间」的,就算在这期间存在短期不一致,我们依旧有失效时间来兜底,这样也能达到最终一致。