在分布式系统中解决幂等性问题是保证系统健壮性的关键挑战之一。以下从原理到实践的完整解决方案,涵盖7种核心模式及落地实现细节:
一、幂等性问题本质
核心定义:相同请求多次执行对系统状态的影响 = 执行一次的结果
产生场景:
- 网络抖动导致客户端重复提交
- 服务端超时后重试机制
- MQ消费者重复消费消息
- 分布式事务补偿机制
# 典型案例:支付重复扣款
curl -X POST https://pay/order/123
请求结果:因网络超时未响应 → 客户端重试 → 两次扣款❗
二、解决方案全景图
方案 | 适用场景 | 原理复杂度 | 实现成本 |
---|---|---|---|
唯一请求ID | 大部分写操作 | ★☆☆ | 低 |
数据库唯一约束 | 数据强唯一性场景 | ★★☆ | 中 |
Token机制 | 防表单重复提交 | ★☆☆ | 低 |
状态机流转 | 订单/工单流程类系统 | ★★★ | 高 |
分布式锁 | 高并发竞争资源 | ★★☆ | 中 |
版本号控制(乐观锁) | 存在版本属性的更新 | ★★☆ | 中 |
去重表(Redis/HBase) | 高频临时性幂等校验 | ★★☆ | 高 |
三、核心解决方案详解
1. 唯一请求ID模式
实现流程:
sequenceDiagramparticipant Clientparticipant Serverparticipant DBClient->>Server: 请求携带全局ID(X-Request-ID)Server->>DB: 查询是否存在相同IDalt 首次请求DB-->>Server: 不存在记录Server->>DB: 执行业务操作+记录IDServer-->>Client: 返回成功else 重复请求DB-->>Server: 已存在记录Server-->>Client: 返回先前结果end
技术要点:
- ID生成使用雪花算法(Snowflake)或Redis原子操作
- 存储使用Redis(TTL自动过期)或MySQL(需定期清理)
- 防暴力破解:ID长度≥20位(包含时间戳+随机数)
示例代码(Spring Boot+Redis):
@RestController
public class PaymentController {@Autowiredprivate RedisTemplate<String, String> redisTemplate;@PostMapping("/pay")public ResponseEntity<?> pay(@RequestHeader("X-Request-ID") String requestId, @RequestBody PaymentRequest request) {// Redis原子操作判断是否存在Boolean isAbsent = redisTemplate.opsForValue().setIfAbsent("payment:req:"+requestId, "processing", 5, TimeUnit.MINUTES);if (!isAbsent) {return ResponseEntity.status(HttpStatus.CONFLICT).body(Map.of("code": 409, "message": "Duplicate request"));}try {// 真实支付逻辑...return ResponseEntity.ok(Map.of("status", "success"));} finally {// 完成处理时更新状态(异步操作)redisTemplate.opsForValue().set("payment:req:"+requestId, "completed");}}
}
2. 数据库唯一约束
设计模式:
-- 订单表增加唯一索引
ALTER TABLE orders ADD UNIQUE INDEX uniq_request (user_id, request_hash);-- 请求哈希生成算法(Java示例)
String requestHash = DigestUtils.md5Hex(userId + orderNo + productId + amount
);
异常处理:
@Transactional
public void createOrder(Order order) {try {orderDao.insert(order); // 触发唯一约束} catch (DataIntegrityViolationException ex) {// 抓取重复提交异常Order existing = orderDao.selectByHash(order.getRequestHash());throw new DuplicateOrderException(existing.getOrderNo());}
}
性能优化:
- 分库分表时需确保唯一键在分片键内
- 高频写场景配合消息队列削峰填谷
3. Token令牌机制
交互流程:
- 客户端先请求获取Token(携带用户ID)
- 服务端生成Token存储(Redis: user:123 -> token:xyz)
- 客户端提交请求时携带Token
- 服务端验证Token后立即删除(原子操作)
关键代码:
public class TokenService {public String generateToken(String userId) {String token = UUID.randomUUID().toString();String key = "token:" + userId;redisTemplate.opsForValue().set(key, token, 5, TimeUnit.MINUTES);return token;}public boolean validateToken(String userId, String token) {String key = "token:" + userId;// Lua脚本保证原子性校验+删除String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +"return redis.call('del', KEYS[1]) else return 0 end";Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),Collections.singletonList(key), token);return result != null && result == 1L;}
}
4. 分布式锁 + 幂等框架整合
架构设计:
集成框架(推荐):
- Spring自带
@Idempotent
注解+AOP - Redisson分布式锁(支持多种锁类型)
- 阿里开源的resilience4j幂等模块
AOP实现示例:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {String key() default ""; // SpEL表达式int ttl() default 300; // 默认5分钟Class<? extends Payload> type(); // 根据payload类型处理
}@Aspect
@Component
public class IdempotentAspect {@Around("@annotation(idempotent)")public Object checkIdempotent(ProceedingJoinPoint joinPoint, Idempotent idempotent) {// 解析SpEL表达式生成唯一KeyString lockKey = generateKey(joinPoint, idempotent);// Redisson尝试获取锁RLock lock = redissonClient.getLock(lockKey);try {if (lock.tryLock(0, idempotent.ttl(), TimeUnit.SECONDS)) {return joinPoint.proceed();} else {throw new ConcurrentRequestException("请求正在处理中");}} finally {lock.unlock();}}
}
四、特殊场景解决方案
1. MQ消息幂等
RocketMQ方案:
// 消费者实现MessageListener
public class OrderListener implements MessageListenerOrderly {@Overridepublic ConsumeOrderlyStatus consumeMessage(List<MessageExt> messages, ConsumeOrderlyContext context) {for (MessageExt msg : messages) {String msgId = msg.getMsgId();if (processed(msgId)) {continue; // 跳过已处理消息}processOrder(msg);markProcessed(msgId);}return ConsumeOrderlyStatus.SUCCESS;}private boolean processed(String msgId) {// Redis记录已处理消息IDreturn !redisTemplate.opsForValue().setIfAbsent("mq:processed:"+msgId, "1", 7, TimeUnit.DAYS);}
}
2. 第三方接口调用
public class ThirdPartyService {@Retryable(value = IOException.class, maxAttempts = 3)@Idempotent(type = Payment.class, key = "#payment.tradeNo")public void callPaymentAPI(Payment payment) {// 使用feign调用对方接口Response resp = paymentClient.create(payment);if (resp.code() == 504) {// 明确要求对方做幂等处理的场景throw new RetryableException("需要重试");}}
}
五、最佳实践建议
-
分层防御策略:
- 前端:按钮防重+Token机制(第一道防线)
- 网关:全局请求ID生成与校验(第二道防线)
- 服务层:分布式锁/唯一索引(最后屏障)
-
监控指标:
# Grafana监控面板关键指标 idempotent_requests_total{status="duplicate"} // 重复请求计数 redis_lock_wait_duration_seconds_bucket // 锁竞争延迟分布 db_unique_violations_total // 唯一约束冲突数
-
压力测试要点:
# 使用wrk测试高并发幂等性 wrk -t12 -c400 -d60s --脚本=post_duplicate.lua http://api/order
测试Lua脚本:
-- 模拟重复提交同一个请求ID request_id = math.random(1000000) wrk.headers["X-Request-ID"] = tostring(request_id) for i=1,5 do -- 每个线程重复提交5次wrk.body = '{"amount":100}'wrk.method = "POST"wrk.path = "/order"wrk.request() end
-
容灾方案:
- 白名单机制:在Redis故障时允许特定商户跳过校验
- 异步对账:定时任务扫描疑似重复数据
- 熔断降级:检测到存储层异常时暂时关闭复杂幂等校验
通过以上多层级、多维度的幂等解决方案,可满足从简单到复杂的分布式场景需求,结合具体业务特性选择组合策略,可有效保障系统在高并发、网络不稳定环境下的数据一致性。