佛山網(wǎng)站建設公司88百度的鏈接
【分布式】: 冪等性和實現(xiàn)方式
冪等(idempotent、idempotence)是一個數(shù)學與計算機學概念,
常見于抽象代數(shù)中。在編程中一個冪等操作的特點是其任意多次執(zhí)行所產(chǎn)生的影響均與一次執(zhí)行的影響相同。冪等函數(shù),或冪等方法,是指可以使用相同參數(shù)重復執(zhí)行,并能獲得相同結果的函數(shù)。這些函數(shù)不會影響系統(tǒng)狀態(tài),也不用擔心重復執(zhí)行會對系統(tǒng)造成改變。例如,“setTrue()”函數(shù)就是一個冪等函數(shù),無論多次執(zhí)行,其結果都是一樣的.更復雜的操作冪等保證是利用唯一交易號(流水號)實現(xiàn)。
編程世界
所謂的冪等性,是分布式環(huán)境下的一個常見問題,一般是指我們在進行多次操作時,所得到的結果是一樣的,即多次運算結果是一致的。
冪等:任意多次執(zhí)行所產(chǎn)生的影響均與一次執(zhí)行的影響相同,這是冪等性的核心特點。
任意多次執(zhí)行所產(chǎn)生的影響均與一次執(zhí)行的影響相同,這是冪等性的核心特點。其實在我們編程中主要操作就是CURD,其中讀取(Retrieve)操作和刪除(Delete)操作是天然冪等的,受影響的就是創(chuàng)建(Create)、更新(Update)。
也就是說,用戶對于同一操作,無論是發(fā)起一次請求還是多次請求,最終的執(zhí)行結果是一致的,不會因為多次點擊而產(chǎn)生副作用。
什么是接口冪等性?
在HTTP/1.1
中,對冪等性進行了定義。它描述了一次和多次請求某一個資源對于資源本身應該具有同樣的結果(網(wǎng)絡超時等問題除外),即第一次請求的時候?qū)Y源產(chǎn)生了副作用,但是以后的多次請求都不會再對資源產(chǎn)生副作用。
這里的副作用是不會對結果產(chǎn)生破壞或者產(chǎn)生不可預料的結果。也就是說,其任意多次執(zhí)行對資源本身所產(chǎn)生的影響均與一次執(zhí)行的影響相同。
什么情況下會產(chǎn)生重復提交( 非冪等性 )
以下幾種情況會導致非冪等性的結果出現(xiàn):
- 連續(xù)點擊提交兩次按鈕;
- 點擊刷新按鈕;
- 使用瀏覽器后退按鈕重復之前的操作,導致重復提交表單;
- 使用瀏覽器歷史記錄重復提交表單;
- 瀏覽器重復地HTTP請求等
為什么需要實現(xiàn)冪等性
在接口調(diào)用時一般情況下都能正常返回信息不會重復提交,不過在遇見以下情況時可以就會出現(xiàn)問題,如:
-
前端重復提交表單:在填寫一些表格時候,用戶填寫完成提交,很多時候會因網(wǎng)絡波動沒有及時對用戶做出提交成功響應,致使用戶認為沒有成功提交,然后一直點提交按鈕,這時就會發(fā)生重復提交表單請求。
-
用戶惡意進行刷單:例如在實現(xiàn)用戶投票這種功能時,如果用戶針對一個用戶進行重復提交投票,這樣會導致接口接收到用戶重復提交的投票信息,這樣會使投票結果與事實嚴重不符。
-
接口超時重復提交:很多時候 HTTP 客戶端工具都默認開啟超時重試的機制,尤其是第三方調(diào)用接口時候,為了防止網(wǎng)絡波動超時等造成的請求失敗,都會添加重試機制,導致一個請求提交多次。
-
消息進行重復消費:當使用 MQ 消息中間件時候,如果發(fā)生消息中間件出現(xiàn)錯誤未及時提交消費信息,導致發(fā)生重復消費。
使用冪等性最大的優(yōu)勢在于使接口保證任何冪等性操作,免去因重試等造成系統(tǒng)產(chǎn)生的未知的問題。
冪等也有不好的地方
冪等性是為了簡化客戶端邏輯處理,能放置重復提交等操作,但卻增加了服務端的邏輯復雜性和成本,其主要是:
-
把并行執(zhí)行的功能改為串行執(zhí)行,降低了執(zhí)行效率。
-
增加了額外控制冪等的業(yè)務邏輯,復雜化了業(yè)務功能;
所以在使用時候需要考慮是否引入冪等性的必要性,根據(jù)實際業(yè)務場景具體分析,除了業(yè)務上的特殊要求外,一般情況下不需要引入的接口冪等性。
Restful API 接口冪等性如何?
http method | 冪等 | 說明 |
---|---|---|
GET | ?? | 查詢讀讀操作,不修改數(shù)據(jù),一般都是冪等 |
POST | ? | 新增資源,每次都是新增。 |
PUT | - | 1. 根據(jù)某個值更新,是冪等。 2. 做累加操作更新等,非冪等 |
DELETE | - | 1. 根據(jù)唯一值刪除,是冪等 2. 根據(jù)條件刪除,非冪等 |
實現(xiàn)冪等的方案
方案 | 適用方法 | 實現(xiàn)復雜度 | 缺點 |
---|---|---|---|
數(shù)據(jù)庫唯一主鍵 | 插入操作 刪除操作 | 簡單 | - 只能用于插入操作 - 只能用于存在唯一主鍵場景 |
數(shù)據(jù)庫樂觀鎖 | 更新操作 | 簡單 | - 只能更新操作 - 表中新增字段 |
請求序列號 | 插入操作 刪除操作 更新操作 | 簡單 | - 需要保證下游生成唯一序號 - 需要redi第三方存儲生成token |
防重Token令牌 | 插入操作 刪除操作 更新操作 | 適中 | 需要redi第三方存儲生成token |
方案原理講解
數(shù)據(jù)庫唯一主鍵實現(xiàn)冪等性
數(shù)據(jù)庫唯一主鍵的實現(xiàn)主要是利用數(shù)據(jù)庫中主鍵唯一約束的特性,一般來說唯一主鍵比較適用于“插入”時的冪等性,其能保證一張表中只能存在一條帶該唯一主鍵的記錄。
使用數(shù)據(jù)庫唯一主鍵完成冪等性時需要注意的是,該主鍵一般來說并不是使用數(shù)據(jù)庫中自增主鍵,而是使用分布式 ID 充當主鍵,這樣才能能保證在分布式環(huán)境下 ID 的全局唯一性。
關鍵步驟:需要生成全局唯一主鍵 ID
主要流程如下:
-
客戶端執(zhí)行創(chuàng)建請求,調(diào)用服務端接口。
-
服務端執(zhí)行業(yè)務邏輯,生成一個分布式
ID
,將該 ID 充當待插入數(shù)據(jù)的主鍵,然 后執(zhí)數(shù)據(jù)插入操作,運行對應的SQL
語句。 -
服務端將該條數(shù)據(jù)插入數(shù)據(jù)庫中,如果插入成功則表示沒有重復調(diào)用接口。如果拋出主鍵重復異常,則表示數(shù)據(jù)庫中已經(jīng)存在該條記錄,返回錯誤信息到客戶端。
數(shù)據(jù)庫樂觀鎖實現(xiàn)冪等性
數(shù)據(jù)庫樂觀鎖方案一般只能適用于執(zhí)行更新操作的過程,我們可以提前在對應的數(shù)據(jù)表中多添加一個字段,充當當前數(shù)據(jù)的版本標識。
這樣每次對該數(shù)據(jù)庫該表的這條數(shù)據(jù)執(zhí)行更新時,都會將該版本標識作為一個條件,值為上次待更新數(shù)據(jù)中的版本標識的值。
關鍵步驟: 需要數(shù)據(jù)庫對應業(yè)務表中添加額外字段
為了每次執(zhí)行更新時防止重復更新,確定更新的一定是要更新的內(nèi)容,我們通常都會添加一個 version
字段記錄當前的記錄版本,這樣在更新時候?qū)⒃撝祹?#xff0c;那么只要執(zhí)行更新操作就能確定一定更新的是某個對應版本下的信息。
這樣每次執(zhí)行更新時候,都要指定要更新的版本號,如下操作就能準確更新 version=5
的信息:
UPDATE my_table SET price=price+50,version=version+1 WHERE id=1 AND version=5
上面 WHERE
后面跟著條件 id=1 AND version=5
被執(zhí)行后,id=1
的 version
被更新為 6
,所以如果重復執(zhí)行該條 SQL 語句將不生效,因為 id=1 AND version=5
的數(shù)據(jù)已經(jīng)不存在,這樣就能保住更新的冪等,多次更新對結果不會產(chǎn)生影響。
防重 Token 令牌實現(xiàn)冪等性
針對客戶端連續(xù)點擊或者調(diào)用方的超時重試等情況,例如提交訂單,此種操作就可以用
Token
的機制實現(xiàn)防止重復提交。
簡單的說就是調(diào)用方在調(diào)用接口的時候先向后端請求一個全局
ID(Token)
,請求的時候攜帶這個全局ID
一起請求(Token
最好將其放到Headers
中),后端需要對這個Token
作為Key
,用戶信息作為Value
到Redis
中進行鍵值內(nèi)容校驗,如果Key
存在且Value
匹配就執(zhí)行刪除命令,然后正常執(zhí)行后面的業(yè)務邏輯。如果不存在對應的Key
或Value
不匹配就返回重復執(zhí)行的錯誤信息,這樣來保證冪等操作。
NOTE : 獲取Token只做一次,而非每次調(diào)用資源請求時都獲取。
獲取一次TOKEN可以隨頁面加載的時候去獲取,或調(diào)用資源接口前獲取一次。
在資源接口一次或多次請求時,使用的token是一樣的。如果刷新界面或切換資源id,可以重新獲取token
關鍵步驟:
需要生成全局唯一
Token
串
需要使用第三方組件Redis
進行數(shù)據(jù)效驗
-
服務端提供獲取 Token 的接口,該 Token 可以是一個序列號,也可以是一個分布式
ID
或者UUID
串。 -
客戶端調(diào)用接口獲取 Token,這時候服務端會生成一個 Token 串。
-
然后將該串存入 Redis 數(shù)據(jù)庫中,以該 Token 作為 Redis 的鍵(注意設置過期時間)。
-
將 Token 返回到客戶端,客戶端拿到后應存到表單隱藏域中。
-
客戶端在執(zhí)行提交表單時,把 Token 存入到
Headers
中,執(zhí)行業(yè)務請求帶上該Headers
。 -
服務端接收到請求后從
Headers
中拿到 Token,然后根據(jù) Token 到 Redis 中查找該key
是否存在。 -
服務端根據(jù) Redis 中是否存該
key
進行判斷,如果存在就將該key
刪除,然后正常執(zhí)行業(yè)務邏輯。如果不存在就拋異常,返回重復提交的錯誤信息。
注意,在并發(fā)情況下,執(zhí)行 Redis 查找數(shù)據(jù)與刪除需要保證原子性,否則很可能在并發(fā)下無法保證冪等性。其實現(xiàn)方法可以使用分布式鎖或者使用
Lua
表達式來注銷查詢與刪除操作。
下游傳遞唯一序列號實現(xiàn)冪等性
所謂請求序列號,其實就是每次向服務端請求時候附帶一個短時間內(nèi)唯一不重復的序列號,該序列號可以是一個有序
ID
,也可以是一個訂單號,一般由下游生成,在調(diào)用上游服務端接口時附加該序列號和用于認證的ID
。
當上游服務器收到請求信息后拿取該 序列號 和下游 認證ID 進行組合,形成用于操作 Redis 的Key
,然后到 Redis 中查詢是否存在對應的Key
的鍵值對,根據(jù)其結果:
- 如果存在,就說明已經(jīng)對該下游的該序列號的請求進行了業(yè)務處理,這時可以直接響應重復請求的錯誤信息。
- 如果不存在,就以該
Key
作為 Redis 的鍵,以下游關鍵信息作為存儲的值(例如下游商傳遞的一些業(yè)務邏輯信息),將該鍵值對存儲到 Redis 中 ,然后再正常執(zhí)行對應的業(yè)務邏輯即可。
關鍵步驟:
-
要求第三方傳遞唯一序列號;
-
需要使用第三方組件 Redis 進行數(shù)據(jù)效驗;
- 下游服務生成分布式
ID
作為序列號,然后執(zhí)行請求調(diào)用上游接口,并附帶唯一序列號與請求的認證憑據(jù)ID。 - 上游服務進行安全效驗,檢測下游傳遞的參數(shù)中是否存在序列號和憑據(jù)ID。
- 上游服務到 Redis 中檢測是否存在對應的序列號與認證ID組成的
Key
,如果存在就拋出重復執(zhí)行的異常信息,然后響應下游對應的錯誤信息。如果不存在就以該序列號和認證ID組合作為Key
,以下游關鍵信息作為Value
,進而存儲到 Redis 中,然后正常執(zhí)行接來來的業(yè)務邏輯。
“
上面步驟中插入數(shù)據(jù)到 Redis 一定要設置過期時間。這樣能保證在這個時間范圍內(nèi),如果重復調(diào)用接口,則能夠進行判斷識別。如果不設置過期時間,很可能導致數(shù)據(jù)無限量的存入 Redis,致使 Redis 不能正常工作。
實現(xiàn)
基于 防重Token令牌方案 實現(xiàn)
代碼倉庫:
無難事者若執(zhí) / Spring Utils · GitCode