分佈式冪等性問題的詳細剖析

目錄

1、何爲冪等性?---多次調用,對資源的影響一樣

2、冪等性主要場景有哪些?---微服務+MQ+用戶交互+第三方接口(支付)

3、冪等性的作用是什麼?

1)查詢

2)新增

3)修改

4)刪除

4、如何解決冪等性問題?

1)控制重複請求-控制操作次數+及時重定向

2)過濾重複動作---分佈式鎖+token令牌+緩衝隊列

3)解決重複寫---樂觀鎖+唯一約束+悲觀鎖(for update)

後記


1、何爲冪等性?---多次調用,對資源的影響一樣

冪等(idempotence),來源於數學中的一個概念,例如:冪等函數/冪等方法(指用相同的參數重複執行,並能獲得相同結果的函數,這些函數不影響系統狀態,也不用擔心重複執行會對系統造成改變)。

簡單理解即:多次調用對系統的產生的影響是一樣的,即對資源的作用是一樣的

冪等性

冪等性強調的是外界通過接口對系統內部的影響, 只要一次或多次調用對某一個資源應該具有同樣的副作用就行。

注意:這裏指對資源造成的副作用必須是一樣的,但是返回值允許不同!

 

2、冪等性主要場景有哪些?---微服務+MQ+用戶交互+第三方接口(支付)

根據上面對冪等性的定義我們得知:產生重複數據或數據不一致,這個絕大部分是由於發生了重複請求。這裏的重複請求是指同一個請求在一些情況下被多次發起。

導致這個情況會有哪些場景呢?

1)微服務架構下,不同微服務間會有大量的基於http,rpc或者mq消息的網絡通信,會有第三個情況【未知】,也就是超時。如果超時了,微服務框架會進行重試。

2)用戶交互的時候多次點擊,無意地觸發多筆交易。

3)MQ消息中間件,消息重複消費

4)第三方平臺的接口(如:支付成功回調接口),因爲異常也會導致多次異步回調

5)其他中間件/應用服務根據自身的特性,也有可能進行重試。

3、冪等性的作用是什麼?

冪等性主要保證多次調用對資源的影響是一致的

在闡述作用之前,我們利用資源處理應用來說明一下:

HTTP與數據庫的CRUD操作對應: 

   PUT :CREATE

   GET :READ 

   POST :UPDATE

   DELETE :DELETE

(其實不光是數據庫,任何數據如文件圖表都是這樣)

1)查詢

SELECT * FROM users WHERE xxx;

不會對數據產生任何變化,天然具備冪等性。

2)新增

INSERT INTO users (user_id, name) VALUES (1, 'zhangsan');

case1:帶有唯一索引(如:`user_id`),重複插入會導致後續執行失敗,具有冪等性;

case2:不帶有唯一索引,多次插入會導致數據重複,不具有冪等性。

3)修改

case1:直接賦值,不管執行多少次score都一樣,具備冪等性。

UPDATE users SET score = 30 WHERE user_id = 1;

case2:計算賦值,每次操作score數據都不一樣,不具備冪等性。

UPDATE users SET score = score + 30 WHERE user_id = 1;

4)刪除

case1:絕對值刪除,重複多次結果一樣,具備冪等性。

DELETE FROM users WHERE id = 1;

case2:相對值刪除,重複多次結果不一致,不具備冪等性。

DELETE top(3) FROM users;

總結:通常只需要對寫請求(新增&更新)作冪等性保證

4、如何解決冪等性問題?

我們在網上搜索冪等性問題的解決方案,會有各種各樣的解法,但是如何判斷哪種解決方案對於自己的業務場景是最優解,這種情況下,就需要我們抓問題本質。

經過以上分析,我們得到了解決冪等性問題就是要控制對資源的寫操作

我們從問題各個環節流程來分析解決:

 

1)控制重複請求-控制操作次數+及時重定向

控制動作觸發源頭,即前端做冪等性控制實現

相對不太可靠,沒有從根本上解決問題,僅算作輔助解決方案。

主要解決方案:

  • 控制操作次數,例如:提交按鈕僅可操作一次(提交動作後按鈕置灰)

  • 及時重定向,例如:下單/支付成功後跳轉到成功提示頁面,這樣消除了瀏覽器前進或後退造成的重複提交問題。

2)過濾重複動作---分佈式鎖+token令牌+緩衝隊列

控制過濾重複動作,是指在動作流轉過程中控制有效請求數量。

(a)分佈式鎖

