目錄
2、冪等性主要場景有哪些?---微服務+MQ+用戶交互+第三方接口(支付)
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)選擇自身業務場景適合的解決方案,而不要去硬套一些現成的技術實現,無論是組合還是創新,要記住適合的纔是最好的。