防重冪等
前言:
在分散式系統下,服務之間相互呼叫,必然會存在呼叫失敗並且進行重試的情況,在某些情況下就需要做好防重冪等。
防重和冪等是什麼?
防重:避免產生重複資料
冪等:除了避免產生重複資料之外,還要求每次請求都返回一樣的結果
什麼情況會導致重複?
傳送方傳送相同的請求到服務端。
- 前端多次傳送相同的請求到後端
- 超時重發導致的重複
- MQ異常導致的重複消費
如何防重?
-
insert之前先select,通常情況下有效,但是在高併發情況下,也會導致重複
-
建立唯一索引,資料庫兜底,防止重複新增
-
某些業務表在特定的場景下才不允許重複,不能直接建立唯一鍵,就可以增加一張防重表(為此類業務),將此類資料在同一事務下先insert進防重表成功,在insert業務表,假如insert進防重表失敗,證明此類資料重複,就不用再處理業務表了
-
加分散式鎖(針對單據來鎖):需要合理設定過期時間。不能太短,導致業務沒有處理完,鎖失效,防重失敗;也不能不設定過期時間,解鎖異常導致鎖一直被佔,阻塞後續處理。
什麼情況要做冪等?
使用者對於同一操作發起的一次請求或者多次請求的結果是一致的,不會因為多次點選而產生了副作用。
例如:
- 比如使用者對一筆訂單發起付款,因為網路問題沒有返回結果,就多次點選付款按鈕,此時只能發起一筆真實的交易,生成一條交易記錄。
- 分散式系統中,因為介面超時,導致的重試,第一次請求介面超時,沒有獲取到返回結果(有可能已經成功了),第二次重試,接收方不能直接返回失敗,要根據第一次處理的結果進行返回。
怎麼解決?
- 新增資料類介面,通過防重解決。
- 更新類介面,比如更新庫存,更改狀態等,通過狀態,加樂觀鎖解決。
根據狀態判斷
很多業務是有狀態的,比如一個訂單表。有下單0、支付中1、已支付2、取消支付3等狀態,
假如id=123的訂單狀態是0,現在要變成支付中狀態。
update order set status=1 where id=123 and status=0;
第一次請求時,該訂單的狀態可以正常更新,sql執行結果的影響行數是1,訂單狀態變成了1。後面有相同的請求過來,再執行相同的sql時,由於訂單狀態變成了1,再用status=0作為條件,最終sql執行結果的影響行數是0,即不會真正的更新資料。但為了保證介面冪等性,介面也需要直接返回成功。
加樂觀鎖,在表中增加一個version欄位。
在更新資料之前先查詢一下資料:
select id,amount,version from user id=123;
如果資料存在,假設查到的version等於1,再使用id和version欄位作為查詢條件更新資料:
update user set amount=amount+100,version=version+1 where id=123 and version=1;
更新資料的同時version+1,然後判斷本次update操作的影響行數,如果大於0,則說明本次更新成功,如果等於0,則說明本次更新沒有讓資料變更。
由於第一次請求version等於1是可以成功的,操作成功後version變成2了。這時如果併發的請求過來,再執行相同的sql:
update user set amount=amount+100,version=version+1 where id=123 and version=1;
該update操作不會真正更新資料,最終sql的執行結果影響行數是0,因為version已經變成2了,為了保證介面冪等性,介面可以直接返回成功,因為version值已經修改了,那麼前面必定已經成功過一次,後面都是重複的請求。
總結
- 網路延遲問題:先發的不一定先到
- 資料庫操作延遲:先到的不一定先執行完
- 不能依賴上游或下游去做防重冪等,自己本身也要把控好
- 對於外部介面,沒有明確返回可重試狀態的,不要輕易重試