Spring Boot 4.0 缓存一致性:双写、失效与延迟删除
前言
数据库更新成功、缓存删除失败,下一次查询仍然会读到旧订单。更隐蔽的情况是:缓存删除成功后,一个先读到旧数据库值的线程又把旧值回填。
这篇教程在上一篇 Redis 缓存的基础上,建立“数据写成功后删除缓存”的流程,并说明延迟双删、版本号和 TTL 各自的边界。
配套源码:dvsusan/spring-boot4-demo。
最终结果

图 1:数据提交后让缓存失效,下一次读取按最新数据重建。
为什么更推荐删除缓存?
双写一致性常见四种顺序:
| 顺序 | 主要风险 |
|---|---|
| 先写缓存再写数据库 | 缓存成功、数据库失败时出现脏数据 |
| 先写数据库再写缓存 | 并发写可能让后提交的旧值覆盖 |
| 先删缓存再写数据库 | 删除后读者可能先回填旧值 |
| 先写数据库再删缓存 | 仍存在旧值回填窗口,但通常更易控制 |
多数读多写少业务采用“先写数据库,提交后删除缓存”,再配合 TTL、版本和失败重试。
第 1 步:把删除放在数据写之后
项目中的示例:
/**
* 演示“更新数据后删除缓存”的最小一致性流程。
*/
public class CacheConsistencyService {
private final OrderService orderService;
private final OrderCacheService cacheService;
public OrderView create(CreateOrderCommand command) {
OrderView order = orderService.create(command);
// 删除旧缓存,不能覆盖并发读者可能写入的更新值。
cacheService.evict(order.orderNo());
return order;
}
}当前订单服务使用内存存储,重点是观察顺序。接入数据库后,删除动作应在事务提交后执行,避免事务最终回滚但缓存已经被删。
第 2 步:启动 Redis 并验证
docker compose up -d redis
mvn clean package -DskipTests
java -jar demo-web/target/demo-web-1.0.0-SNAPSHOT.jar \
--server.port=18092 \
--demo.chapter4.redis.enabled=true写入一个订单:
curl -X POST "http://localhost:18092/api/v1/chapter4/cache/orders" \
-H "Content-Type: application/json" \
-d '{"customerId":"C1002","amount":88.00}'把订单号保存下来。然后主动读取一次,让缓存回填:
curl "http://localhost:18092/api/v1/chapter4/cache/orders/SO..."
docker compose exec redis redis-cli GET "chapter4:order:SO..."再次通过一致性接口创建订单,只会删除新订单自身对应的旧缓存。已有订单的更新场景需要在业务更新接口中复用同样的失效逻辑。
第 3 步:理解旧值回填
一种危险时序:
T1 查询发现缓存未命中
T2 从数据库读到旧值
T3 写请求更新数据库并删除缓存
T4 T1 把旧值写回缓存T1 的回填晚于删除,旧值重新出现。常见处理:
- 缓存值带数据库版本号,读取时拒绝旧版本。
- 删除后延迟一小段时间再次删除。
- 写请求设置短期“禁止回填”标记。
- 用 TTL 限制最坏影响时间。
延迟双删不是万能方案。延迟太长会增加旧值窗口,延迟太短可能挡住第二次并发。时间必须根据业务读耗时和写入模型测量。
第 4 步:删除失败怎么办?
缓存删除失败不能只写一行日志。可把删除任务放入本地消息表或 Outbox,由重试任务继续执行;同时保留 TTL 作为最后兜底。
对于订单、库存等强一致数据,不要只依赖缓存一致性技巧。关键判断应以数据库或带版本条件的更新为准。
常见问题排查
| 现象 | 原因 | 处理方式 |
|---|---|---|
| 更新后一直读旧值 | 更新路径漏删缓存 | 统一封装缓存失效 |
| 偶发旧值又出现 | 并发回填覆盖 | 版本号、延迟删除或禁填 |
| 删除成功但事务回滚 | 删除早于提交 | 使用事务提交后回调 |
| 删除失败无人处理 | 没有重试记录 | Outbox/消息表 + TTL |
| 延迟双删效果不稳定 | 延迟拍脑袋设置 | 测量读耗时和写并发 |
练习题
在缓存 JSON 中增加 version,读取时发现缓存版本低于数据库版本就拒绝返回。
总结
“先写数据、提交后删缓存”能降低很多常见问题,但无法消除所有并发窗口。TTL、版本、重试和监控要一起使用。
下一篇解决同一订单被并发操作的问题:用 Redisson 分布式锁建立互斥区。
上一节:Redis 缓存设计
下一节:Redisson 分布式锁
13 个企业级实战项目 · 源码 + 教程 + 答疑
简历包装指导AI Agent 实战高并发·微服务Offer 导向持续更新3000+ 同行交流1V1 答疑3 天包退
覆盖高并发、微服务、分布式与 AI Agent:100万QPS短链、企业智能知识库、商城微服务、SaaS点餐、秒杀、刷题小程序、代码生成器、AI Agent 等。券后 ¥129(原价 ¥159),不满意 3 天包退。
