缓存失效时数据不一致的典型表现
在 MySQL + Redis 架构中,并发写入常导致「缓存未更新但数据库已变」,比如用户 A 更新订单状态为
paid,写库成功,删缓存也成功;但用户 B 几乎同时读缓存,拿到旧的
unpaid状态——这不是缓存没删,而是删完之后、新值还没写入缓存前的窗口期被读到了脏数据。
先删缓存再更新数据库?不行,有竞态
看似合理的顺序,在并发下反而更危险:两个请求都走「删缓存 → 更新 DB」,第二个请求的 DB 写入完成后,缓存仍是空的,后续读请求会回源查到第二个请求写入的新值,然后写入缓存——这本身没问题;但若此时第一个请求的缓存重建(比如它带了延迟双删逻辑)把旧值又刷进去了,就彻底错乱。
DEL cache:order:123(请求 A)
UPDATE orders SET status='paid' WHERE id=123(请求 A)
DEL cache:order:123(请求 B)
UPDATE orders SET status='shipped' WHERE id=123(请求 B)
SET cache:order:123 {status:'paid'}(请求 A 的延迟重建,覆盖了正确的 shipped)
推荐方案:更新数据库后删除缓存 + 设置过期时间兜底
核心是放弃「强一致」幻想,接受短暂不一致,用「最终一致 + 降低风险」组合拳。关键点不在删缓存时机多精巧,而在如何让错误窗口更小、更可测。
所有写操作统一走「先更新 MySQL,再DEL缓存」,不搞延迟双删 缓存必须设
EXPIRE,比如
SET cache:order:123 {...} EX 300(5 分钟),避免永久脏数据
读请求遇到缓存 miss,查库后写入缓存时,加一层「版本号 or 时间戳校验」:只当 DB 中的 updated_at比缓存里记录的更新,才允许写入(需业务表有该字段) 对一致性要求极高的场景(如支付单状态),读请求直接查库,绕过缓存——用开关控制,不是所有读都缓存
UPDATE orders SET status = 'paid', updated_at = NOW() WHERE id = 123 AND version = 5;
配合应用层检查返回影响行数是否为 1,失败则重试或告警。
监听 binlog 做缓存更新比应用层更可靠
应用代码里删缓存容易漏(比如新增一个 DAO 方法但忘了配缓存清理),而 MySQL 的 binlog 是唯一真实写入源。用
canal或
debezium订阅变更,收到
UPDATE orders事件后触发
DEL cache:order:${id},能消除应用层逻辑分散带来的不一致风险。
binlog 方案不依赖业务代码,改 SQL 就生效
注意事务边界:一个事务含多条语句时,binlog 解析要按事务粒度投递,避免中间状态被消费
消费端需幂等:同一条 binlog 可能重复投递,DEL本身是幂等的,但如果是
SET缓存就要加判断
真正难的不是选哪种方案,而是意识到「缓存永远比数据库慢半拍」,然后在业务可接受范围内,把这半拍控制在毫秒级、可监控、可降级。
