冪等性設計和解決方案

什麼是冪等性

(一)介紹

冪等,英文Idempotence
冪等這個詞原自數學,冪等性是數學中的一個概念,常見於抽象代數中.表達的是N次變換與1次變換的結果相同;
簡單來說就是如果方法調用一次和多次產生的效果是相同的,它就具有冪等性.
冪等函數或冪等方法,是指可以使用相同參數重複執行,並能得相同結果的函數,這些函數不會影響系統狀態,也不用擔心重複執行會對系統造成改變.
冪等性(ldempotence)本身是一個數學概念.在計算機的各個領域都有涉及和借用

(二)HTTP維度

在 HTTP/1.1規範中冪等性的定義是:
Methods can also have the property of"idempotence"in that(aside from error or expiration issues)the side-effects of N>o identical requests is the same as for a single request.
從定義上看,HTTP方法的冪等性是指一次和多次請求某一個資源應該具有同樣的副作用.
HTTP請求常見有 GET、DELETE、PUT、POST 四種主要方法;

(1)GET方法

HTTP GET方法用於獲取資源,不應有副作用,所以是冪等的.
比如:GET https//ww.XXXX.com/categories/100不會改變資源的狀態,不論調用一次1還是N 次都沒有副作用.
請注意,這裏強調的是一次和N次具有相同的副作用,而不是每次GET的結果相同.
GET htps//wwww.XXXX.com/categories這個HTTIP請求可能會每次得到不同的結果,但它本身並沒有產生任何副作用,因而是滿足冪等性的.

(2)DELETE方法

HTTP DELETE方法用於刪除資源,有副作用,但它應該滿足冪等性.
比如:DELETE https;//www.XXX.com/article/detail/10,調用一次和N次對系統產生 的副作用是相同的,即刪掉id爲10的帖子,因此調用者可以多次調用或刷新頁面而不必擔心引起錯誤.

(3)Post方法

HTTP POST所對應的URI爲資源的接收者.
比如:
POST
https//wwww.XXX.com/article 的語義是在https://www.XXXX.com/article 下發表一篇文章,兩次相同的POST請求會在服務器端創建兩份資源,所以POST方法不具備冪等性.

(4)PUT方法

HTTP PUT所對應的 URI是要創建或更新的資源.
比如:PUT https//www.XXX.com/article/5231的語義是創建或更新ID爲5231的文章,對同一URI進行多次PUT的副作用和一次PUT是相同的,因此PUT方法具有冪等性.

以上是主要針對RESTful風格的HTTP冪等性討論;
我們知道,HTTP協議是一種面向資源的應用層協議,但對HTTP協議的應用存在兩種不同的方式:

第一種是RESTful的,它把HTTP當成應用層協議,遵守HTTP協議的各種規定;

另一種是在HTTP協議之上封裝我們的 RPC,沒有完全把HTTP當成應用層協議,而是把HTTP協議作爲了傳輸層協議,然後在HTTP之上建立了自己的應用層協議.
那麼拋開HTTP協議的規範 冪等性是分佈式系統的重要特性所以不論是RESTful的WebAPI設計還是RPC方式的其他API設計都應該考慮冪等性;

(三)應用維度

冪等性衍生到軟件工程中,它的語義是指 函數/接口可以使用相同的參數重複執行,不應該影響系統狀態,也不會對系統造成改變.
也就是任意多次執行所產生的影響均與一次執行所產生的影響相同;
如果用戶對於同一操作發起的一次請求或者多次請求的所產生的影響是一致的,不會因爲多次調用(點擊)而產生了副作用;
第一次請求的時候對資源產生了副作用,但是以後的多次請求都不會再對資源產生副作用.
這裏的副作用是指不會對結果產生破壞或者產生不可預料的結果,即冪等性=多次執行無副作用;

產生冪等性場景

冪等性問題在我們的開發中,分佈式、微服務架構中是隨處可見的:因網絡波動,可能會引起重複請求;
用戶重複操作,用戶在使用產品時可能會無意的觸發多次下單多次交易,甚至沒有響應而有意觸發多筆交易;
應用使用了失敗或超時重試機制(如Nginx重試、RPC重試或業務層重試等)
第三方平臺的接口(如:支付成功回調接口),因爲異常導致多次異步回調;中間件/應用服務根據自身的特性,也有可能進行重試.
用戶雙擊提交按鈕;
頁面重複刷新
使用瀏覽器後退按鈕重複之前的操作,導致重複提交表單;使用瀏覽器歷史記錄重複提交表單;
瀏覽器重複的HTTP 請求;
定時任務重複執行;

