我是怎么设计一个防重复提交机制的(库存出库场景)
我是怎么设计一个防重复提交机制的(库存出库场景)
一、背景
我在做一个库存系统的时候,遇到了一个很头疼的问题:用户提交出库单时,经常因为网络延迟、页面卡顿、或者手速太快,导致同一笔请求被重复提交。
这直接带来的后果就是:
- 同一批商品被多次出库
- Redis 与数据库库存不一致
- 系统出现超卖、数据异常等问题
这些可不是小问题,尤其是库存扣错了,后续很难对账、补救成本也高。
所以,我决定设计一个通用、有效、可扩展的防重复提交机制,来解决这个问题。
二、我是怎么做的?
我最终采用的是 Token + Redis 的方式,实现了一个简单、安全、高效的幂等性控制方案。
整个流程是这样的:
- 前端点击“出库”按钮前,先调用接口获取一个 Token
- 后端生成唯一 Token,并写入 Redis,设置有效期
- 前端带上 Token 提交出库请求
- 后端校验 Token 是否存在,存在则执行业务逻辑并删除 Token
- Token 用完即失效,防止重复提交
1. Token 生成规则
我使用 UUID 生成唯一 Token:
String token = UUID.randomUUID().toString().replace("-", "");
UUID 生成的 Token 基本可以保证全局唯一,长度适中,适合用作请求标识。
2. Redis Key 设计
使用 token:${token}
作为 Redis Key,例如:
SET token:abc123xyz 1 EX 60 NX
EX 60
:设置过期时间为 60 秒,防止 Token 长时间堆积NX
:仅当 Key 不存在时才设置成功,防止并发重复设置
这样设计的好处是:
- 每个 Token 只能使用一次
- 自动过期,无需手动清理
- 可扩展性强,可结合用户 ID、业务类型进一步隔离
3. 获取 Token 接口
@RestController
@RequestMapping("/stock")
public class TokenController {@Autowiredprivate RedisTemplate<String, String> redisTemplate;@GetMapping("/token")public Map<String, String> getToken() {String token = UUID.randomUUID().toString().replace("-", "");String redisKey = "token:" + token;redisTemplate.opsForValue().set(redisKey, "1", 60, TimeUnit.SECONDS);return Collections.singletonMap("token", token);}
}
前端调用这个接口获取 Token,然后带着 Token 去提交出库请求。
4. 出库接口(带 Token 校验)
@PostMapping("/out")
public ResponseEntity<String> batchOutStock(@RequestParam String token,@RequestBody List<OutStockItem> items) {String redisKey = "token:" + token;// 1. 校验 Token 是否存在Boolean exists = redisTemplate.hasKey(redisKey);if (exists == null || !exists) {return ResponseEntity.status(HttpStatus.FORBIDDEN).body("请勿重复提交");}// 2. 删除 Token(防止重复提交)redisTemplate.delete(redisKey);// 3. 执行出库逻辑stockService.batchDeductStock(items);return ResponseEntity.ok("出库成功");
}
这个接口的逻辑很清晰:
- Token 不存在,说明已经提交过,直接拒绝
- Token 存在,说明是第一次提交,执行业务逻辑后删除 Token
- Token 用完即失效,防止复用
5. 前端配合
前端在点击“出库”按钮前,先调用接口获取 Token:
axios.get('/stock/token').then(res => {const token = res.data.token;axios.post('/stock/out', items, {params: { token }}).then(response => {console.log('出库成功');}).catch(err => {console.error('出库失败,请重试');});
});
- 每次提交使用新的 Token
- Token 用完即失效,防止复用
- 若请求失败,前端提示用户重新提交
三、遇到的难点与优化点
1. Token 被恶意刷取怎么办?
- 每个 Token 只能使用一次
- Token 用完即失效
- 可结合用户 ID、业务类型进行 Token 隔离,防止刷 Token
2. Token 删除后业务执行失败怎么办?
- 如果 Token 删除了,但出库失败,前端提示用户“提交失败,请重新提交”
- 不建议自动恢复 Token,避免引入幂等性问题
- 可记录请求状态日志,供后续查询或补偿使用
3. 如何防止 Token 被滥用?
- 每次请求生成新的 Token,旧 Token 无法复用
- Redis 自动过期,避免 Token 堆积
- 后端限制单位时间内获取 Token 的频率(可选)
四、实现亮点
亮点 | 说明 |
---|---|
简单易用 | 前后端配合简单,易于实现 |
安全可靠 | Token 用完即失效,防止复用 |
高并发支持 | Redis 快速判断是否存在,性能好 |
可扩展性强 | 可结合用户ID、业务类型进一步优化 |
五、实际效果如何?
这套方案在我们系统中上线后,重复提交导致的超卖问题基本消失,日志里也不再出现“库存不一致”的异常。
- 接口幂等性得到保障
- 用户体验提升,前端提示更清晰
- 后端逻辑稳定,无并发问题
六、总结
通过基于 Token 的防重复提交机制,我实现了一个通用、安全、高效的幂等性控制方案,适用于库存出库、订单提交、支付操作等关键业务场景。
如果你也在开发类似的库存系统、订单系统或支付系统,不妨尝试这套方案。希望这篇文章能对你有所帮助!