Redis 和 MySQL 缓存一致性,延迟双删为什么不是强一致

Redis 和 MySQL 缓存一致性,延迟双删为什么不是强一致

Redis 放在 MySQL 前面,读请求是快了,但"数据库更新成功,缓存是不是也一定是新值"并没有答案。

我更愿意把缓存当成数据库的副本,而不是第二个事实源。这样遇到故障时,先问三个问题:旧值最多能存在多久,删除失败怎么补,哪些判断根本不能读缓存。

先看最容易漏掉的竞态

假设缓存里有 V1,现在写请求 B 把数据库改成 V2。

常见的 Cache-Aside 写法是:

text 复制代码
1. 提交 MySQL 事务
2. 删除 Redis key
3. 下一次读请求从 MySQL 回源,再写入 Redis

问题在第 2 步和第 3 步之间。读请求 A 可能已经拿到了旧数据,或者刚好从一个还没追上的只读副本读到了 V1。A 随后把 V1 写回 Redis,最后就变成:

text 复制代码
MySQL = V2
Redis  = V1

这不是"Redis 删除命令偶尔慢"这么简单,而是读写并发下的顺序没有被约束。

延迟双删到底解决哪一段

有人会这样写:

java 复制代码
updateDatabase();
deleteCache();
sleep(500);
deleteCache();

第二次删除确实能覆盖一部分场景:读请求先拿到旧值,第一次删除后回填,第二次删除再把它清掉。

但这个方案有几个前提,少一个都不能把它叫成"强一致":

  • 延迟必须大于一次慢读、序列化和回填的总耗时。固定写死 500 毫秒只是猜测,线上 P99 变了就会失效。
  • 第二次删除本身可能失败。失败只打日志,缓存不会因为你"计划删过"就自动消失。
  • 回源如果走延迟副本,副本还没追上时读到的仍然是 V1。这时再删一次,也只是把错误窗口往后推。
  • sleep 会占住业务线程,流量一高,缓存问题还没解决,线程池先被拖慢了。

所以延迟双删更像是给历史系统加的一层概率缓解,适用于允许短时间旧读的场景。库存、余额、权限判定这类数据,不能靠它做最终判断。

更稳的落地顺序

我一般按下面的边界拆:

1. 数据库事务提交后再删缓存

不要在事务还没提交时就删缓存。事务回滚了,缓存反而先没了;这虽然最终可能回源修复,但会制造不必要的抖动。

写路径可以先保持简单:

text 复制代码
提交 MySQL
  ↓
删除 Redis
  ↓
删除失败进入持久重试

重试记录要有业务 key、版本或事件 id,不能只靠内存里的一个异步任务。进程重启、队列积压、Redis 短暂不可用时,都要能继续处理。

2. 读请求要认识"副本不新鲜"

如果数据库有只读副本,缓存 miss 后直接读副本,再回填缓存,旧值写回的窗口会更大。

可以选一种明确的策略:

  • 写入后的短窗口内,回源读主库。
  • 带上最低版本号,副本没追上就改读主库。
  • 复制延迟超过阈值时,暂停这类数据的缓存回填。

这里的重点不是把所有请求都打到主库,而是别把"副本可读"误当成"副本最新"。

3. 写入口多时,用事件做收敛

如果更新不只来自一个服务,应用层删缓存很容易漏。MySQL 的 binlog 能按提交顺序记录事务变更,可以用 CDC 或消息链路消费它,再做缓存失效。

这条链路也不是魔法,至少要处理:

  • 消息重复:同一个事件重放不能把数据搞坏。
  • 消息乱序:旧事件不能覆盖新版本。
  • 消费失败:要有重试、死信和对账。
  • 延迟监控:看"数据库提交时间到缓存失效时间"的 P95/P99,而不是只看消费成功数。

缓存一致性不是一个"选哪种方案"的单选题。真正要定的是:数据库是事实源,缓存允许多旧,失效事件多久必须收敛,以及哪些请求永远不能用缓存结果做最后决定。

面试时我会这样回答

如果只剩一分钟,我会先说:

Redis 是 MySQL 的副本,不是共同事务的一部分。提交数据库后删除缓存是常见的 Cache-Aside 写法,但删除失败、慢读回填、只读副本延迟都会造成短暂不一致。延迟双删只能缩短部分竞态窗口,不能保证强一致。重要数据要用主库或一致读,删除失败要持久重试,多写入口可以用 binlog CDC 做最终收敛。

面试官继续追问"那到底多久删第二次",我不会报一个看起来很精确的数字,而是先问业务的读 P99、复制延迟和允许的旧读时长。这个数字应该从监控里量出来,不是从博客里抄出来。

相关推荐
三84412 小时前
redis防御加固与安全基线
数据库·redis·安全·应急响应·防御
SendTomo12 小时前
【技术交流】从0到1构建一个汉字学习平台:汉字吧(hanzi8.com)的技术架构猜想
redis·mysql·nginx·php·个人开发
IT大白鼠13 小时前
MySQL 分布式集群系列 · 第四篇——实操部署指南:从零搭建生产级 MySQL NDB 集群
数据库·分布式·mysql
IT大白鼠13 小时前
MySQL 分布式集群系列 · 第三篇——核心原理精讲:NDB 自动分片、多主写入与数据同步
数据库·分布式·mysql
初願致夕霞15 小时前
MySQL_索引
数据库·mysql
一只小李郁vickie16 小时前
mysql 开启压缩传输3402条数据2.1秒压缩到毫秒级
数据库·mysql
右耳朵猫AI17 小时前
Go周刊2026W36 | Go 1.27.1 发布、TinyGo 0.42、HTTP/2 原生迁入 net/http、quic-go 0.62
redis·http·golang
前端兰博17 小时前
04-数据库-MySQL
后端·mysql
这个DBA有点耶18 小时前
COUNT慢不是因为用了*,是这5个原因——1000万行数据实测+执行计划深度解析
数据库·mysql·算法
kyrie_sakura18 小时前
MySQL数据库学习笔记2--系统函数(分组,单行,窗口函数)
数据库·学习·mysql