冪等性在那一層實現

現在都是分佈式、微服務的架構,在那一層進行冪等設計,在那一層解決冪等性問題?

在數據訪問層實現比較合適

數據訪問層的冪等性
冪等性針對分下面兩個方面:
讀請求
寫請求

讀請求需要做冪等?很顯然是不需要的

寫請求呢?涉及到需要做insert、update、delete數據庫操作的.肯定是需要的; 那我們可以得出一個結論,即不會改變數據的操作我們可以不做冪等,會改變數據的操作我們就一定要做冪等.

那我們逐個討論寫請求:insert、delete、update操作,首先我假設我沒有做任何應用層
面上的冪等操作.

(1)insert

對於 insert操作,當我重複插入數據的時候會出現什麼情況?這裏分兩種情況:
自增主鍵(有冪等性問題)
業務主鍵 (沒有冪等問題,拋異常)
比如:insert into product (id, name, type, price).
假如我的id是自增主鍵會有問題嗎?一定會有,因爲會產生多條業務數據相同主鍵不同的數據.那如果是業務主鍵呢?即我假設對 name、type、price建立唯一索引,即使我id相同,數據庫也會報錯了.
不使用自增主鍵,使用自定義的序列號,比如UUID等

(2)delete

對於delete 操作,當重複執行的時候會出現什麼情況?這裏也要分兩種情況:

相對值刪除
絕對值刪除

比如:
delete from product_info where id=1234;-幕等的
delete top(10)from product_info;–不是冪等
如果是絕對值刪除,重複操作兩次是不會出現問題的,但是如果相對值刪除,重複操作就是重複刪除多次

(3)update

對於update操作,當重複更新數據的時候會出現什麼情況?這裏其實和刪除操作是一樣,也需要分兩種情況討論:
相對值刪除
絕對值刪除
update product info set price=99 where id=1234;–冪等的
update product info set price= price+100 where id=1234;–不是冪等
如果是絕對值修改,重複操作也不會有問題,但是相對值修改,一定會有問題,?會重複修改多次.

(4)select

最後是select操作,其實這個不用討論,因爲不會對數據發生改變的操作我們不用做冪等.

(5)狹義與廣義的冪等

以上的所有討論都是基於單庫的,這是狹義上的冪等處理,但是在實際的業務場景中,比如分佈式系統中,我們的一次請求可能有多個步驟,那這種跨服務、跨事務請求的冪等處理怎麼辦呢?也就是廣義上的冪等處理怎麼辦呢?其實這個就需要分佈式事務來幹這個事;
所以廣義上的冪等處理通過分佈式事務來解決,狹義上的冪等處理,對於服務分層來說只需要在數據訪問層做冪等操作,而對於讀寫請求冪等處理,select其實我們不用處理,因爲從規範層面上來講,insert 操作你只要要求必須有唯一業務主鍵,delete 操作在實際業務上是不會被允許的,select 操作又不需要做冪等處理,那唯一需要處理的是 update 操作,但是也很簡單,就把相對值修改轉換成絕對值修改即可.

冪等性解決方案

(一)前端冪等性的實現(不是可靠的)
(1)按鈕只可操作一次

一般是提交後把按鈕置灰或loding 狀態,按鈕置灰或 loding 狀態可以用一些js 組件實現,消除用戶因爲重複點擊而產生的副作用,比如添加操作由於點擊兩次而產生兩條記錄.

(2)token機制

產品上允許重複提交,但要保證重複提交不產生副作用,比如點擊n次只產生一條記錄;具體實現就是進入頁面時申請一個token,然後後面所有的請求都帶上這個token,根據token 來免重複請求;

(3)使用Post/Redirect/Get模式

在提交後執行頁面重定向,這就是所謂的Post-Redirect-Get(PRG)模式.簡言之,當用戶提交了表單後,去執行一個客戶端的重定向,轉到提交成功信息頁面,這樣避免用戶按F5刷新導致的重複提交,而其也不會出現瀏覽器表單重複提交的警告,也能消除按瀏覽器前進和後退按導致的同樣重複提交的問題;

(4)在 Session 中存放特殊標誌

在服務器端,生成一個唯一的標識符,將它存入 session,同時將它寫入表單的隱藏中,然後將表單頁面發給瀏覽器,用戶輸入信息後點擊提交,在服務器端,獲取表單中隱建字段的值與session中的唯一標識符比較 相等說明是首次提交 就處理本次請求 然後將session中的唯一標識符移除,不相等則表示是重複提交,不再做處理;

