单纯靠前端防抖不能防止重复订单,必须前后端配合:前端立即禁用按钮并显示loading状态,后端用Redis setIfAbsent基于业务上下文(如用户ID+商品ID)加唯一令牌锁,过期时间略大于接口平均耗时,且key需通过SpEL动态生成,避免硬编码导致锁失效。

单纯靠前端防抖不能防止重复订单,必须前后端配合——前端限制用户操作节奏,后端用唯一令牌校验请求合法性。
前端按钮禁用 + 状态反馈要立刻生效
用户点击瞬间就要锁住按钮,不是等接口返回才处理。否则网络卡顿时,用户会反复点击,产生多个并发请求。
- 用
disabled属性禁用按钮,同时配合loading状态显示(如 Element UI 的:loading或 Ant Design 的loading属性) - 不要只依赖
setTimeout恢复按钮,得在请求完成(无论 success 还是 error)后手动重置状态 - Vue 场景下避免直接操作 DOM,优先用响应式数据控制
disabled和文字,比如:disabled="submitting" - React 场景下注意函数组件中闭包陷阱:
submitting状态更新后,事件回调里可能仍读到旧值,建议用useRef存储最新状态或用useCallback重新绑定
后端必须用 Redis setIfAbsent 做令牌校验
setIfAbsent 是原子操作,能真正防止并发写入。光靠数据库唯一索引或事务隔离级别扛不住高并发下的“时间差”问题。
- key 要包含业务上下文,比如
"order:submit:uid_123:ts_1744800300",不能只用用户 ID 或固定字符串 - 过期时间(
lockTime)建议设为略大于接口平均耗时(如 5–8 秒),太短容易误放行,太长影响用户体验 - 别在
finally里主动删 key——万一服务挂了,key 过期自动清理更安全;主动删反而可能删掉别人刚抢到的锁 - 异常抛出要用业务异常(如自定义
DuplicateSubmitException),别直接 throw new RuntimeException,否则全局异常处理器可能吞掉关键信息
@PreventDuplicateSubmit 注解的 key 表达式别硬编码
注解里的 key 参数支持 SpEL,这是动态构造防重 key 的关键,写死就失去灵活性。
- 常见写法:
@PreventDuplicateSubmit(key = "#request.getHeader('X-Request-ID')"),前提是前端带了该 header - 更稳妥的是结合参数:
@PreventDuplicateSubmit(key = "#order.userId + ':' + #order.productId"),但要注意空值导致 key 重复 - 如果参数是对象,SpEL 里取不到 getter 方法返回值(如
#order.getOrderId()不生效),得确保字段是 public 或用toString()等可访问方式 - Spring EL 解析失败时,
buildLockKey方法会拿到空字符串,最终 key 变成类似"duplicate:submit:",所有请求都撞同一个锁——这是线上最隐蔽的坑之一
真正难的不是写对一行 setIfAbsent,而是让前后端的“防抖窗口”对齐:前端禁用时长、后端锁过期时间、网关超时、客户端重试机制,四者稍有错位,就会漏防或误拦。上线前务必用 JMeter 或 wrk 模拟连击,观察日志里是否真只进一次切面、数据库是否只插入一条记录。

















