Java接口防重案例:从基础到高并发的完整实战指南
目录导读
- 为什么接口需要防重?——业务痛点与场景剖析
- 防重设计的核心原则与常见误区
- 基于数据库唯一索引的防重(最稳妥)
- 基于Redis的分布式锁防重(高性能首选)
- Token令牌机制(适用于表单提交)
- 状态机与幂等表(适用于复杂业务流)
- 高并发场景下的防重优化策略
- 常见问题问答(FAQ)
- 总结与最佳实践建议
为什么接口需要防重?——业务痛点与场景剖析
在真实的Java后端开发中,接口重复请求是导致数据错乱、重复扣款、库存超卖等严重问题的“头号元凶”。

- 电商下单:用户快速点击“提交订单”按钮两次,可能生成两个相同订单,导致重复扣款。
- 支付回调:第三方支付平台因网络超时多次通知同一笔支付结果,若不做防重,用户余额会被重复增加。
- 表单提交:前端未做按钮禁用,用户狂点提交,后台写入多条相同数据。
这些场景的共同点是:同一业务请求在极短时间内被客户端或上游系统发送多次,防重设计的本质是保证接口的幂等性(即无论调用多少次,业务结果一致)。
防重设计的核心原则与常见误区
核心原则
- 唯一性标识:每个业务请求必须有全局唯一的业务ID(如订单号、流水号、token)。
- 先检查后操作:在业务逻辑执行前,先验证该ID是否已处理过。
- 原子性:检查与状态标记必须原子化执行,否则并发下依然会穿透。
常见误区
- ❌ 只用前端按钮置灰:用户可绕过前端直接发HTTP请求。
- ❌ 用同步锁(synchronized):只能锁单机进程,无法解决分布式部署问题。
- ❌ 依赖数据库查询后再插入:先查后插非原子,并发下仍会重复。
方案一:基于数据库唯一索引的防重(最稳妥)
适用场景:对性能要求不高,但必须绝对可靠的数据写入操作。
实现方式
在数据表的关键字段上创建唯一索引(如订单号、业务流水号),当重复请求尝试插入相同记录时,数据库会抛出DuplicateKeyException,程序捕获该异常即可优雅处理。
示例代码
public void createOrder(OrderDTO orderDTO) {
try {
orderMapper.insert(orderDTO);
} catch (DuplicateKeyException e) {
log.warn("重复订单请求,订单号:{}", orderDTO.getOrderNo());
throw new BizException("请勿重复提交");
}
}
优缺点
- ✅ 实现简单,强一致,天然支持分布式。
- ❌ 每次插入都要触发数据库索引维护,高并发小规模下存在性能瓶颈。
- ❌ 无法防住“先查后改”类业务(如更新库存)。
方案二:基于Redis的分布式锁防重(高性能首选)
适用场景:高并发、低延迟场景(如秒杀、抢红包)。
实现方式
使用Redis的SETNX命令(或Redisson的tryLock)实现分布式锁,以业务ID作为key,设置过期时间,如果SETNX返回true,则执行业务;否则直接拒绝。
核心代码
public boolean tryLock(String bizKey, long expireTime) {
String lockKey = "lock:" + bizKey;
return redisTemplate.opsForValue()
.setIfAbsent(lockKey, "locked", Duration.ofSeconds(expireTime));
}
// 使用示例
if (tryLock("order:" + orderNo, 30)) {
try {
// 执行业务逻辑
} finally {
redisTemplate.delete("order:" + orderNo); // 释放锁
}
} else {
throw new BizException("请求处理中,请勿重复提交");
}
注意事项
- 锁必须设置过期时间:防止业务异常导致死锁。
- 释放锁前验证持有者:使用Lua脚本保证原子性,避免误删他人锁。
- 分布式锁的粒度:应以业务ID(如订单号)为维度,而不是锁住整张表。
方案三:Token令牌机制(适用于表单提交)
适用场景:用户交互型页面,如提交报名、发布文章。
实现方式
- 前端请求后端获取一个一次性token(存入Redis,key为token,value为用户ID)。
- 提交表单时必须携带该token。
- 后端收到请求后,原子删除Redis中的token(使用
DEL命令),删除成功则继续执行,否则拒绝。
核心代码
// 获取token
public String getToken(String userId) {
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("form:" + token, userId, Duration.ofMinutes(10));
return token;
}
// 校验并消费token
public boolean checkAndConsume(String token) {
String key = "form:" + token;
Boolean success = redisTemplate.delete(key);
return Boolean.TRUE.equals(success); // true表示首次使用,false表示重复提交
}
优点
- ✅ 实现简单,无需处理数据库锁。
- ✅ 天然防重复,因为token一次性失效。
缺点
- ❌ 需要前后端配合,前端必须获取token。
- ❌ 若token泄露,可能被恶意利用(需增加用户绑定校验)。
方案四:状态机与幂等表(适用于复杂业务流)
适用场景:订单状态流转、支付退款等需要跟踪“当前进度”的复杂业务。
实现方式
创建一张idempotent_record表,记录业务ID、请求时间、处理状态(0=处理中,1=完成),业务执行前插入一条状态为0的记录,执行成功后更新状态为1,重复请求发现存在状态为0或1的记录时直接返回。
关键表结构
CREATE TABLE idempotent_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
biz_id VARCHAR(64) NOT NULL,
status TINYINT NOT NULL DEFAULT 0 COMMENT '0处理中,1完成',
create_time TIMESTAMP,
UNIQUE KEY uk_biz_id (biz_id)
);
状态机流转
- 插入记录(状态0)→ 执行业务 → 更新状态1。
- 若业务失败,可删除记录或标记为失败,允许重试(需额外字段)。
对比优点
- ✅ 可感知业务执行结果(成功/失败)。
- ✅ 天然支持异步重试(重试前检查状态)。
高并发场景下的防重优化策略
- 双写一致性优化:使用
Redis SETNX作为第一道防线,数据库唯一索引作为兜底,先快速拦截,再防穿透。 - 批量请求防重:对批量提交接口,将整个请求体进行MD5哈希后作为防重key,注意排除时间戳等随机字段。
- 防重与缓存结合:对于读多写少场景,可用Redis缓存处理结果,重复请求直接返回缓存中的结果,减少业务计算。
- 流量削峰:接入消息队列(如RabbitMQ)将请求异步化,由消费端通过幂等表去重。
- 监控与告警:对防重拦截次数进行Prometheus监控,及时发现恶意攻击或异常流量。
常见问题问答(FAQ)
Q1:数据库唯一索引和Redis锁,我该怎么选?
- A:如果业务是纯插入(如用户注册、日志记录),优先选唯一索引——零运维成本,如果业务是“读-改-写”(如扣库存),必须用Redis锁或状态机,因为唯一索引无法防止并发下的更新覆盖。
Q2:Redis锁过期了但业务还没执行完怎么办?
- A:这是经典的“锁过期”问题,解决方案有:①使用Redisson的看门狗自动续期;②业务结束时主动删除锁,并设置合理过期时间为最长执行时间的1.5倍。
Q3:防重成功是返回“成功”还是“失败”?
- A:需区分场景,如果重复请求是用户误提交,应返回“重复提交”错误提示,但如果是支付回调重试,则应返回“成功”(幂等设计),避免上游无限重试。
Q4:防重和幂等是同一个概念吗?
- A:不完全相同,防重是“防止第二次请求执行”,幂等是“无论执行多少次,结果一致”,防重是实现幂等的一种手段,采用Token防重后,第二次请求被拒绝,而真正幂等的接口会返回第一次请求的结果。
Q5:如何测试防重逻辑的正确性?
- A:使用JMeter模拟100个并发请求携带相同业务ID,断言数据库只产生一条数据;同时测试Redis锁的过期时间是否触发;最后做故障演练,模拟Redis宕机时数据库唯一索引能否兜底。
总结与最佳实践建议
防重没有“银弹”,需要根据业务场景权衡,以下是我在实际项目中总结的选型参考:
| 业务类型 | 推荐方案 | 理由 |
|---|---|---|
| 低并发简单插入 | 数据库唯一索引 | 简单可靠,无需额外组件 |
| 高并发秒杀/扣库存 | Redis分布式锁 + 数据库乐观锁 | 高性能且保证数据一致 |
| 表单提交 | Token令牌 | 用户感知良好,拦截率100% |
| 订单状态流转 | 幂等表 + 状态机 | 可追踪执行状态,支持重试 |
最佳实践口诀:
- 前端必防:按钮禁用+Token隐藏域,减少无效请求。
- 后端兜底:Redis快拦截,数据库慢兜底(最终一致性)。
- 日志记录:每次防重拦截都记录请求方、业务ID、原因,方便排查。
- 过期清理:对于幂等表或Redis Key,设置TTL,避免无限制堆积。
请记住:防重设计要提前融入系统架构,而不是事后打补丁,一个优秀的防重方案,能让你在“用户狂点”、“上游重试”、“恶意刷接口”三座大山下,依然保持数据坚如磐石。