一、背景:為什么需要冪等性
在微服務、分布式架構下,網絡不可靠、請求重試機制(如前端超時重發、客戶端重發、網關重試、消息消費失敗重試等)會帶來重復請求,如果接口沒有冪等性,可能導致:
- 重復扣費
- 重復訂單
- 重復數據寫入
- 資源狀態異常
因此接口的冪等性是保障系統一致性、穩定性的關鍵。
二、冪等性的演進歷程
階段 | 實現方式 | 適用場景 | 特點 |
---|---|---|---|
1. 前端防重復提交 | 禁用按鈕、Token機制 | 簡單場景(如表單) | 限制小,易繞過 |
2. 數據庫唯一約束 | 唯一索引字段控制插入 | 插入類接口 | 利用數據庫能力 |
3. 冪等標識(唯一業務ID) | 唯一業務號控制冪等 | 訂單類接口 | 明確控制粒度 |
4. 分布式鎖 | 請求上鎖 | 任意接口 | 控制請求串行,犧牲吞吐 |
5. 狀態機機制 | 狀態+版本控制 | 狀態轉換操作 | 精細控制副作用 |
6. Redis 冪等性控制 | 冪等Key+過期時間 | 高并發下 | 快速、安全,結合令牌機制 |
三、最佳實踐總結
🧠 原則
- 冪等性以“結果相同”為基準,副作用不可重復。
- 控制粒度應適當:寫操作必須冪等,讀操作天然冪等。
- 冪等 Token 最好由客戶端生成或服務端下發后強制綁定。
? 最佳實踐建議
- 業務唯一標識驅動冪等性,如訂單號、支付流水號。
- 冪等性應放在業務代碼邏輯前端處理,即“第一次判斷是否重復提交”。
- 采用 Redis + 原子操作(如SETNX)+ Token + TTL 實現防重。
- 接口冪等性建議作為注解能力進行抽象封裝,統一使用。
四、Spring Boot 實現示例(完善版)
下面我們以 Token機制 + Redis原子寫入實現接口冪等性,構建完整示例。
1?? 添加依賴
<!-- Redis starter -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2?? Redis配置類(Lettuce連接池)
@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {RedisTemplate<String, String> template = new RedisTemplate<>();template.setConnectionFactory(factory);template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(new StringRedisSerializer());return template;}
}
3?? 冪等Token生成接口(服務端)
@RestController
@RequestMapping("/token")
public class TokenController {@Autowiredprivate RedisTemplate<String, String> redisTemplate;@GetMapping("/generate")public String generateToken() {String token = UUID.randomUUID().toString();redisTemplate.opsForValue().set("idem-token:" + token, "valid", 5, TimeUnit.MINUTES);return token;}
}
4?? 冪等校驗注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {String key() default ""; // 可自定義Key策略
}
5?? 冪等校驗切面
@Aspect
@Component
public class IdempotentAspect {@Autowiredprivate RedisTemplate<String, String> redisTemplate;@Around("@annotation(idempotent)")public Object checkIdempotency(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();String token = request.getHeader("Idempotency-Token");if (StringUtils.isEmpty(token)) {throw new RuntimeException("冪等Token不能為空");}String redisKey = "idem-token:" + token;Boolean result = redisTemplate.delete(redisKey); // 核心邏輯:刪除即消費if (Boolean.FALSE.equals(result)) {throw new RuntimeException("重復請求,請勿重復提交");}return joinPoint.proceed();}
}
6?? 接口使用示例
@RestController
@RequestMapping("/order")
public class OrderController {@PostMapping("/create")@Idempotentpublic String createOrder(@RequestBody OrderDTO orderDTO) {// 模擬業務邏輯return "訂單創建成功,訂單號:" + UUID.randomUUID().toString();}
}
7?? Postman 調用流程:
-
調用
GET /token/generate
獲取 token -
調用
POST /order/create
,在請求頭加上:Idempotency-Token: 你剛剛拿到的token
多次調用同一個 token,只會成功一次,后續將拋出“重復請求”異常。
五、可擴展建議
功能 | 實現方式 |
---|---|
支持注解中的自定義 key 邏輯 | SpEL 表達式,讀取參數字段 |
冪等記錄持久化 | Redis + 數據庫雙寫 |
異常重試隊列支持 | Kafka / RocketMQ 消息冪等處理 |
Redisson替換RedisTemplate | 使用 RLock.tryLock() 可實現鎖粒度的冪等控制 |
六、架構圖(邏輯)
客戶端|
[請求附帶Token]|
Spring Boot 接口(注解攔截)|
切面邏輯校驗 Token|
Redis SETNX 刪除 key -> 是第一次? -> 繼續業務|
業務邏輯執行(訂單創建、支付、提交...)
后續實例回陸續添加
參數冪等、結果緩存、冪等日志記錄