接口冪等性
一徐鹤、什么是冪等
冪等(Idempotence)是一個(gè)數(shù)學(xué)和計(jì)算科學(xué)概念丈屹,簡單的來說:一個(gè)操作多次執(zhí)行產(chǎn)生的結(jié)果與一次執(zhí)行產(chǎn)生的結(jié)果一致藕漱,不會(huì)因多次執(zhí)行而產(chǎn)生負(fù)面影響锅劝。如等電梯時(shí),我們按一次按鈕和按多次結(jié)果是一樣疫粥,或者刷公交卡時(shí)茬斧,二次刷卡會(huì)提示刷卡重復(fù)。
二手形、冪等發(fā)生場景
-
表單重復(fù)提交:
在填寫頁面form表單時(shí)啥供,不小心快速保存了兩次,對應(yīng)的后臺產(chǎn)生了兩條相同的記錄库糠。
-
接口超時(shí)重試:
為解決接口調(diào)用超時(shí)伙狐,我們引入了重試機(jī)制,但如果因?yàn)榫W(wǎng)絡(luò)抖動(dòng)瞬欧、臨時(shí)故障等問題導(dǎo)致第一次請求沒及時(shí)收到返回結(jié)果(實(shí)際上服務(wù)端業(yè)務(wù)處理成功)贷屎,那么就會(huì)重試接口,從而產(chǎn)生重復(fù)數(shù)據(jù)艘虎。
-
消息重復(fù)消費(fèi):
mq消費(fèi)者在讀取消息時(shí)唉侄,如果讀取到重復(fù)的消息,也會(huì)導(dǎo)致重復(fù)消費(fèi)問題野建。
三属划、冪等發(fā)生在哪些操作
所有的操作最終對應(yīng)的數(shù)據(jù)庫都是CURD。因此冪等最終反映在select候生、update同眯、insert、delete上唯鸭。
對于select查詢须蜗,無論查詢一次還是多次,其返回的結(jié)果都是一致的目溉,因此明肮,所以select查詢是天然冪等;
對于delete刪除缭付,如果在不考慮返回結(jié)果情況下柿估,刪除一次或多次,其也是具有冪等性陷猫;
對于insert新增官份,新增一次或多次只厘,對應(yīng)產(chǎn)生重復(fù)記錄烙丛,其不具有冪等性舅巷;
對于update更新,如果是直接更新設(shè)置河咽,不管是執(zhí)行多少次钠右,都是冪等的(比如:
update user set status=1 where id=1
),但如果是更新計(jì)算忘蟹,那么就存在冪等問題(比如:update user set status=status+1 where id=1
)飒房。
因此,冪等問題主要發(fā)生在新增操作以及更新計(jì)算操作中媚值。
四狠毯、如何保證冪等
-
insert前先select判斷
在執(zhí)行新增操作時(shí),先查詢是否存在褥芒,如果存在嚼松,執(zhí)行更新操作,如果不存在锰扶,才進(jìn)行新增操作献酗。方案簡單實(shí)用,但如果對于多節(jié)點(diǎn)并發(fā)場景中坷牛,該方案仍然會(huì)存在重復(fù)數(shù)據(jù)的冪等問題罕偎,需要結(jié)合其他方案進(jìn)行優(yōu)化。
-
增加唯一索引
在表中建立唯一索引京闰,需要注意的是颜及,如果二次請求操作,需要對數(shù)據(jù)庫拋出DuplicateKeyException進(jìn)行捕獲蹂楣,正常返回處理成功結(jié)果俏站。
-
引入token
需要兩次請求完成一次業(yè)務(wù)操作,第一次請求獲取token捐迫,第二次請求帶上這個(gè)token乾翔,完成業(yè)務(wù)操作。
-
采用悲觀鎖
悲觀鎖適合高并發(fā)并且對數(shù)據(jù)的準(zhǔn)確性要求很高的場景施戴,如支付反浓、庫存場景。悲觀鎖常用的是數(shù)據(jù)庫表的for update
語句赞哗,原則:一鎖二判三更新
如下對于支付場景案例雷则,多個(gè)端扣除賬戶金額時(shí),需要保證賬戶金額充足才能扣減肪笋,否則會(huì)造成預(yù)支情形月劈。引入悲觀鎖機(jī)制度迂,在每筆金額扣減時(shí),先查詢金額猜揪,對當(dāng)前用戶金額記錄進(jìn)行加鎖惭墓,防止并發(fā)修改數(shù)據(jù),導(dǎo)致并發(fā)事務(wù)預(yù)支扣減而姐。`begin;` `select * from user_account where userId=xxx for update;` `update user_account set account=account-pay where userId=xxx;` `commit;`
-
采用樂觀鎖
由于悲觀鎖腊凶,鎖的記錄行,如商品庫存系統(tǒng)拴念,某類商品并發(fā)場景下會(huì)造成大量的請求累積钧萍,從而直接影響接口性能。為提高性能問題政鼠,采用樂觀鎖機(jī)制风瘦。樂觀鎖,通過在表中引入timestamp或version字段進(jìn)行版本控制更新公般。假設(shè)有一張商品庫存表goods_stock万搔,包含id,goodsId(商品id), stock(庫存數(shù)量),version(版本號)四個(gè)字段俐载。
步驟1:根據(jù)商品id查詢庫存表信息 `select goodsId, stock,version from goods_stock where goodId=xxx` 步驟2:根據(jù)商品id和當(dāng)前版本號進(jìn)行扣減庫存 `update goods_stock set stock=stock - buyCount,version=version+1 where goodsId=xxx and version=xxx` 步驟3:如果更新失敗蟹略,進(jìn)入重試 。
采用分布式鎖
不論是悲觀鎖遏佣,還是樂觀鎖挖炬,都是屬于數(shù)據(jù)庫層面上分布式鎖。還有基于zookeeper和redis的分布式鎖状婶,一般常用的redis來作為分布式鎖意敛,同時(shí)spring官網(wǎng)也推薦使用redisson這個(gè)框架api。這里不做詳細(xì)介紹膛虫,像分布式鎖單點(diǎn)問題草姻;集群后,加鎖成功稍刀,master未來及復(fù)制到slave上導(dǎo)致鎖信息丟失問題撩独;由于GC或網(wǎng)絡(luò)延遲導(dǎo)致的任務(wù)時(shí)間變長,從而導(dǎo)致執(zhí)行時(shí)間超過鎖過期時(shí)間账月,其他線程獲取鎖综膀;以及為解決以上問題,引入redLock是如何的原理局齿。在分布式鎖文章中再做詳細(xì)介紹剧劝。-
建立防重表
防重表,也是利用數(shù)據(jù)庫的唯一索引約束抓歼,在業(yè)務(wù)表所在的數(shù)據(jù)庫中單獨(dú)創(chuàng)建一個(gè)去重表讥此。每次請求來時(shí)拢锹,先執(zhí)行去重表插入記錄,如果成功執(zhí)行業(yè)務(wù)表邏輯萄喳,如果失敗卒稳,結(jié)束。
這里取胎,有個(gè)小插曲展哭,很多人可能會(huì)疑惑,既然是用了唯一索引約束闻蛀,那又跟上面方案2唯一索引有啥區(qū)別的,為什么不直接在業(yè)務(wù)表里采用唯一索引進(jìn)行去重您市,反而單獨(dú)設(shè)計(jì)一張去重表里增加維護(hù)數(shù)據(jù)庫表的難度觉痛。是這樣的,我們的業(yè)務(wù)可能涉及到的不止一張表茵休,而唯一索引又是通過這個(gè)多個(gè)業(yè)務(wù)表的多個(gè)字段來確認(rèn)唯一性的薪棒,那么這時(shí)候,就只能通過去重表來去解決榕莺。
采用狀態(tài)機(jī)
對于有些業(yè)務(wù)存在業(yè)務(wù)狀態(tài)控制流轉(zhuǎn)俐芯,每個(gè)狀態(tài)都有前置狀態(tài)和后置狀態(tài),例如工單系統(tǒng):待審批钉鸯、審批中吧史、撤銷、審批通過唠雕、審批拒絕贸营。訂單支付系統(tǒng):待提交、待支付岩睁、已支付钞脂、取消。待提交的后置狀態(tài)為待支付捕儒,已支付的上一狀態(tài)必須為待支付狀態(tài)冰啃,取消的上一狀態(tài)必須為待支付狀態(tài)。
狀態(tài)機(jī)方案從某一定的程度上可以理解為樂觀鎖的版本號方案刘莹,但不同的是前者基于業(yè)務(wù)層面的阎毅,后者基于數(shù)據(jù)庫層面,同時(shí)后者是遞增方式栋猖【谎Γ總體來說,處理的業(yè)務(wù)場景不同蒲拉。