架構師必備:系統性解決冪等問題

Java烘焙師發表於2022-01-14

要在應用中做到冪等,其實並不難,本文嘗試做一個系統性的總結,歡迎一起探討。

什麼是冪等

某個操作執行一次,跟執行多次的效果一樣。冪等一詞來自於數學中的冪等,即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 {
        // 稍後重試
    }
    

相關文章