深浅主题
缓存和数据库一致性
问题现象
数据库更新成功但页面仍读到旧值;删除缓存后旧值又被并发请求写回。
适用环境
Redis、关系型数据库、cache-aside;允许的陈旧时间需先定义。
本文是可复用的排障示例,不代表某次个人生产事故。命令中的名称、账号和数据均为示例,执行前请替换为已确认的目标。
复现或诊断步骤
- 记录同一业务 ID 的数据库提交、缓存删除、回源和缓存写入时间。
- 检查缓存删除是否在提交之前,删除失败是否有持久化重试。
- 构造慢读与快写交错:读拿到旧值,写提交并删除,随后慢读写入旧值。
原因分析
数据库和 Redis 之间没有天然的同一事务。提交后删除比提交前删除更合理,但仍有并发回填旧值的窗口;TTL 只能限制陈旧时长,不能提供严格一致性。
修复示例
text
数据库事务:
更新业务行并递增 version
写入 outbox 失效事件(包含业务 ID 与 version)
一起提交
工作器:
按事件删除或更新缓存
失败按退避策略重试,成功标记完成
读取:
缓存未命中才回源
若采用版本缓存,必须原子比较版本并写入
严格实时读走数据库或专门的一致性路径选择与业务匹配的一致性:普通列表可接受短时陈旧并配置 TTL、失效重试;库存扣减、余额等关键决策由数据库原子约束完成。需要防旧值回填时可使用有版本的缓存写入协议,并处理 key 缺失后版本信息丢失的问题,不能只加一个 version 字段就宣称解决。
验证方法
测试更新后立即读、并发慢读回填、缓存删除失败、工作器重启和重复事件。用数据库版本与缓存版本对比陈旧时长,明确可接受边界。
故障演练应使用隔离的测试 key 前缀与测试记录。修复上线后先观察有限范围的业务 ID,确认 outbox 积压能够收敛,再扩大范围;回滚应用时保留未处理事件,避免把尚未完成的失效请求误标记为成功。
常见误区
延迟双删没有覆盖所有执行时序。Redis Pub/Sub 通知不是可靠消息存储,断线可能丢事件;分布式锁也不能代替数据库的业务约束。