接口冪等性定義:無論一次或多次調用某個接口,對資源產生的副作用都是一致的。
簡單來說:用戶由于各種原因(網絡超時、前端重復點擊、消息重試等)對同一個接口發了多次請求,系統只能處理一次,不能因為多次請求導致數據錯誤(如扣款兩次、生成兩個訂單)。
方案一:Token 機制(適用于新增、提交類操作)
這是最經典的防重提交方案,尤其適合前端表單提交、支付下單等場景。
核心思想:客戶端先獲取一個服務器頒發的唯一令牌(Token),提交請求時必須帶上這個Token。服務器處理請求后,使該Token失效。
流程:
- 獲取Token:客戶端在發起業務請求前,先調用一個接口獲取一個全局唯一的Token(通常存于Redis,并設置較短的有效期)。
- 提交請求:客戶端帶著業務參數和這個Token發起業務請求。
- 校驗Token:
服務器端(通常通過AOP或Filter實現)檢查Redis中是否存在該Token。
存在:執行業務邏輯,然后刪除Redis中的Token。
不存在:說明該請求已被處理過,直接返回重復提交的錯誤信息。
代碼示例(AOP實現):
// 1. 自定義一個冪等性注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {int expireTime() default 60; // Token有效期,秒
}// 2. 編寫Token創建接口
@RestController
public class TokenController {@Autowiredprivate StringRedisTemplate redisTemplate;@GetMapping("/token")public String getToken() {String token = UUID.randomUUID().toString();redisTemplate.opsForValue().set(token, "1", Duration.ofSeconds(60)); // 存入Redis,60秒過期return token;}
}// 3. 編寫AOP切面處理冪等性校驗
@Aspect
@Component
public class IdempotentAspect {@Autowiredprivate StringRedisTemplate redisTemplate;@Around("@annotation(idempotent)")public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();String token = request.getHeader("X-Idempotent-Token"); // 從Header中獲取Tokenif (StringUtils.isEmpty(token)) {throw new RuntimeException("Token不存在");}// 核心邏輯:刪除Token(原子操作)。如果刪除成功,返回1,說明是第一次請求。Boolean isDeleted = redisTemplate.delete(token);if (Boolean.TRUE.equals(isDeleted)) {// 是第一次請求,放行return joinPoint.proceed();} else {// 刪除失敗,可能是Token已過期或被使用// 嘗試獲取Token,如果還能獲取到值,說明是重復請求(但還沒過期)。如果獲取不到,說明已過期。String tokenValue = redisTemplate.opsForValue().get(token);if (tokenValue != null) {throw new IdempotentException("請勿重復操作");} else {throw new IdempotentException("操作已過期,請刷新頁面后重試");}}}
}// 4. 在需要冪等性的接口上使用注解
@RestController
public class OrderController {@PostMapping("/createOrder")@Idempotent // 加上注解public String createOrder(@RequestBody Order order) {// 業務邏輯...return "訂單創建成功";}
}
優點:實現簡單,通用性強。
缺點:需要額外一次獲取Token的請求。
方案二:基于數據庫唯一索引(適用于插入操作)
核心思想:利用數據庫唯一索引的排他性,防止重復數據插入。
流程:
- 在數據表中為一個或多個字段建立唯一索引。這個“唯一鍵”可以是:
業務主鍵(如訂單ID)
組合字段(如:用戶ID + 業務類型 + 關聯ID) - 插入數據時,如果唯一鍵重復,數據庫會拋出 DuplicateKeyException。
- 代碼中捕獲這個異常,返回“請勿重復操作”的提示。
示例:防止用戶重復創建訂單。
- 在 order 表為 user_id 和 order_source_id(本次請求的唯一源ID)建立聯合唯一索引。
- 每次創建訂單時,如果同一個用戶使用同一個源ID請求,第二次插入就會失敗。
優點:實現最簡單,無需額外編碼,依靠數據庫本身能力。
缺點:只適用于新增場景,不適用于更新、刪除操作。
注意:
這個唯一id并不是數據庫表的自增id,而是在發起請求前,生成一個全局唯一的業務ID,例如:order_id = 雪花算法生成的長整形ID。
將這個 order_id 隨請求一起傳到后端。
在數據庫的 order 表上,為 order_id 字段建立一個唯一索引。
插入時,如果兩個請求帶著同一個 order_id 到來,第二個請求就會因為唯一索引沖突而插入失敗。
方案三:悲觀鎖/樂觀鎖(適用于更新、扣減操作)
- 悲觀鎖(Pessimistic Lock)
思想:“先取鎖,再操作”。認為并發沖突一定會發生,因此在操作數據時直接將其鎖住。
實現:使用 SELECT … FOR UPDATE。
場景:適用于寫操作非常頻繁,沖突概率極高的場景。要謹慎使用,容易導致性能瓶頸和死鎖。 - 樂觀鎖(Optimistic Lock) - 更推薦
思想:“先操作,再驗證”。認為沖突不常發生,通過版本號(Version)或狀態機來保證。
實現:
在表中增加一個 version 字段。
更新數據時,將 version 作為條件:UPDATE table SET value = new_value, version = version + 1 WHERE id = #{id} AND version = #{old_version}。 - 檢查 executeUpdate() 返回的影響行數:
如果為 1:更新成功,是第一次請求。
如果為 0:說明version已被其他請求修改過,是重復請求或沖突請求,操作失敗。
示例(余額扣款):
UPDATE account SET balance = balance - 100,version = version + 1
WHERE user_id = 123
AND version = 5; -- 舊的版本號
--通過在一次事務內,先執行 SELECT 語句查詢出來最新的版本號再進行更新操作。
優點(樂觀鎖):性能高,避免數據庫鎖競爭。
缺點:需要修改表結構,失敗后需要重試或告知客戶端。
方案四:狀態機約束(適用于有狀態流轉的業務)
核心思想: 很多業務數據都有明確的狀態流轉(如:訂單狀態:0待支付->1已支付->2已發貨)。只有在特定狀態下,操作才是允許的。
流程:
執行更新操作時,不僅以ID為條件,還必須加上當前狀態作為條件。
如果狀態不符合預期,則更新失敗,說明請求無效或已處理過。
示例(支付回調接口):
-- 只有狀態為“待支付”的訂單才能被更新為“已支付”
UPDATE orders SET status = '已支付' WHERE order_id = '123' AND status = '待支付';
優點:業務邏輯本身自帶的冪等性,無需額外組件。
缺點:僅適用于有狀態變化的業務。
總結與選擇
方案 | 適用場景 | 優點 | 缺點 |
---|---|---|---|
Token 機制 | 通用,尤其前端提交、創建操作 | 通用性強,可靠性高 | 需額外接口,多一次交互 |
唯一索引 | 數據插入類操作 | 實現最簡單,絕對可靠 | 僅限插入操作 |
樂觀鎖 | 數據更新、扣減庫存類操作 | 性能好,避免鎖競爭 | 需修改表結構,增加version字段 |
狀態機 | 有明確狀態流轉的業務(訂單、流程) | 天然冪等,符合業務邏輯 | 局限性較強 |
悲觀鎖 | 極高并發寫場景(少用) | 保證強一致性 | 性能差,易死鎖 |
在金融項目中如何選擇?
薪酬計算觸發接口:可用 Token機制 或基于業務ID的唯一索引(如 calculation_id),防止重復觸發計算。
獎金發放、余額扣減接口:樂觀鎖是最佳選擇,保證金額不會重復扣減。
訂單、流程狀態變更:狀態機約束是必須的,例如“只有未支付的訂單才能支付”。
最佳實踐: 通常會將多種方案組合使用。例如,先用 Token 機制 防重,在業務邏輯內再用 樂觀鎖 或 狀態機 做最終保障,形成雙保險。