SpringBoot防重复提交-加一把Redis锁就够吗

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("接口正在处理中,请勿重复提交");

这条源码链至少包含 ApplicationAutoConfigurationApiRepeatSubmitHandlerAspectInfoRedisLockRedisLockerResp 六个可以核验的对象。它证明当前设计选择是: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

相关推荐
卷无止境18 分钟前
FastAPI 的 CI/CD 之路,从代码提交到线上运行
后端·python·fastapi
计算机毕设定制辅导-无忧学长22 分钟前
《基于SpringBoot的图书管理系统设计与实现》
java·spring boot·后端
小蒜学长23 分钟前
借助于大模型工具Cursor的中药材交易系统的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端
狂炫冰美式40 分钟前
电脑合盖之后 Cursor 还在偷我电?看看为啥
前端·人工智能·后端
程序员三明治41 分钟前
【体验毛坯房】Deep Harness 入门教程
java·人工智能·后端·大模型·llm·deepseek·dsh
newerp1 小时前
Golang 切片扩容策略
后端·程序员·go
k4m7v2pz1 小时前
用 rust-verb-shell 重写进程管理:从 game.sh 到 .rvs 的迁移实录
开发语言·后端·rust
我的div丢了肿么办1 小时前
go中make声明切片, 修改切片,append给切片扩容,合并切片,复制切片
后端·go
xcl09251 小时前
分销返利商城系统开发实战:返利算法与架构设计指南
java·spring boot