利用Redis記錄當前處理的業務標識,當檢測到沒有此任務在處理中,就進入處理,否則判爲重複請求,可做過濾處理。

訂單發起支付請求,支付系統會去Redis緩存中查詢是否存在該訂單號的Key,如果不存在,則向Redis增加Key爲訂單號。查詢訂單支付已經支付,如果沒有則進行支付,支付完成後刪除該訂單號的Key。通過Redis做到了分佈式鎖,只有這次訂單訂單支付請求完成,下次請求才能進來。

分佈式鎖相比去重表,將放併發做到了緩存中,較爲高效。思路相同,同一時間只能完成一次支付請求。

(b)token令牌

應用流程如下:

1)服務端提供了發送token的接口。執行業務前先去獲取token,同時服務端會把token保存到redis中;

2)然後業務端發起業務請求時,把token一起攜帶過去,一般放在請求頭部;

3)服務器判斷token是否存在redis中,存在即第一次請求,可繼續執行業務,執行業務完成後將token從redis中刪除;

4)如果判斷token不存在redis中,就表示是重複操作,直接返回重複標記給client,這樣就保證了業務代碼不被重複執行。

 

(c)緩衝隊列

把所有請求都快速地接下來,對接入緩衝管道。後續使用異步任務處理管道中的數據,過濾掉重複的請求數據。

優點:同步轉異步,實現高吞吐。

缺點:不能及時返回處理結果,需要後續監聽處理結果的異步返回數據。

  

3)解決重複寫---樂觀鎖+唯一約束+悲觀鎖(for update)

實現冪等性常見的方式有:悲觀鎖(for update)、樂觀鎖、唯一約束。

1)悲觀鎖(Pessimistic Lock)

簡單理解就是:假設每一次拿數據,都有認爲會被修改,所以給數據庫的行或表上鎖。

當數據庫執行select for update時會獲取被select中的數據行的行鎖,因此其他併發執行的select for update如果試圖選中同一行則會發生排斥(需要等待行鎖被釋放),因此達到鎖的效果。

select for update獲取的行鎖會在當前事務結束時自動釋放,因此必須在事務中使用。(注意for update要用在索引上,不然會鎖表)

START TRANSACTION; # 開啓事務

SELETE * FROM users WHERE id=1 FOR UPDATE;

UPDATE users SET name= 'xiaoming' WHERE id = 1;

COMMIT; # 提交事務

2)樂觀鎖(Optimistic Lock)

簡單理解就是:就是很樂觀,每次去拿數據的時候都認爲別人不會修改。更新時如果version變化了,更新不會成功。

不過,樂觀鎖存在失效的情況,就是常說的ABA問題,不過如果version版本一直是自增的就不會出現ABA的情況。

UPDATE users SET name='xiaoxiao', version=(version+1) WHERE id=1 AND version=version;

缺點:就是在操作業務前,需要先查詢出當前的version版本

另外,還存在一種:狀態機控制

例如:支付狀態流轉流程:待支付->支付中->已支付。具有一定要的前置要求的,嚴格來講,也屬於樂觀鎖的一種。

3)唯一約束

常見的就是利用數據庫唯一索引或者全局業務唯一標識(如:source+序列號等)。

這個機制是利用了數據庫的主鍵唯一約束的特性,解決了在insert場景時冪等問題。但主鍵的要求不是自增的主鍵,這樣就需要業務生成全局唯一的主鍵,

全局ID生成方案:

  • UUID:結合機器的網卡、當地時間、一個隨記數來生成UUID;

  • 數據庫自增ID:使用數據庫的id自增策略,如 MySQL 的 auto_increment。

  • Redis實現:通過提供像 INCR 和 INCRBY 這樣的自增原子命令,保證生成的 ID 肯定是唯一有序的。

  • 雪花算法-Snowflake:由Twitter開源的分佈式ID生成算法,以劃分命名空間的方式將 64-bit位分割成多個部分,每個部分代表不同的含義。

小結:按照應用上的最優收益,推薦排序爲:樂觀鎖 > 唯一約束 > 悲觀鎖

後記

1)冪等性處理 雖然複雜了業務處理,也可能會降低接口的執行效率,但是爲了保證系統數據的準確性,是非常有必要的;

2)遇到問題,善於發現並挖掘本質問題,這樣解決起來才能高效且精準;

3)選擇自身業務場景適合的解決方案,而不要去硬套一些現成的技術實現,無論是組合還是創新,要記住適合的纔是最好的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章