(二)後端冪等性的實現
(1)使用唯一索引防止新增髒數據

此方案可以限制重複插入數據,當數據重複時,插入數據庫會拋異常,保證不會出現髒數據,這也是一種簡單粗暴的辦法;

(2)Token+Redis的冪等方案

這種方式分成兩個階段:申請 token 階段和業務操作階段.
以支付爲例:
第一階段,在進入到提交訂單頁面之前,需要訂單系統根據用戶信息向支付系統發起一次申請token的請求工支付系統將token 保存到Redis 緩存中,爲第二階段支付使用.
第二階段,訂單系統拿着申請到的 token發起支付請求,支付系統會檢查Redis 中是否存在該token,如果存在,表示第一次發起支付請求,開始支付邏輯處理,處理完邏輯後冊刪除redis 中的 token
當重複請求時候,檢查緩存中token不存在,表示非法請求.
該方案的不足之處是需要與系統間交互兩次;

(3)狀態機冪等

針對更新操作,比如業務上需要修改訂單狀態,訂單有待支付、支付中、支付成功、支付失敗、訂單超時關閉等,在設計的時候最好只支持狀態的單向改變(不可逆),這樣在更新的時候where條件裏可以加上 status = 我期望的原來的 status,多次調用的話實際上也只會執行次.
失敗、訂單超時關閉等,在設計的時候最好只支持狀態的單向改變(不可逆),這樣在更新的時候where條件裏可以加上 status = 我期望的原來的 status,多次調用的話實際上也只會執行次.
Update x set xx=xx where status='待支付’and id=XXX

(4)樂觀鎖實現冪等

如果更新已有數據,可以進行加鎖更新,也可以設計表結構時使用樂觀鎖,通過 version來做樂觀鎖,這樣既能保證執行效率,又能保證冪等.樂觀鎖的 version 版本在更新業務數據要自增,
1、查詢數據,得到版本號;version=1
2、通過版本號去更新,版本號匹配就更新,版本號不匹配就不能更新;update XXX set money= money-99, version= version++1 where id= xx and version=1;

也可以採用 update with condition,更新帶條件,實現樂觀鎖,通過 version 或者其他條件來實現樂觀鎖
update table xxx set quality=quality-#subQuality#,version=version-1 where id=xx and version=version
帶條件的樂觀鎖:
update table xx set quality=quality-#subQuality# where quality-#subQual

(5)防重表實現冪等性

需要增加一個表,這個表叫做防重表(防止數據重複的表)

使用唯一主鍵去做防重表的唯一索引,比如使用訂單號 orderNo 做爲防重表的唯一索引,每次請求都根據訂單號向去重表中插入一條數據,第一次請求查詢訂單支付狀態,當然訂單沒有支付,進行支付操作,支付前先向防重表中插入該支付的訂單號,插入成功說明可以支忖,無論成功與否,執行完後更新訂單狀態爲成功或其他狀態,或者是失敗,然後可以刪除去重表中的數據.後續的訂單因爲表中唯一索引而插入失敗,則返回操作失敗,直到第一次的請求操作完成(成功或失敗),可以看出防重表作用是加鎖的功能;
在這裏插入圖片描述

(6)select + insert

該方案就是操作之前先查詢一下,符合要求再插入,該方案在沒有併發的系統中可以解決冪等問題,在單JVM有併發的時候可以JVM加鎖來保證冪等性,在分佈式環境它是無法保證冪等性,可以使用分佈式鎖來保證.

(7)分佈式鎖保證冪等性

在進入方法時,先去獲取鎖,假如獲取到鎖,就繼續後面的流程.假如沒有獲取到鎖,就等待鎖的釋放直到獲取到鎖,當執行完方法時,釋放鎖,當然,鎖要設個超時時間,防止意外沒有釋放到鎖,它可以用來解決分佈式系統的冪等性;
常用的分佈式鎖實現方案是 redis 和zookeeper 等工具.
使用分佈式鎖類似於防重表,將防重併發放到了緩存中,較爲高效.思路相同,同一時間只能完成一次支付請求.

(8)緩衝隊列

將請求都快速地接收下來,放入緩衝隊列,後續使用異步任務處理隊列中的數據,過濾掉重
復的情求,此方案優點是同步改爲異步處理,高吞吐,不足是不能及時地返回請求結果,需 要後續輪詢處理結果.

(9)全局唯一號實現冪等

比如通過source來源+seq序列號來判斷請求是否重複,在併發時只能理一個請求,其它相同併發請求要麼返回請求重複,要麼等待前面請求執行完成在執行;

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