從架構(gòu)思維角度分析高并發(fā)下冪等性解決方案
1 背景
我們的云辦公系統(tǒng)有一個(gè)會(huì)議預(yù)定模塊,每個(gè)月最后一個(gè)工作日的下午三點(diǎn),會(huì)啟動(dòng)對(duì)下個(gè)月會(huì)議室的可用預(yù)定。
公司的 會(huì)議室大約200個(gè),但是需求量遠(yuǎn)不止于此,所以會(huì)形成會(huì)議室搶訂的場(chǎng)面(搶訂大軍為行政助理、人事助理、開發(fā)經(jīng)理、產(chǎn)品運(yùn)營(yíng)等對(duì)會(huì)議室有剛性需求的人)。
程序團(tuán)隊(duì),經(jīng)常會(huì)接到投訴,A同學(xué)和B同學(xué)搶了同一個(gè)會(huì)議室, 前端頁(yè)面顯示為兩個(gè)占位圖片,從數(shù)據(jù)庫(kù)看,是插入了兩條同一個(gè)會(huì)議位置的數(shù)據(jù),這兩條數(shù)據(jù)的發(fā)起人員分別是A和B。
這就牽扯出一個(gè)數(shù)學(xué)與計(jì)算機(jī)學(xué)概念: 冪等。
在計(jì)算機(jī)系統(tǒng)操作中,有很多種行為,需要保證無(wú)論執(zhí)行多少次,都應(yīng)該產(chǎn)生一樣的效果或返回一樣的結(jié)果。
比如:
1、前端重復(fù)點(diǎn)擊提交表單選中的數(shù)據(jù),在后臺(tái)應(yīng)該只能有一個(gè)數(shù)據(jù)錄入到數(shù)據(jù)庫(kù);
2、發(fā)送同一個(gè)消息,也應(yīng)該只發(fā)一次,用戶不會(huì)收到多條一樣的數(shù)據(jù);
3、創(chuàng)建業(yè)務(wù)訂單,一次業(yè)務(wù)請(qǐng)求只能創(chuàng)建一個(gè),如果程序沒有保證冪等,創(chuàng)建出多條訂單數(shù)據(jù),就混亂了。
4、在高并**況下,對(duì)于單一的數(shù)據(jù),不可以多次使用,比如一張確定位置的電影票,不會(huì)被多次預(yù)訂成功。同理的,同一時(shí)間的一個(gè)會(huì)議室信息,不會(huì)被多次預(yù)訂。
etc.很多重要的場(chǎng)景都需要冪等的特性來(lái)支持。
2 冪等性概念
冪等(idempotent)是一個(gè)數(shù)學(xué)與計(jì)算機(jī)學(xué)概念,常見于抽象代數(shù)中。
在我們的開發(fā)過程中,保證冪等性就是保證你的程序的無(wú)論執(zhí)行多少次,影響均與第一次執(zhí)行的影響是一致的,產(chǎn)生的結(jié)果也是一樣的。
而冪等函數(shù)(冪等方法),是指使用相同的參數(shù)結(jié)構(gòu)重復(fù)執(zhí)行,產(chǎn)生相同的結(jié)果的函數(shù),重復(fù)執(zhí)行冪等函數(shù)不會(huì)影響系統(tǒng)的狀態(tài)或者造成改變。
例如,"getUserName(String uCode)" 和 "delUser(String uCode)" 函數(shù)就是典型的冪等函數(shù),而更復(fù)雜的冪等保證是類似 高并發(fā)場(chǎng)景下的訂單號(hào)(流水號(hào))或者 秒殺場(chǎng)景下的唯一有效數(shù)據(jù) 等。
所以,冪等就是一個(gè)操作,不論執(zhí)行多少次,產(chǎn)生的效果和返回的結(jié)果都是一樣的。
3 冪等性問題的常見解決方案
3.1 查詢操作和刪除操作
查詢一次和查詢多次,在數(shù)據(jù)不變的情況下,查詢結(jié)果是一樣的,所以嚴(yán)格來(lái)說, select是天然的冪等操作。
刪除也是一樣的, 對(duì)于單條數(shù)據(jù)來(lái)說,刪除一次和刪除多次都是把數(shù)據(jù)刪除,影響和結(jié)果都是一樣(當(dāng)然,程序上 的執(zhí)行的返回結(jié)果可能會(huì)不一樣,比如操作數(shù)據(jù)庫(kù)的時(shí)候,刪除的數(shù)據(jù)不存在,返回0,正常刪除成功,返回1) 。
1 -- 用戶庫(kù)查詢某個(gè)身份證號(hào)的用戶名 2 select user_name from t_user where id_no ='xxx'; 3 4 -- 用戶庫(kù)刪除某個(gè)身份證號(hào)的用戶 5 delete from t_user where id_no ='xxx';
3.2 使用唯一索引 或者唯一組合索引
避免插入同樣信息的臟數(shù)據(jù)。
比如:中秋節(jié)到了,淘寶上線某款**版的月餅,每個(gè)用戶都只能購(gòu)買一盒月餅,如何防止用戶被創(chuàng)建多條月餅訂單數(shù)據(jù),可以給月餅銷售表中的用戶ID加唯一索引( 不允許被索引的數(shù)據(jù)列包含重復(fù)的值),
保證一個(gè)用戶只能創(chuàng)建成功一條月餅訂單記錄。
1 CREATE UNIQUE INDEX uni_user_userid ON t_user(userid);
唯一索引或唯一組合索引來(lái)防止新增數(shù)據(jù)出現(xiàn)臟數(shù)據(jù)(當(dāng)表存在唯一索引,并發(fā)執(zhí)行時(shí),先進(jìn)入的執(zhí)行成功,后進(jìn)入的會(huì)執(zhí)行失敗,說明該數(shù)據(jù)已經(jīng)存在了,返回結(jié)果即可)。如下圖所示。
回到我們上面的哪個(gè)會(huì)議室預(yù)訂,也可以是一樣的方式,可以用會(huì)議室編號(hào)(該編號(hào)具有唯一標(biāo)識(shí))作為唯一索引,但是他的實(shí)際情況更復(fù)雜。
3.3 token機(jī)制
防止頁(yè)面重復(fù)提交而導(dǎo)致的數(shù)據(jù)重復(fù)
業(yè)務(wù)現(xiàn)象: 頁(yè)面的數(shù)據(jù)只能被提交一次,或者提交多次的結(jié)果是一致的,不會(huì)產(chǎn)生多余的臟數(shù)據(jù)。
產(chǎn)生的原因: 由于系統(tǒng)卡頓導(dǎo)致的重復(fù)點(diǎn)擊或網(wǎng)絡(luò)重發(fā),還有就是nginx重發(fā)等情況,導(dǎo)致的數(shù)據(jù)被重復(fù)提交;
解決方法:
- 集群環(huán)境采用token加redis(redis單線程的,處理需要排隊(duì));
- 單JVM環(huán)境:采用token加redis或token加jvm內(nèi)存。
處理步驟:
- 數(shù)據(jù)提交前要向服務(wù)的申請(qǐng)token,token放到redis或jvm內(nèi)存,token需要設(shè)置有效時(shí)間,一般我們一個(gè)請(qǐng)求從request到respond時(shí)間是很短的,所以有效時(shí)間可以設(shè)置短一點(diǎn);
- 提交后后臺(tái)校驗(yàn)token,同時(shí)刪除token,返回執(zhí)行結(jié)果。token特點(diǎn):一次有效性,用完即刪,可以限流執(zhí)行。
流程如下,注意:redis要用刪除操作來(lái)判斷token,刪除成功代表token校驗(yàn)通過;
3.4 悲觀鎖
獲取數(shù)據(jù)的時(shí)候加鎖獲取。 select * from t_name where id='xxx' for update;
注意:這邊的id字段一定是主鍵或者唯一索引,不然會(huì)導(dǎo)致鎖表。悲觀鎖使用時(shí)一般會(huì)配合事務(wù)一起使用,數(shù)據(jù)鎖定時(shí)間可能會(huì)很長(zhǎng),根據(jù)實(shí)際情況選用。
3.5 樂觀鎖
樂觀鎖只是在更新數(shù)據(jù)那一刻鎖表,其他時(shí)間不鎖表,所以相對(duì)于悲觀鎖,效率更高,適用于多讀少寫的類型,并發(fā)大的情況。
樂觀鎖的實(shí)現(xiàn)方式多種多樣,可以通過version或者其他狀態(tài)條件:
1. 通過版本號(hào)實(shí)現(xiàn) update t_name set name=#{name},version=version+1 where version=#{version};
2. 通過條件限制 update t_name set avai_amount=avai_amount-#subAmount# where avai_amount-#subAmount# >= 0
使用版本號(hào)的方式執(zhí)行過程如下圖:
這邊需要注意: 樂觀鎖的更新操作,如果加上主鍵或者唯一索引來(lái)作為條件, 更新時(shí)鎖的是行,否則更新時(shí)會(huì)鎖表,性能效率差很多。所以上面兩個(gè)sql改成下面兩個(gè)會(huì)好很多。
1 update t_name set name=#name#,version=version+1 where id=#id# and version=#version#; 2 update t_name set avai_amount=avai_amount-#subAmount# where id=#id# and avai_amount-#subAmount# >= 0;
3.6 分布式鎖
如果是分布是系統(tǒng),構(gòu)建全局唯一索引比較困難,不同的鏈路業(yè)務(wù)可能分布在不同的數(shù)據(jù)庫(kù)表中,所以唯一性的字段沒法確定,這時(shí)候可以引入分布式鎖,通過第三方的系統(tǒng)(redis或zookeeper),
在業(yè)務(wù)系統(tǒng)插入數(shù)據(jù)或者更新數(shù)據(jù),獲取分布式鎖,然后做操作,完成業(yè)務(wù)操作之后,釋放鎖,這樣其實(shí)是把多線程并發(fā)的鎖的思路,引入多多個(gè)系統(tǒng),也就是分布式系統(tǒng)中得解決思路。
關(guān)鍵點(diǎn):某個(gè)長(zhǎng)流程處理過程要求不能并發(fā)執(zhí)行,可以在流程執(zhí)行之前根據(jù)某個(gè)標(biāo)志(用戶ID+后綴等)獲取分布式鎖,其他流程執(zhí)行時(shí)獲取鎖就會(huì)失敗,也就是同一時(shí)間該流程只能有一個(gè)能執(zhí)行成功,執(zhí)行完成后,釋放分布式鎖(分布式鎖要第三方系統(tǒng)提供)。
3.7 select + insert
并發(fā)不高的后臺(tái)系統(tǒng),或者一些簡(jiǎn)單的執(zhí)行任務(wù),為了支持冪等,支持重復(fù)執(zhí)行,簡(jiǎn)單的處理方法是,先查詢下一些關(guān)鍵數(shù)據(jù),判斷是否已經(jīng)執(zhí)行過,在進(jìn)行業(yè)務(wù)處理,就可以了。
但是同樣有問題,核心高并發(fā)流程不便使用這種方法。因?yàn)樗举|(zhì)上還是兩個(gè)步驟,中間還有執(zhí)行間隙的,在超高并發(fā)的情況還是會(huì)造成數(shù)據(jù)不一致的情況,這對(duì)于核心業(yè)務(wù)就是災(zāi)難了。
3.8 狀態(tài)機(jī)冪等
在設(shè)計(jì)單據(jù)相關(guān)的業(yè)務(wù),或者是任務(wù)相關(guān)的業(yè)務(wù),肯定會(huì)涉及到狀態(tài)機(jī)(狀態(tài)變更圖),就是業(yè)務(wù)單據(jù)上面有個(gè)狀態(tài),狀態(tài)在不同的情況下會(huì)發(fā)生變更,一般情況下存在有限狀態(tài)機(jī),
這時(shí)候,如果狀態(tài)機(jī)已經(jīng)處于下一個(gè)狀態(tài),這時(shí)候來(lái)了一個(gè)上一個(gè)狀態(tài)的變更,理論上是不能夠變更的,這樣的話,保證了有限狀態(tài)機(jī)的冪等。
注意:訂單等單據(jù)類業(yè)務(wù),存在很長(zhǎng)的狀態(tài)流轉(zhuǎn),一定要深刻理解狀態(tài)機(jī),對(duì)業(yè)務(wù)系統(tǒng)設(shè)計(jì)能力提高有很大幫助
3.9 保證Api接口的冪等性
如銀聯(lián)提供的付款接口:需要接入商戶提交付款請(qǐng)求時(shí)附帶:source來(lái)源,seq序列號(hào) ,source+seq在數(shù)據(jù)庫(kù)里面做唯一索引,防止多次付款(并發(fā)時(shí),只能處理一個(gè)請(qǐng)求) 。
關(guān)鍵點(diǎn):核心業(yè)務(wù)功能,對(duì)外提供接口為了支持冪等調(diào)用,接口有兩個(gè)字段必須傳,一個(gè)是來(lái)源source,一個(gè)是來(lái)源方序列號(hào)seq,這個(gè)兩個(gè)字段在提供方系統(tǒng)里面做聯(lián)合唯一索引,這樣當(dāng)?shù)谌秸{(diào)用時(shí),
先在本方系統(tǒng)里面查詢一下,是否已經(jīng)處理過,返回相應(yīng)處理結(jié)果;沒有處理過,進(jìn)行相應(yīng)處理,返回結(jié)果。為了冪等友好,最好先查詢一下,是否處理過該筆業(yè)務(wù),不查詢直接插入業(yè)務(wù)系統(tǒng),會(huì)報(bào)錯(cuò),而實(shí)際是已經(jīng)處理過了。
4 會(huì)議室的解決方案
將每天的會(huì)議預(yù)定按照半個(gè)小時(shí)1位做48位占用位符預(yù)算,建立緩存機(jī)制,進(jìn)行高效率的占位判斷,并反寫到預(yù)定表;啟動(dòng)額外調(diào)度服務(wù)做最終的預(yù)定持久化;
采用唯一聯(lián)合索引保障高并發(fā)下的冪等性策略。將會(huì)議室ID、時(shí)間段、日期,建立唯一組合索引,防止新增臟數(shù)據(jù),保證不會(huì)有兩條一樣的會(huì)議室預(yù)定記錄插入
1 CREATE UNIQUE CLUSTERED INDEX [ClusteredIndex_A9_MeetingReser] ON A9_MeetingReser 2 ( 3 [timespan] ASC, 4 [roomid] ASC, 5 [sdate] ASC 6 )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
執(zhí)行會(huì)議預(yù)訂的事務(wù)腳本,如下,當(dāng)數(shù)據(jù)庫(kù)中存在一樣的會(huì)議室信息時(shí),會(huì)返回錯(cuò)誤(被占用)的狀態(tài)值。
1 BEGIN TRAN T_Add; 2 DECLARE @code INT; DECLARE @occupyMeeing TABLE ( sMeetCode INT ); 3 DECLARE @resutlTable TABLE ( lType TINYINT,/*返回類型0為失敗類型,1為成功類型*/ resutlValue NVARCHAR(60)/*返回的信息*/ ); 4 -- Todo 業(yè)務(wù)邏輯 寫入數(shù)據(jù)庫(kù)操作,即會(huì)議號(hào)和占用的時(shí)間段標(biāo)識(shí)為聯(lián)合索引,不可重復(fù)插入,重復(fù)插入報(bào)錯(cuò) 5 IF @@ERROR!=0 goto w_err; 6 COMMIT TRAN T_Add ; 7 goto w_end w_err: 8 ROLLBACK TRAN T_Add ; 9 w_end: SELECT * FROM @resutlTable;
原來(lái)從預(yù)定到判斷占用到寫庫(kù)會(huì)耗時(shí)0.5~1s,優(yōu)化后整個(gè)流程執(zhí)行性能提升到50ms左右,避免了會(huì)議室預(yù)定沖突的情況。
結(jié)果:根據(jù)會(huì)議室預(yù)定記錄的統(tǒng)計(jì),優(yōu)化發(fā)布之后再未發(fā)生過預(yù)定沖突的問題。免除了會(huì)議管理員與預(yù)定人員溝通協(xié)調(diào)會(huì)議室的成本,解決了長(zhǎng)期困擾他們的問題。
5 總結(jié)
冪等本質(zhì)上與系統(tǒng)是否分布式、高并發(fā),業(yè)務(wù)執(zhí)行頻率高不高,沒有直接的關(guān)系。關(guān)鍵是程序的操作過程是不是冪等的。
典型的冪等操作就是:把某個(gè)變量設(shè)置為1這種行為,不管執(zhí)行多少次都是冪等的,你在進(jìn)行互聯(lián)網(wǎng)支付的時(shí)候,即使系統(tǒng)卡頓,你提交多次,也只支付一次。
要做到冪等性,從接口設(shè)計(jì)上來(lái)說不設(shè)計(jì)任何非冪等的操作即可。特別在類似支付寶,銀行,互聯(lián)網(wǎng)金融公司等涉及的網(wǎng)上資金系統(tǒng),既要高效,數(shù)據(jù)也要準(zhǔn)確,不能出現(xiàn)多扣款,多打款,產(chǎn)生金錢交易不一致等問題。
以上就是從架構(gòu)思維角度分析高并發(fā)下冪等性解決方案的詳細(xì)內(nèi)容,更多關(guān)于高并發(fā)下冪等性架構(gòu)思維解決方案的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
java實(shí)現(xiàn)水仙花數(shù)的計(jì)算
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)水仙花數(shù)的計(jì)算,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-08-08springboot+kafka中@KafkaListener動(dòng)態(tài)指定多個(gè)topic問題
這篇文章主要介紹了springboot+kafka中@KafkaListener動(dòng)態(tài)指定多個(gè)topic問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-12-12Spring @Scheduler使用cron表達(dá)式時(shí)的執(zhí)行問題詳解
Spring給程序猿們帶來(lái)了許多便利。下面這篇文章主要給大家介紹了關(guān)于Spring @Scheduler使用cron表達(dá)式時(shí)的執(zhí)行問題的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-09-09Java動(dòng)態(tài)代理(設(shè)計(jì)模式)代碼詳解
這篇文章主要介紹了Java動(dòng)態(tài)代理(設(shè)計(jì)模式)代碼詳解,具有一定借鑒價(jià)值,需要的朋友可以參考下2017-12-12springmvc接口接收參數(shù)與請(qǐng)求參數(shù)格式的整理
這篇文章主要介紹了springmvc接口接收參數(shù)與請(qǐng)求參數(shù)格式的整理,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11