
50cad3c088554ab6b7f621a4fb2e0ba3.jpg
什么是冪等性
-
冪等性定義:
- 一次和多次請求某一個資源對于資源本身應(yīng)該具有同樣的結(jié)果
- 任意多次執(zhí)行對資源本身所產(chǎn)生的影響均與一次執(zhí)行的影響相同
-
冪等性定義的幾個重點(diǎn):
-
冪等不僅僅只是一次或者多次請求對資源沒有副作用
- 比如,查詢數(shù)據(jù)庫操作,沒有增刪改,無論多少次操作對數(shù)據(jù)庫都沒有任何影響
- 冪等還包括第一次請求的時候?qū)Y源產(chǎn)生了副作用,但是以后的多次請求都不會再對資源產(chǎn)生副作用
- 冪等關(guān)注的是以后多次請求是否對資源產(chǎn)生副作用,并不關(guān)注結(jié)果
- 網(wǎng)絡(luò)超時等問題,不是冪等的討論范圍
-
冪等不僅僅只是一次或者多次請求對資源沒有副作用
- 冪等性是系統(tǒng)服務(wù)對外一種承諾,而不是實(shí)現(xiàn)
- 承諾只要調(diào)用接口成功,外部多次調(diào)用對系統(tǒng)的影響是一致的
- 聲明為冪等的服務(wù)會認(rèn)為外部調(diào)用失敗是常態(tài),并且失敗后必然會有重試
冪等性的使用場景
-
業(yè)務(wù)開發(fā)中,經(jīng)常遇到重復(fù)提交的情況:
- 由于網(wǎng)絡(luò)問題無法收到請求結(jié)果而重新發(fā)起請求
- 前端的操作抖動而造成的重復(fù)提交的情況
-
在交易系統(tǒng)中,支付系統(tǒng)這種重復(fù)提交造成的問題尤為明顯:
- 用戶在APP上連續(xù)點(diǎn)擊多次提交訂單,后臺應(yīng)該只產(chǎn)生一個訂單
- 向支付系統(tǒng)發(fā)起請求,由于網(wǎng)絡(luò)問題或者系統(tǒng)Bug問題導(dǎo)致重發(fā),支付系統(tǒng)應(yīng)該只做一次扣除操作
- 聲明冪等的服務(wù)認(rèn)為,外部調(diào)用者會存在多次調(diào)用的情況,為了防止外部多次調(diào)用對系統(tǒng)的數(shù)據(jù)狀態(tài)發(fā)生多次改變,需要將服務(wù)設(shè)計(jì)為冪等
冪等和防重
- 重復(fù)提交的情況和服務(wù)冪等的初衷是不同的
- 重復(fù)提交是在第一次請求已經(jīng)成功的情況下 ,人為地進(jìn)行多次操作, 導(dǎo)致不滿足冪等要求的服務(wù)多次改變狀態(tài)
- 冪等更多使用的情況是第一次請求因?yàn)槟承┣闆r,不如超時,而導(dǎo)致不知道結(jié)果或者請求失敗的異常情況下,發(fā)起多次請求
- 冪等的目的是請求多次確認(rèn)第一次請求成功,不會因?yàn)槎啻握埱蠖霈F(xiàn)多次的狀態(tài)變化
保證冪等性的情況
- 在SQL中,有以下三種場景,只有第三種場景需要保證冪等性:
- SELECT col1 FROM tab1 WHERE col2=2 : 無論執(zhí)行多少次都不會改變狀態(tài),是天然的冪等
- UPDATE tab1 SET col1=1 WHERE col2=2 : 無論執(zhí)行成功多少次狀態(tài)都是一致的,也是冪等操作
- UPDATE tab1 SET col1=col1+1 WHERE col2=2: 每次執(zhí)行的結(jié)果都會發(fā)生變化,這種不是冪等的,要采取策略保證冪等性
設(shè)計(jì)冪等性服務(wù)
- 冪等使得客戶端邏輯處理很簡單,但是服務(wù)端邏輯會很復(fù)雜
- 滿足冪等性服務(wù)需要包含兩點(diǎn)邏輯:
- 首先去查詢上一次的執(zhí)行狀態(tài),如果沒有則認(rèn)為是第一次請求
- 在服務(wù)改變狀態(tài)的業(yè)務(wù)邏輯前保證防重復(fù)提交的邏輯
保證冪等策略
- 冪等需要通過唯一的業(yè)務(wù)單號來保證:
- 相同的業(yè)務(wù)單號,認(rèn)為是同一業(yè)務(wù)
- 使用唯一的業(yè)務(wù)單號確保:后面多次相同業(yè)務(wù)單號的處理邏輯和執(zhí)行效果是一致的
-
冪等實(shí)現(xiàn)示例-支付:
- 先查詢訂單是否支付過
- 如果已經(jīng)支付過,返回支付成功
- 如果沒有支付,則進(jìn)行支付流程,修改訂單的狀態(tài)為已支付
防重復(fù)提交策略
- 在保證冪等的策略中,執(zhí)行是分兩步執(zhí)行的,后面一步依賴上面一步的查詢結(jié)果,這樣就無法保證原子性
-
無法保證原子性在高并發(fā)的情況下會存在問題:
- 第二次請求在第一次請求的下一步訂單狀態(tài)沒有修改為"已支付狀態(tài)"時進(jìn)行
- 為了解決這個問題 :將查詢和變更狀態(tài)操作加鎖,并將并行操作改為串行執(zhí)行
樂觀鎖
- 如果只是更新已有的數(shù)據(jù),沒有必要對業(yè)務(wù)進(jìn)行加鎖
- 設(shè)計(jì)表結(jié)構(gòu)時使用樂觀鎖,一般通過version來實(shí)現(xiàn)樂觀鎖:
- 保證執(zhí)行效率
- 保證冪等
UPDATE tab1
SET col1=1,version=version+1
WHERE version=#version#
由于ABA問題會導(dǎo)致樂觀鎖存在失效的情況,只要保證version值自增就不會出現(xiàn)ABA的問題
防重表
- 使用orderNo作為去重表中的唯一索引,每次請求都根據(jù)訂單號orderNo向去重表中插入一條數(shù)據(jù):
-
第一次請求查詢訂單支付狀態(tài):
- 訂單沒有支付
- 進(jìn)行支付操作
- 無論成功與否,執(zhí)行完成之后更新訂單的狀態(tài)為成功或失敗,刪除去重表中的數(shù)據(jù)
- 后續(xù)訂單因?yàn)楸碇械奈ㄒ凰饕迦胧?返回操作失敗,直到第一次請求完成(成功或者失敗)
-
第一次請求查詢訂單支付狀態(tài):
- 防重表的作用是實(shí)現(xiàn)加鎖的功能
分布式鎖
- 可以使用Redis分布式鎖代替防重表的功能
-
示例:
- 訂單發(fā)起支付請求
- 支付系統(tǒng)會去Redis緩存中查詢是否存在該訂單Key
- 如果不存在,向Redis中增加Key為訂單號
- 查詢訂單支付是否已經(jīng)支付
- 如果沒有則進(jìn)行支付,支付完成后刪除該訂單的Key
- 通過Redis實(shí)現(xiàn)分布式鎖,只有這次訂單請求完成,下次請求才會進(jìn)來
- 對比去重表,Redis分布式鎖將放并發(fā)做在緩存中,效率更高
- 同一時間只能完成一次支付請求
token令牌
-
token令牌分為兩個階段:
-
申請token階段:
- 在進(jìn)入到提交訂單頁面之前,需要訂單系統(tǒng)根據(jù)用戶信息向支付系統(tǒng)發(fā)起一次申請token的請求
- 支付系統(tǒng)將token保存到Redis緩存中,給支付階段使用
-
支付階段:
- 訂單系統(tǒng)獲取到申請的token, 發(fā)起支付請求,
- 支付系統(tǒng)檢查Redis是否存在該token
- 如果存在,表示第一次發(fā)起支付請求,刪除緩存中的token開始支付邏輯處理
- 如果緩存中不存在,表示非法請求
-
申請token階段:
支付緩沖區(qū)
-
支付緩沖區(qū):
- 將訂單的支付請求都快速地接收下來,是一個快速接收請求的緩沖管道
- 使用異步任務(wù)處理管道中的數(shù)據(jù),過濾調(diào)掉重復(fù)的待支付的數(shù)據(jù)
- 優(yōu)點(diǎn): 同步轉(zhuǎn)異步,高吞吐
- 缺點(diǎn): 無法及時返回支付結(jié)果,需要后續(xù)監(jiān)聽支付結(jié)果的異步返回
冪等的不足:
- 冪等是為了簡化客戶端邏輯,但是增加了服務(wù)提供者的邏輯和成本
- 冪等的使用需要根據(jù)具體場景具體分析
- 增加了額外控制冪等的業(yè)務(wù)邏輯,復(fù)雜了業(yè)務(wù)功能
- 將并行的功能轉(zhuǎn)化為串行,降低了執(zhí)行效率