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