冪等最佳實踐

得物技術發表於2022-02-10

1. 為什麼需要冪等

分散式場景下,多個業務系統間實現強一致的協議是極其困難的。一個最簡單和可實現的假設就是保證最終一致性,這要求服務端在處理一個重複的請求時需要給出相同的回應,同時不會對持久化資料產生副作用(即多次操作與單次操作的結果需要是業務角度一致的)。

一個API擁有冪等能力的話,呼叫發起方就可以很安全的進行重試。這符合我們普遍的假設。

提供冪等能力是服務提供方需要做的事,所以本文是站在服務提供者的角度來寫的,即下文的“我們”通常指的是服務提供方。

2. 怎麼做冪等判斷

那麼我們怎麼對一次請求做出冪等的判斷呢?首先我們需要有方法來區分什麼是“一次”請求,其次,針對API實現邏輯中持久化資料方式的不同,還需要不同的判斷方法。

2.1 請求唯一標識

通常我們的API需要增加bizId這樣的能夠標識請求唯一性的屬性,呼叫發起方使用它來標識一次業務上認為的獨特的業務需求。一定要注意它是“業務唯一性標識”,而不是技術唯一性標識,這兩者是有本質區別的。

事實1:不過大多數情況下,呼叫發起方可能並不太知道應該如何生成一個有效的請求唯一標識,這需要我們在業務對接的過程中有更多的交流。

2.2 需要冪等多久

能夠冪等代表我們肯定做了某些資料的持久化,但是任何資料都不可能永久存在,都會要求有一個有效期。

對於大多數的請求,建議冪等的有效期是三個月。一些特殊的場景甚至可以是一年。但是幾乎不需要更久了。

有效期是一個明確的契約,這代表我們可以定期對持久化資料做一些資料治理的工作,同時,超過有效期的請求基本上會冪等失敗,那麼它的後果就是“同樣的事,在幾個月後又做了一次”。

2.3 寫入型

如果我們的業務邏輯是在持久化儲存中寫入什麼,那麼最好的做法是增加一個unique key,它通常由 user_id, 操作型別, biz_id組成。操作型別是我們系統設計上自行定義的業務操作型別,加上它是為了避免不同的操作型別之間互相干擾。加上user_id是因為通常情況下我們都會分庫分表。

針對既會寫入,還會更新資料的場景,需要把插入放到前面,不然冪等很可能無法工作。比如如下的庫存操作的場景,如果兩條SQL倒過來寫,在庫存售磬的場景下就無法冪等了:

insert 庫存扣減流水;
update 庫存 set 庫存可用數=庫存可用數-1 where 庫存id=? and 庫存可用數>0;

在unique key衝突時,我們需要捕獲它,這大概率是冪等了。為什麼說是大概率而不是一定呢?這是因為前述提前的“事實1”。呼叫方很可能錯誤的重用了請求唯一號,並且可能在重試時就一些核心的引數進行了改動。

所以,我們需要在發生unique key衝突時做如下的事情:

  1. 根據唯一鍵,將之前寫入的資料查詢出來
  2. 進行關鍵資訊的核對。比如我們提供的API是發放優惠券,那麼我們需要至少校驗這些資訊:接收使用者Id,券模板,券面額,有效期等。在關鍵資訊不一致時,返回類似於“DUPLICATE_BUT_DIFFERENT_REQUEST”的錯誤碼;在校驗通過時,返回傳送成功的券Id。這非常重要,不進行關鍵資訊的校驗就返回冪等成功是大多數場景下故障的根源。

2.4 更新型

更新型最大的挑戰在於我們沒有地方來儲存對於一條資料的多次更新行為,所以大多數情況下需要使用狀態機來推測某個更新行為是否發生過了,更復雜的情況可能需要我們增加專用的冪等表。

2.4.1 使用狀態機

業務上的資料處理大多都是有狀態的,比如交易訂單。這個時候首選的方法是通過狀態機的序列來判斷某個更新行為是否已經做過了。不過這並不簡單,我們需要非常小心的去看業務上的各種規則和限制,同時,除了更新狀態之外,大多數情況下還會伴隨著更新一些別的附加資訊,我們還需要去檢查這些附加資訊是否如請求要求的那樣更新過了。

2.4.2 增加冪等表

通過增加冪等表,就把更新型轉化為了2.3節描述的寫入型。不過需要注意冪等表需要和當前更新的表在同一個事務中,不然就是無效的。

冪等表需要增加表明資料寫入/更新時間的欄位,這便於我們定期進行過期資料的清理。

2.5 刪除型

建議在儲存上使用邏輯刪除而不是物理刪除,這樣我們就有辦法判斷刪除操作是否已經做過了。刪除往往意味著資料進行終態,所以在冪等上也是最好處理的;如果業務的設計上刪除不是資料的終態,那麼就需要相當小心,因為這違反了通用的設計原則。

3. 總結

冪等可以說是分散式應用的基石,如你所見,實現它並不像大多數人一開始想像的那麼簡單。做好它需要我們站在業務語義的角度來設計與思考。

文/蘇木

關注得物技術,做最潮技術人

相關文章