????????實現 HTTP 接口的冪等性是確保多次相同請求產生相同結果的重要設計原則,尤其在網絡不穩定或分布式系統中非常關鍵。以下是幾種常見的實現方式:
1. 基于冪等性令牌(Token)的實現
適合支付、訂單創建等場景,步驟如下:
- 客戶端獲取令牌
- 客戶端攜帶令牌請求接口
- 服務端驗證并消費令牌
@Service
public class IdempotentService {// 實際項目中使用Redis等分布式緩存private final Set<String> tokenStore = ConcurrentHashMap.newKeySet();// 生成令牌public String generateToken() {String token = UUID.randomUUID().toString();tokenStore.add(token);return token;}// 驗證令牌public boolean validateToken(String token) {return tokenStore.remove(token); // 原子操作,確保唯一消費}
}@RestController
@RequestMapping("/orders")
public class OrderController {@Autowiredprivate IdempotentService idempotentService;@Autowiredprivate OrderService orderService;@PostMappingpublic ResponseEntity<?> createOrder(@RequestHeader("Idempotency-Token") String token,@RequestBody OrderRequest request) {// 驗證令牌if (!idempotentService.validateToken(token)) {return ResponseEntity.ok("重復請求,已處理");}// 處理訂單邏輯OrderResult result = orderService.createOrder(request);return ResponseEntity.ok(result);}
}
2. 基于數據庫唯一約束
通過數據庫唯一索引確保重復數據無法插入:
// 實體類
@Entity
@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"order_no"}) // 訂單號唯一約束
})
public class Order {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;private String orderNo;// 其他字段...
}// 服務層
@Service
public class OrderService {@Autowiredprivate OrderRepository orderRepository;@Transactionalpublic Order createOrder(OrderRequest request) {String orderNo = generateOrderNo();Order order = new Order();order.setOrderNo(orderNo);// 設置其他字段...try {return orderRepository.save(order);} catch (DataIntegrityViolationException e) {// 捕獲唯一約束異常,視為重復請求log.warn("訂單已存在: {}", orderNo);return orderRepository.findByOrderNo(orderNo).orElseThrow();}}
}
3. 基于 Redis 的分布式鎖
適合分布式系統中的冪等性控制:
@Service
public class RedisIdempotentService {@Autowiredprivate StringRedisTemplate redisTemplate;private static final String IDEMPOTENT_KEY_PREFIX = "idempotent:";private static final long EXPIRATION_TIME = 30L; // 30秒過期public boolean checkAndLock(String key) {String redisKey = IDEMPOTENT_KEY_PREFIX + key;// SET NX 命令:不存在則設置,返回truereturn Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(redisKey, "1", EXPIRATION_TIME, TimeUnit.SECONDS));}public void releaseLock(String key) {String redisKey = IDEMPOTENT_KEY_PREFIX + key;redisTemplate.delete(redisKey);}
}// 控制器中使用
@PostMapping("/pay")
public ResponseEntity<?> pay(@RequestParam String orderId) {if (!redisIdempotentService.checkAndLock(orderId)) {return ResponseEntity.ok("支付請求已處理");}try {// 處理支付邏輯paymentService.processPayment(orderId);return ResponseEntity.ok("支付成功");} finally {redisIdempotentService.releaseLock(orderId);}
}
4. 基于 Spring AOP 的冪等性注解
通過自定義注解簡化冪等性控制:
// 自定義注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {// 冪等鍵的參數索引,默認取第一個參數int keyIndex() default 0;
}// AOP切面
@Aspect
@Component
public class IdempotentAspect {@Autowiredprivate RedisIdempotentService redisService;@Around("@annotation(idempotent) && args(.., request)")public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent, HttpServletRequest request) throws Throwable {// 獲取冪等鍵(此處從請求頭獲取)String key = request.getHeader("Idempotency-Key");if (StringUtils.isEmpty(key)) {return ResponseEntity.badRequest().body("缺少冪等鍵");}if (!redisService.checkAndLock(key)) {return ResponseEntity.ok("重復請求");}try {return joinPoint.proceed();} finally {// 非必須,根據業務設置過期時間自動釋放// redisService.releaseLock(key);}}
}// 接口使用
@PostMapping("/transfer")
@Idempotent
public ResponseEntity<?> transferMoney(@RequestBody TransferRequest request) {// 處理轉賬邏輯return ResponseEntity.ok(transferService.transfer(request));
}
總結
- 選擇合適的方案:查詢操作天然冪等,無需額外處理;寫操作根據業務選擇令牌或唯一約束
- 過期策略:冪等鍵需設置合理過期時間,避免存儲空間無限增長
- 異常處理:確保冪等控制邏輯的異常不影響正常業務流程
- 分布式場景:必須使用 Redis 等分布式存儲,避免單機存儲導致的問題
- 原子性保證:驗證和業務操作需在同一事務中,或使用分布式鎖確保原子性