SpringBoot结合Redis实现防止重复提交订单的实战指南
作者:狼爷
一次点击,下单成功;两次点击,客服崩溃。
重复提交订单的问题,几乎每个后端都遇到过。今天咱们就从底层逻辑到实战代码,聊聊这个老生常谈却又暗藏玄机的话题。
一、背景:为什么会重复提交?
先说场景。
用户点了一次“立即支付”,发现没反应,再点一次,结果悲剧发生:
- 生成了两笔订单;
- 扣了两次库存;
- 财务对账炸了锅。
这还只是正常用户,有的还会:
- 前端按钮没禁用,点个爽;
- 支付网关超时自动重试;
于是,后端同学开始加锁、加幂等、加约束,最后一看,代码又胖了一圈。
二、常见的防重复提交方案
其实防重复提交的核心目标就一句话:
在一定时间内,确保同一个请求只被处理一次。
从架构层次看,可以分为:
| 层次 | 方案 | 优点 | 缺点 |
|---|---|---|---|
| 前端层 | 按钮防抖 / 禁用 | 简单直观 | 不可靠,易被跳过 |
| 网关层 | 幂等性Key | 通用 | 实现复杂 |
| 服务层 | Redis分布式锁 | 高性能 | 需要良好锁管理 |
| 数据库层 | 唯一索引 / 状态判断 | 最稳 | 执行慢,影响性能 |
实际生产中,一般会前后端双保险:
前端防抖 + 后端幂等控制。
三、服务端防重复提交的常见思路
来点干货。
后端层面常用的方案主要有三种。
方案一:幂等性 Token 机制(推荐)
核心逻辑:
- 前端先调用
/token接口,获取一个随机token; - 请求下单时携带该 token;
- 后端校验 token 是否已使用;
- 没用过 → 处理业务并标记“已使用”;用过 → 拒绝请求。
非常适合「表单类接口」或「下单支付类接口」。
伪流程
前端请求 token -> 缓存保存 token -> 请求接口时携带 token -> 校验通过一次后删除 token
方案二:Redis 分布式锁机制
使用 Redis 的 SETNX(或 Redisson)实现业务级互斥锁:
boolean lock = redis.setIfAbsent(key, "1", 5, TimeUnit.SECONDS);
if (!lock) {
throw new BusinessException("请勿重复提交");
}
try {
createOrder();
} finally {
redis.delete(key);
}
比如 key 可设计为:
lock:order:create:userId:123
优点:
- 性能高
- 实现简单
- 适合短时间的防重需求
缺点:
需要注意锁过期与释放问题。
方案三:数据库层幂等约束
老实人的方案——数据库唯一索引。
ALTER TABLE t_order ADD UNIQUE KEY uk_order_no (order_no);
或者逻辑判断:
if (orderRepository.existsByOrderNo(orderNo)) {
log.info("订单重复提交: {}", orderNo);
return;
}
优点:稳如老狗。
缺点:性能不高,适合作为兜底。
四、综合方案流程图
下面是一个比较推荐的方案:
幂等Token + Redis锁 双层防护

五、实战代码(Spring Boot)
1. 获取幂等 Token 接口
@RestController
@RequestMapping("/api/token")
public class TokenController {
@Autowired
private StringRedisTemplate redisTemplate;
@GetMapping("/generate")
public String generateToken() {
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("token:" + token, "1", 10, TimeUnit.MINUTES);
return token;
}
}
2. 下单接口(幂等 + Redis锁)
@RestController
@RequestMapping("/api/order")
public class OrderController {
@Autowired
private StringRedisTemplate redisTemplate;
@PostMapping("/create")
public String createOrder(@RequestHeader("Idempotency-Token") String token,
@RequestBody OrderRequest request) {
String key = "token:" + token;
// 校验token是否存在
Boolean exists = redisTemplate.hasKey(key);
if (Boolean.FALSE.equals(exists)) {
throw new BusinessException("请勿重复提交");
}
// 删除token,确保一次性
redisTemplate.delete(key);
// 加锁防并发
String lockKey = "lock:order:create:" + request.getUserId();
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 5, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(locked)) {
throw new BusinessException("请勿重复点击");
}
try {
// TODO: 创建订单逻辑
return "下单成功";
} finally {
redisTemplate.delete(lockKey);
}
}
}
六、最佳实践经验总结
| 项目 | 建议 |
|---|---|
| Token有效期 | 建议 5~10 分钟 |
| 锁粒度 | 以用户ID 或 业务单号为单位 |
| 幂等Key传递方式 | 推荐放在 Header 中,例如 Idempotency-Token |
| 数据库层兜底 | 唯一约束防止极端情况 |
| 日志 | 建议记录 Token 与锁状态,方便排查 |
一句话总结:
Redis防快点,数据库防慢点,前端防乱点。
七、总结
| 层级 | 手段 | 优点 | 备注 |
|---|---|---|---|
| 前端 | 按钮防抖 | 用户体验好 | 仅表层防护 |
| Redis | Token + Lock | 性能高 | 推荐主力方案 |
| 数据库 | 唯一约束 | 稳定 | 最终兜底 |
最终结论:
没有银弹,只有多层防御。
前端防抖 + 后端幂等 + 数据库约束 = 才是真正的生产级防重复方案。
写在最后
防重复提交这事,说简单也简单,说复杂也复杂。
它考验的不是写代码的能力,而是你对业务一致性和系统幂等性的理解。
下次再有人问你“为什么我点两次下单按钮扣了两次钱”,
你可以淡定地说一句——“兄弟,我们系统现在有幂等锁,不怕你点三次。”
到此这篇关于SpringBoot结合Redis实现防止重复提交订单的实战指南的文章就介绍到这了,更多相关SpringBoot防止重复提交订单内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
