分佈式系列---冪等性實現方案解析

什麼是冪等

冪等(idempotence)一詞原爲數學上的概念,用一個最直觀的數學式子表達爲:

f(f(x)) = f(x)

對應到軟件開發領域,即爲同樣的請求被執行一次與連續執行多次的效果是一樣的,服務器的狀態也是一樣的,實際上就是接口的可重複調用(包括時間和空間上兩個維度)。
不是要求返回值完全相同,而且是指後續多餘的調用對系統的數據一致性不造成破壞。對於寫入類操作,如果第一次寫入是成功的,後續的寫入應該拋出異常或者空操作,或者執行了寫入但是未對數據造成變化。對於讀取類操作,需要保證其實現上是真正的讀取,不能在讀操作中夾帶寫操作。

實現方案

簡單場景

查詢select

查詢數據時,無論是查詢單條數據,還是查詢多條數據,數據返回結果都不會變,select是天然的冪等操作。

刪除delete

刪除操作也是冪等的,無論刪除一條,還是刪除多條數據,目的是將數據刪除,當被刪除當數據再次執行,結果也是一樣的。

唯一索引/主鍵

防止新增髒數據
當設置要新增的字段爲唯一索引,或這個字段與另外的字段是組合索引時,當請求參數,新增字段一致時,數據庫會通過索引機制視爲失效。

例如:數據來源方調用API進行同步數據,必須傳遞sourceId(來源)、seq(來源序列號),API提供方,在接收數據之後用這兩個字段作爲聯合索引插入到數據庫,每次調用API的時候,先用這兩個字段進行校驗,保證不重複,這裏來源序列號的生成方式可參考雪花算法或者其它唯一主鍵生成算法。

token機制

相同請求只允許提交一次。

實現思路
集羣環境:
採用token加Redis(redis單線程,處理需要排隊)。
單JVM進程:
採用token加redis或token加jvm內存。
實現流程:
主要使用RedisLock —— redis.setnx函數,所以我們要讓請求生成統一的redisKey來存儲,在這裏,我們認爲token即是redisKey。

首次請求服務器時,服務器根據請求參數生成唯一的token(redisKey)
將這個唯一的key保存在redis或者jvm內存中(在使用JVM內存情況下),並設置key有效時間(根據業務設計)
服務器再次接收請求時,生成規則不變(根據請求參數生成唯一的redisKey),然後先根據redisKey刪除redis中的對象,如果刪除成功代表請求內容相同。
上面的例子適用兩次請求,當發生3次併發,刪除會失敗,然後又創建一個相同的線程進行數據持久化處理。

select + insert/update

併發不高的後臺系統,或一些任務調度系統(job),可以先查詢,在更新操作,以此完成業務操作。
注意:高併發場景這種方式並不適用。

複雜場景

悲觀鎖

select - for update
示例:

select column_1,column_2....column_N from table where id = 'xxx' for update;

注意:id一定要是主鍵或者唯一索引,否則鎖表會發生錯誤!!!!

悲觀鎖使用時一般伴隨事務一起使用,數據鎖定時間可能會很長,實際根據業務設定

樂觀鎖MVCC(多版本併發控制)

樂觀鎖相對於悲觀鎖效率更高,因爲樂觀鎖只有在更新表的時候會鎖表,其他時候不會鎖表,所以效率更高。

樂觀鎖實現方式
1. 通過version

UPDATE TABLE_NAME SET NAME=#name#, VERSION=VERSION+1 WHERE VERSION =#version#

2. 通過條件限制

UPDATE TABLE_NAME SET AVAI_AMOUNT=AVAI_AMOUNT-#SUBAMOUNT# WHERE AVAI_AMOUNT-#SUBAMOUNT# >= 0

條件AVAI_AMOUNT-#SUBAMOUNT# >= 0 ,這個情景,適用於不用版本號,只更新做數據安全校驗,適合校驗庫存模型(因爲庫存不會小於等於0)、扣分額、回滾份額等,這個效率更高。
注意 : 樂觀鎖更新操作,最好用主鍵或者唯一索引來更新,這樣是行鎖,否則更新時會鎖表
根據“注意”中的提示,上面的兩條sql,可優化成下面的樣子

-- 添加where id=
UPDATE TABLE_NAME SET NAME=#name#, VERSION=VERSION+1 WHERE ID=#id# AND VERSION =#version#
-- 添加where id=
UPDATE TABLE_NAME SET AVAI_AMOUNT=AVAI_AMOUNT-#subAmount# WHERE ID=#id# AND  AVAI_AMOUNT-#subAmount# >= 0

分佈式鎖

可參考分佈式事務所的實現和設計思想,分佈式鎖在分佈式多進程環境下時,會生成一個唯一“key”(redis和zookeeper都可實現),後續相同操作這個鎖是不一致的,在比較長業務扭轉中(處理業務較多的場景時),分佈式鎖可以鎖住一整套業務對數據的更新操作,然後提交,失敗回滾。

分佈式鎖的具體實現方案,參見如下:

框架精粹系列3---分佈式鎖:ZooKeeper實現原理刨析

框架精粹系列6---分佈式鎖:Redis分佈式鎖原理分析

框架精粹系列9---分佈式鎖:Mysql實現分佈式鎖原理分析

去重表

這是利用數據庫表單的特性來實現冪等。以訂單請求支付場景爲例:
將訂單號orderId設爲去重表的唯一索引,每次請求支付都根據訂單號向去重表中插入一條數據,只有插入成功才繼續執行支付操作,相當於在事務的開始階段加鎖。
考慮兩種失敗的情況:

  1. Insert去重表失敗,事務回滾,無任何影響;
  2. Insert去重表成功,支付業務操作失敗,事務回滾,刪除之前插入去重表的記錄,無任何影響;

以上兩種失敗的情況下,事務的冪等性是可以保持的,避免了單個訂單同時多次進行支付的情況。
下圖爲該支付場景下的時序圖:
圖1-1:支付場景時序圖

參考

冪等的多種實現方式

冪等的實現方案

發佈了375 篇原創文章 · 獲贊 150 · 訪問量 117萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章