单纯用synchronized无法解决接口重复提交,因其仅限单JVM内生效,多节点部署下无效,且阻塞正常请求、降低吞吐量。

为什么单纯用 synchronized 无法解决接口重复提交
因为 synchronized 只作用于单个 JVM 实例内的线程,而生产环境通常是多节点部署(Nginx + 多台 Tomcat),用户两次请求可能打到不同机器上,synchronized 根本不生效。另外,它还会阻塞正常请求,影响吞吐量。
常见错误现象:java.lang.IllegalMonitorStateException(误用 wait/notify)、接口响应变慢、并发压测时重复数据仍入库。
- 不要在 Controller 方法上加
synchronized - 避免对整个 service 方法加锁,尤其涉及 DB 查询或远程调用时
- 锁粒度必须是「业务唯一标识」,比如订单号、用户+操作类型组合
Redis 分布式锁实现幂等性最简可行方案
核心是用 SET key value NX PX timeout 命令保证原子性,value 必须是唯一随机值(如 UUID),用于防止误删他人锁。
关键细节:
立即学习“Java免费学习笔记(深入)”;
- 锁 key 建议格式:
"idempotent:" + userId + ":" + orderId或"idempotent:" + requestDigest(对请求参数做 SHA256) - 超时时间不能设太短(低于业务执行时间),否则锁自动释放导致重复执行;也不宜过长(影响故障恢复)
- 必须用 Lua 脚本释放锁:
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 idempotent:1001:ORD123 abcdef - 建议配合 Spring AOP,在注解
@Idempotent(key = "#order.userId + ':' + #order.orderId")中提取 SpEL 表达式生成 key
数据库唯一约束是最可靠但有局限的兜底方式
在表中对业务关键字段加 UNIQUE 索引(如 order_no、user_id + biz_type + biz_id 组合),让数据库直接拒绝重复插入。
但要注意:
- 它只能防「写入重复」,不能防「重复扣款」「重复发消息」这类非 insert 操作
- 抛出的异常是
MySQLIntegrityConstraintViolationException或PSQLException,需统一捕获并转为友好提示(如“操作已存在,请勿重复提交”) - 如果业务逻辑复杂(先查再判再写),仍需配合 Redis 锁或状态机,否则会因查写分离产生竞态
前端 + 后端协同防重的实操要点
纯后端方案无法完全规避用户狂点、F5 刷新、网关重试等情况,必须前后端配合。
前端该做:
- 按钮点击后立即置灰 + 加载态,禁用 submit 或调用按钮
- 生成并携带一次性 token:
POST /submit → header: X-Idempotent-Token: abc123,后端校验并消费该 token - 对 GET 请求不做防重(幂等性天然成立),重点保护 POST/PUT/DELETE
后端接收时,需检查 X-Idempotent-Token 是否已存在 Redis 中(SETNX),存在则直接返回 409 Conflict,不存在才继续流程并设置过期时间(如 5 分钟)。
容易被忽略的是:token 过期时间要略大于接口最大可能耗时,且必须和业务操作绑定——比如支付接口的 token 不能用于退款接口,否则会误拦截。










