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