Java案例如何实现服务防重?——从原理到实战的完整指南
📖 目录导读
为什么服务需要防重?
在微服务架构中,重复请求是导致数据不一致、资源浪费甚至系统崩溃的常见原因。

- 用户快速点击“支付”按钮,导致同一订单被创建两次。
- 网络重试机制(如HTTP 502后的自动重试)发送重复请求。
- 消息队列消费端未做幂等处理,导致消息被重复消费。
防重(Idempotency) 的核心目标是:无论请求被重复执行多少次,最终结果与执行一次完全一致,这对于金融交易、订单创建、库存扣减等场景至关重要。
防重核心原理:幂等性设计
幂等性是数学中的概念,映射到服务端表现为:
- 相同参数、相同请求,无论调用多少次,业务侧状态不变。
- 查询操作天然幂等;更新操作需通过锁或唯一索引保证。
常见实现方式:
- 去重表:利用数据库唯一索引,重复插入会报错。
- 状态机:订单状态不允许从“已支付”变回“待支付”。
- Token令牌:前端获取唯一Token,后端处理时校验并消费Token。
常见防重方案对比
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 数据库唯一键 | 写操作,如订单号 | 简单可靠 | 依赖数据库性能 |
| Redis分布式锁 | 高频并发场景 | 性能高 | 需处理锁过期 |
| 本地锁 | 单机应用 | 零网络开销 | 无法跨进程 |
| 消息去重 | 消息队列消费端 | 天然支持 | 需引入消息ID |
Java实战案例:基于Redis的分布式防重
业务场景:用户提交订单,防止重复创建。
整体流程:
前端请求 -> 拦截器生成唯一请求ID(如UUID) -> Redis SETNX加锁 -> 执行业务逻辑 -> 解锁
代码实现与细节解析
1 自定义防重注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
long lockTime() default 3000; // 默认锁3秒
String keyPrefix() default "order:";
}
2 AOP拦截器实现
@Aspect
@Component
public class RepeatSubmitAspect {
@Autowired
private StringRedisTemplate redis;
@Around("@annotation(repeatSubmit)")
public Object around(ProceedingJoinPoint pjp, RepeatSubmit repeatSubmit) throws Throwable {
// 生成唯一Key,order:userId:orderId
String businessKey = buildBusinessKey(pjp);
// 使用SETNX尝试加锁
Boolean success = redis.opsForValue().setIfAbsent(businessKey, "1",
Duration.ofMillis(repeatSubmit.lockTime()));
if (Boolean.FALSE.equals(success)) {
throw new RuntimeException("请勿重复提交!");
}
try {
return pjp.proceed();
} finally {
// 业务完成后释放锁(注意:防止误删)
redis.delete(businessKey);
}
}
}
3 客户端请求头携带唯一ID
// 前端示例:生成UUID放入Header
const requestId = crypto.randomUUID();
fetch('/api/order', {
method: 'POST',
headers: { 'X-Request-Id': requestId }
});
QA:高频问题解答
Q1:为什么不直接使用数据库唯一索引?
A:数据库唯一索引适合对幂等性要求极高且低频的场景(如订单号),但对于高并发,数据库频繁插入失败会导致性能下降,而Redis适合做第一道防线。
Q2:锁过期时间怎么设置?
A:锁时间应大于业务最大执行耗时,业务平均200ms,设置3秒锁时间,若业务超时,锁自动释放,防止死锁。
Q3:如果业务执行中途宕机,锁未释放怎么办?
A:锁设置过期时间(TTL)即可自动解除,但需注意:若业务执行时间超过TTL,锁被释放后可能被其他请求获取,导致重复执行,建议结合业务ID和状态机二次校验。
Q4:分布式环境下如何确保锁原子性?
A:使用Redisson的RLock(基于Redis的分布式锁)或Zookeeper的临时节点,Redis SETNX虽然简单,但需配合Lua脚本保证“检查-删除”原子性。
Q5:防重和幂等有什么区别?
A:防重是阻止重复请求执行;幂等是允许多次执行但结果一致,防重是幂等的一种实现手段。
性能优化与注意事项
- Key设计:
userId:methodName:requestId避免全局唯一Key过少导致误拦截。 - 缓存雪崩:锁过期时间设置固定值或加上随机偏移量,避免同时过期。
- 降级处理:若Redis宕机,可降级为本地锁(如Guava Cache)或直接放行(风险自担)。
- 测试覆盖:必须测试高并发下的锁争抢、超时释放、重复请求处理。
服务防重并非单一技术,而是一种贯穿请求链路的设计思维,通过本文的Java案例,你可以从零搭建基于Redis的分布式防重方案,真正的防重需要“后端拦截+前端控制+业务校验”三层配合,才能在不同场景下保证数据正确性。
最后提醒:如果您的业务需要更复杂的分布式锁管理,可以了解Redisson或etcd的官方文档,以上代码已脱敏,生产环境需适配具体业务。