Spring Boot 防重复提交:锁、请求指纹与业务幂等的三层闭环
问题现场: Redis 锁只能约束同一时刻的并发,无法阻止锁释放后的重试,也无法证明第一次业务是否已经成功。
用户连点、HTTP 超时重试、第三方回调重放和消息重复消费都表现为"请求来了两次",但需要的去重期限和结果语义完全不同。本文不再重复文章 052 已讲过的 Lua 解锁、TTL 与续期,而是讨论请求指纹、入口短锁和业务幂等怎样形成闭环。
一、先区分四种重复
| 来源 | 时间窗口 | 推荐标识 | 第二次请求的处理 |
|---|---|---|---|
| 页面连点 | 毫秒到数秒 | 用户+接口+资源 | 快速拒绝 |
| HTTP 重试 | 秒到分钟 | 客户端幂等号 | 返回第一次结果 |
| 第三方回调 | 小时到天 | 外部流水号 | 只允许一次状态迁移 |
| 消息重投 | 取决于保留期 | messageId+业务主键 | 安全重放 |
不先定义来源,所谓防重最后往往只是给 URL 加五秒锁:它能挡住双击,却挡不住十秒后的网络重试。
二、第一层是稳定请求指纹
text
业务动作 + 租户或用户范围 + 资源标识 + 客户端幂等号
没有幂等号时,可以对规范化后的关键参数计算摘要,但不能直接对原始 JSON 求哈希。字段顺序、时间戳、TraceId 和非业务字段都会造成同一业务生成不同 key。订单号、支付流水号等业务标识通常更稳定,也更便于排查。
三、第二层短锁只收敛并发窗口
短锁只能保证同一指纹在有效期内由一个执行者进入。它不能回答锁释放后能否重试、响应丢失后返回什么,以及业务耗时超过 TTL 后如何避免第二次写入。
锁 key、TTL 和续期是并发控制参数,不是业务幂等协议;锁所有者校验和原子释放统一由文章 052 说明,这里不再重复。
四、MetaLite 当前解决入口互斥
ApiRepeatSubmitHandler 位于 API_RECEIVE 入口处理链。它读取 @RedisLock,通过 SpEL 从已解析的方法参数生成 key,成功获取锁后才允许业务继续执行。
统一处理链保证解密和参数接收完成后再计算 key,也避免业务方法散落加锁与解锁代码。后置阶段使用 AspectInfo 保存的 key 和唯一值释放锁。
源码注册链也明确表达了"可选 Redis"的边界:
java
@Bean
public ApiRepeatSubmitHandler apiRepeatSubmitHandler(
@Autowired(required = false) RedisLocker redisLocker) {
return new ApiRepeatSubmitHandler(redisLocker);
}
处理器的关键分支可以压缩为:
java
if (redisLocker == null) {
return Resp.ok();
}
RedisLock lock = (RedisLock) aspectInfo.findMethodAnnotation(RedisLock.class);
if (lock == null) {
return Resp.ok();
}
return redisLocker.lock(aspectInfo)
? Resp.ok()
: Resp.error("接口正在处理中,请勿重复提交");
这条源码链至少包含 ApplicationAutoConfiguration、ApiRepeatSubmitHandler、AspectInfo、RedisLock、RedisLocker 和 Resp 六个可以核验的对象。它证明当前设计选择是:Redis 或注解缺失时放行,抢锁失败时阻断,方法结束后由 postHandle 解锁。这个降级边界必须进入部署检查,否则开发环境关闭 Redis 时很容易误判防重已经生效。
RedisLocker 是可选能力,所以代码存在注解不等于生产环境一定启用了防重。MetaLite 当前提供的是入口防线,不能替业务完成长期幂等。
五、第三层必须记录业务结果
真正的幂等是"请求到达多少次,最终只产生一次业务效果"。通常需要:
- 业务表唯一索引;
- 幂等请求表记录处理中、成功、失败和结果未知;
- 状态机限制非法重复迁移;
- 保存第一次成功响应供重试复用;
- 消费端记录消息与业务处理结果。
支付回调可以用第三方流水号建立唯一约束。第二次插入冲突时,应读取第一次结果并返回约定响应,而不是把数据库异常直接暴露给上游。
六、失败不一定意味着可以立即重试
失败可能表示什么都没做、下游成功但本地失败,或者结果未知。短锁释放只说明线程退出临界区,不代表业务回到执行前。
对于后两种状态,重试可能再次扣款、发券或创建外部订单。幂等记录必须区分状态,并提供查询、超时接管或补偿机制。
七、用时间线验证闭环
text
00s A 获得五秒短锁并调用下游
05s 锁过期
06s B 使用相同幂等号再次进入
08s A 写入成功
09s B 尝试写入同一业务结果
完整闭环应保证:指纹识别 A、B 是同一请求;短锁尽量避免并发;唯一约束或状态机保证只写入一次;B 最终复用 A 的结果。
集成测试应主动让业务耗时超过锁 TTL。只有这种测试才能证明系统防住的是业务重复,而不只是正常速度下的双击。
八、上线前检查
- key 是否包含稳定业务标识?
- 去重期限是五秒、一天还是永久?
- 第一次响应丢失时能否返回原结果?
- 锁过期后是否还有唯一约束或状态机?
- 是否区分失败、处理中和结果未知?
- Redis 未启用时是放行还是拒绝?
- 是否测试过业务耗时超过 TTL?
把这些问题回答清楚,防重复提交才从一个 Redis 注解升级为可验证的业务幂等协议。
框架简介
MetaLite 是面向企业生产环境的新一代 Java 微服务技术底座。系列文章重点分享代码背后的设计思路、技术取舍与工程实践。
源码基线
JDK 21、Spring Boot 3.2.9、Spring Cloud 2023.0.1、Spring Cloud Alibaba 2023.0.1.3,具体组件版本以项目 backend-bom 为准。
作者简介
15 年 Spring 体系企业级开发经验,专注于 Java 微服务架构、工程治理与生产实践。
持续更新
MetaLite 系列内容将持续更新,围绕核心设计、源码链路、技术取舍与生产实践展开。欢迎关注作者,及时获取后续内容。
在线演示
演示地址: https://admin.metalite.top/
演示账号: guess
演示密码: admin@2026