要在應用中做到冪等,其實並不難,本文嘗試做一個系統性的總結,歡迎一起探討。
什麼是冪等
某個操作執行一次,跟執行多次的效果一樣。冪等一詞來自於數學中的冪等,即f(f(x)) = f(x)。
需要保證冪等的場景
查詢類的讀操作,天然是冪等的,多次呼叫不會有副作用。需考慮以下幾種寫操作的情況:
- 呼叫下游寫介面
- 寫資料庫、寫Redis等
- 訊息訂閱和處理
例子:不能給使用者重複發放優惠券、現金獎勵、通知等,商家更新商品時不能重複增加或減少庫存。
下面分別討論這幾種情況。
1、呼叫下游寫介面
主要依靠下游服務保證冪等。
本服務能做的是,在調下游寫介面時不做重試,需設定重試次數為0。
2、自己服務保證
2.1 基於狀態的冪等
這種情況比較簡單,只有當滿足前置條件時才允許操作,否則不允許更新(例如已經是終態),直接返回。
例子:訂單支付成功後,不允許重複支付。
2.2 基於唯一鍵的冪等
冪等key的選取
與業務強相關,可以是商品id、訂單id、使用者id,或者日期等,或者是幾個業務欄位的組合。
幾個例子:
- 一個使用者每天只能領一張優惠券,通過 使用者id+優惠券型別+日期字串 即可唯一標識
- B端更新庫存,商品id+該商品的版本號
- C端扣庫存,訂單id
值得注意的是,需要區分新增和修改:修改時的冪等key往往需要帶上版本號,才能區分是否同一次修改,每次修改對應一個唯一的版本號。
實現方式
MySQL表中為冪等key建立唯一索引:強冪等,例如資金、訂單,絕對不允許重複處理,當插入重複資料時報錯。
不推薦用Redis實現冪等,一旦Redis出問題,比如節點當機,可能出現2個client同時獲取到鎖的情況。
MySQL冪等虛擬碼:
插入重複記錄,捕獲異常,提示冪等攔截。
try {
// 插入記錄
someDao.create(someRecord);
} catch (DataIntegrityViolationException e) {
// 如果是重複記錄,返回異常
return failResponse("冪等攔截");
} catch (Throwable t) {
// 異常處理
return failResponse("其他異常");
}
3、訊息訂閱和處理
MQ通常會保證訊息至少傳送一次(可能多次),並且在機器例項重啟或發版時,consumer group會做rebalance,進而收到重複的訊息。因此,訊息的冪等處理必不可少。
實現方式:
在處理訊息前加上Redis鎖:如果上鎖成功,則繼續處理,否則稍後重試。
- setnxex,不存在時才設定,時效即為鎖的租期,否則忽略
- 接下來的業務處理,如果是自身邏輯需要強冪等則使用上述資料庫冪等方式,如果全部依賴下游則依賴下游實現冪等
Redis冪等虛擬碼:
// 生成冪等key
String redisKey = buildRedisKey();
// 上Redis鎖,租期為leaseTime
if (redisLock.tryLock(redisKey, leaseTime)) {
// 業務邏輯處理
} else {
// 稍後重試
}