Redis 缓存一致性:从“数据不一致”根源到解决方案全梳理

原文链接

前言

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

数据一致性的概念

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

常见缓存更新策略

Cache-Aside(缓存旁路)

以数据库中的数据为准,缓存中的数据是按需加载的。它可以分为读策略和写策略。

读策略

从缓存中读取数据;如果缓存命中,则直接返回数据;如果缓存不命中,则从数据库中查询数据;查询到数据后,将数据写入到缓存中,并且返回给用户。

写策略

更新数据库中的记录;删除缓存记录。

适用场景

  • 读多写少的场景(如商品详情页、用户信息查询);
  • 对缓存一致性要求不是极端严格,且希望降低实现复杂度的系统。

Read-Through(读穿透)

  1. 应用程序查询数据时,直接请求缓存;
  2. 若缓存命中,缓存直接返回数据;
  3. 若缓存未命中,缓存中间件主动查询数据库,将查询到的数据写入缓存后,再返回给应用程序。

适应场景

  • 希望简化应用程序逻辑的场景;
  • 已引入成熟缓存中间件(如 Redis 的部分高级封装、Memcached 的扩展工具)的系统。

Write-Through(写贯穿)

  1. 应用程序更新数据时,直接写入缓存;
  2. 缓存中间件收到数据后,立即同步将数据写入数据库;
  3. 数据库写入成功后,缓存返回 "写入成功" 给应用程序。

适用场景

  • 对数据一致性要求极高(如金融交易记录、订单状态更新);
  • 写操作频率不高,或可接受写入延迟的场景。

Write-Behind(写回 / 延迟写)策略

  1. 应用程序更新数据时,直接写入缓存,缓存立即返回 "写入成功";
  2. 缓存中间件在后台异步(如定时、或积累一定量数据后)将数据批量写入数据库;
  3. 若数据库写入失败,缓存需有重试机制或错误记录逻辑。

适用场景

  • 写操作频繁但对数据实时一致性要求较低的场景(如用户行为日志、非核心业务的统计数据);
  • 对写入性能要求极高,且能接受少量数据丢失风险的系统(如高并发的临时数据存储)。

TTL(Time-To-Live,过期时间)策略

  • 写入缓存时,为数据设置 TTL(如 10 分钟);
  • 缓存系统定期清理过期数据,或在查询时判断数据是否过期(过期则视为未命中);
  • 过期数据需通过 "重新查库 + 写入缓存" 更新。

适用场景

  • 作为 Cache-Aside 等策略的补充,降低 "缓存删除失败" 导致的长期不一致风险;
  • 数据变化频率可预测的场景(如每日更新的商品榜单,TTL 可设为 1 天)。

更新缓存

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

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

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

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

总结:需要保证缓存和数据库这两个操作的原子性

删除缓存

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

因为有些缓存需要经过大量的运算或者是慢查询得出来的结果,如果每次都计算好,如果这个缓存一直没被访问,然后下次再更新数据库,还得重新计算一遍缓存,效率较低。所以一般查询时写缓存,更新时删缓存。

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

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

延迟双删

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

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

为什么?

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

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

一般参考因素:

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

缺点

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

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

需要确保删除缓存必须成功,放到MQ 异步删除,失败了重试,或者监听数据库 binglog,删除缓存,失败同样重试,优点是不侵入业务代码。另外还有一种情况就是数据库如果配置主从同步,主库写数据,从库读数据,由于主从同步需要时间,所以在主库更新数据后,其他请求可能读取的也是旧的数据,具体解决方法只能根据业务做取舍,可以等待一段时间再去读从库,如果无法等待,就只能去读主库了。

不一致的根源

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

总结

缓存和数据库很难保证强一致性,一般通过给缓存加过期时间,就算在这期间存在短期不一致,我们依旧有失效时间来兜底,这样也能达到最终一致。

相关文章:

https://www.cnblogs.com/wzh2010/p/17205453.html

http://kaito-kidd.com/2021/09/08/how-to-keep-cache-and-consistency-of-db/

相关推荐
power-辰南6 个月前
亿级分布式系统架构演进实战(五)- 横向扩展(缓存策略设计)
spring cloud·高并发·分布式系统·缓存一致性·多级缓存策略·缓存问题解决方案
xushuanglu_csdn2 年前
高并发系统实战课个人总结(极客时间)
架构·高并发·测试·电商·缓存一致性·个人提升