關(guān)于Redis庫(kù)存超賣(mài)問(wèn)題的分析
一、分析問(wèn)題
剛剛秒殺優(yōu)惠券購(gòu)買(mǎi)測(cè)試的時(shí)候是我們自己在頁(yè)面上點(diǎn)擊進(jìn)行測(cè)試的,這跟真實(shí)的秒殺場(chǎng)景還是有很大差異的,因?yàn)檎鎸?shí)的秒殺場(chǎng)景下肯定有無(wú)數(shù)的用戶(hù)一起來(lái)?yè)屬?gòu),一起來(lái)點(diǎn)購(gòu)這個(gè)按鈕,因此一瞬間的并發(fā)量可能會(huì)達(dá)到每秒數(shù)百甚至上千、上萬(wàn)的并發(fā),那我們這個(gè)結(jié)構(gòu)還能不能工作呢?
要想模擬這種高并發(fā)的場(chǎng)景,肯定要用到JeMter
數(shù)據(jù)庫(kù)總量是100
將訂單也清0
接下來(lái)我們有100個(gè)券,我們希望的是只賣(mài)出100個(gè),理論上來(lái)講只生成100個(gè)訂單。
啟動(dòng)JeMeter,結(jié)果肯定有些成功有些失敗
查看報(bào)告 49.25%
的異常率,跟我們預(yù)期有出入,我們的預(yù)期應(yīng)該是一般失敗
回到數(shù)據(jù)庫(kù)中查看,可以看見(jiàn)訂單生成數(shù)量是 102
并且?guī)齑孀優(yōu)榱?-2
由此可見(jiàn)票出現(xiàn)了超賣(mài),我們只能賣(mài)一百件,現(xiàn)在卻賣(mài)出了 102件
,如果這件賣(mài)出的商品很貴重,這樣可能會(huì)給商家?guī)?lái)巨大的損失。
那么我們?yōu)槭裁磿?huì)出現(xiàn)這個(gè)問(wèn)題呢?
有關(guān)超賣(mài)問(wèn)題分析:在我們?cè)写a中是這么寫(xiě)的
if (voucher.getStock() < 1) { // 庫(kù)存不足 return Result.fail("庫(kù)存不足!"); } //5,扣減庫(kù)存 boolean success = seckillVoucherService.update() .setSql("stock= stock -1") .eq("voucher_id", voucherId).update(); if (!success) { //扣減庫(kù)存 return Result.fail("庫(kù)存不足!"); }
正常情況下一個(gè)如下圖,一個(gè)執(zhí)行完再執(zhí)行另一個(gè)
但是高并發(fā)的場(chǎng)景下,你就沒(méi)辦法控制線(xiàn)程的順序了,假設(shè)線(xiàn)程1過(guò)來(lái)查詢(xún)庫(kù)存,判斷出來(lái)庫(kù)存大于1,正準(zhǔn)備去扣減庫(kù)存,但是還沒(méi)有來(lái)得及去扣減,此時(shí)線(xiàn)程2過(guò)來(lái),線(xiàn)程2也去查詢(xún)庫(kù)存,發(fā)現(xiàn)這個(gè)數(shù)量一定也大于1,那么這兩個(gè)線(xiàn)程都會(huì)去扣減庫(kù)存,最終多個(gè)線(xiàn)程相當(dāng)于一起去扣減庫(kù)存,此時(shí)就會(huì)出現(xiàn)庫(kù)存的超賣(mài)問(wèn)題。
二、解決辦法
超賣(mài)問(wèn)題是典型的多線(xiàn)程安全問(wèn)題,針對(duì)這一問(wèn)題的常見(jiàn)解決方案就是加鎖:而對(duì)于加鎖,我們通常有兩種解決方案:見(jiàn)下圖:
悲觀(guān)鎖和樂(lè)觀(guān)鎖并不是真正的鎖,它只是鎖設(shè)計(jì)的理念
悲觀(guān)鎖:
如果我們多個(gè)線(xiàn)程是串行執(zhí)行的,就不會(huì)出現(xiàn)安全問(wèn)題了。所以這就是悲觀(guān)鎖的實(shí)現(xiàn)思想,既然多線(xiàn)程并發(fā)有安全問(wèn)題,那你就不要并發(fā)執(zhí)行了。正因?yàn)槿绱?,悲觀(guān)鎖的性能就不是很好,因?yàn)槟悴还苡卸嗌倬€(xiàn)程,都只能一個(gè)一個(gè)的去執(zhí)行,因此高并發(fā)的場(chǎng)景下悲觀(guān)鎖并不是很適合。
JDK中提供的syn,和lock、數(shù)據(jù)庫(kù)中的互斥的鎖,都是悲觀(guān)鎖的代表,同時(shí),悲觀(guān)鎖中又可以再細(xì)分為公平鎖,非公平鎖,可重入鎖,等等。
樂(lè)觀(guān)鎖:
因?yàn)闃?lè)觀(guān)鎖折后轉(zhuǎn)給你方案它不用加鎖,而是在執(zhí)行時(shí)才做一個(gè)判斷,因此它的性能要比悲觀(guān)鎖好很多。
但是它的關(guān)鍵點(diǎn)在于:我怎么知道在我更新的時(shí)候別人有沒(méi)有來(lái)做修改?因此這個(gè)判斷成為了關(guān)鍵,這也是我們接下來(lái)要研究的。
悲觀(guān)鎖比較簡(jiǎn)單,相信大家都會(huì),這里就不演示了,這里就演示樂(lè)觀(guān)鎖
三、樂(lè)觀(guān)鎖
判斷是否有人進(jìn)行修改,常見(jiàn)的方式有兩種
實(shí)現(xiàn)方式一:版本號(hào)法
這種方案是應(yīng)用最廣泛的,也是最普遍的。
樂(lè)觀(guān)鎖:會(huì)有一個(gè)版本號(hào),每次操作數(shù)據(jù)會(huì)對(duì)版本號(hào)+1,再提交回?cái)?shù)據(jù)時(shí),會(huì)去校驗(yàn)是否比之前的版本大1 ,如果大1 ,則進(jìn)行操作成功,這套機(jī)制的核心邏輯在于,如果在操作過(guò)程中,版本號(hào)只比原來(lái)大1 ,那么就意味著操作過(guò)程中沒(méi)有人對(duì)他進(jìn)行過(guò)修改,他的操作就是安全的,如果不大1,則數(shù)據(jù)被修改過(guò)。
有了版本號(hào)后,線(xiàn)程1在做查詢(xún)的時(shí)候,就不僅僅是查庫(kù)存了,它還要將版本號(hào)也查出來(lái),此時(shí)線(xiàn)程1查到的庫(kù)存和版本號(hào)是 1
,緊接著,它本來(lái)要進(jìn)行扣減了,但是此時(shí)另外一個(gè)線(xiàn)程插入進(jìn)來(lái)了,此時(shí)就出現(xiàn)并發(fā)的問(wèn)題了,此時(shí)線(xiàn)程二也去查詢(xún),同樣也是查詢(xún)stock和version,查到的也是1。緊接著又切到了線(xiàn)程1,線(xiàn)程1要去扣減庫(kù)存,判斷庫(kù)存是否大于0,此時(shí)就要去扣減。
以前是直接扣減就完了,但是現(xiàn)在不行,版本號(hào)每次修改的時(shí)候都要加1,因此它在修改庫(kù)存的時(shí)候不僅僅要修改庫(kù)存,還需要修改版本號(hào),因此在修改時(shí),樂(lè)觀(guān)鎖的方案是:修改前先判斷一下,之前查詢(xún)到的數(shù)據(jù)是否被修改過(guò),這里就是判斷版本是否被修改過(guò), where version = 1
,因?yàn)橹安樵?xún)出來(lái)的version是1,如果執(zhí)行這個(gè)條件時(shí)version依然等于1,說(shuō)明跟我們之前查詢(xún)到的一樣,說(shuō)明在我執(zhí)行修改之前,是沒(méi)有人修改過(guò)這個(gè)數(shù)據(jù)的,既然沒(méi)有人修改過(guò),我就可以放心大膽的去減了。
那么第一個(gè)線(xiàn)程在操作后,數(shù)據(jù)庫(kù)中的version變成了2,但是他自己滿(mǎn)足version=1 ,所以沒(méi)有問(wèn)題,此時(shí)線(xiàn)程2執(zhí)行,線(xiàn)程2 最后也需要加上條件version =1 ,但是現(xiàn)在由于線(xiàn)程1已經(jīng)操作過(guò)了,所以線(xiàn)程2,操作時(shí)就不滿(mǎn)足version=1 的條件了,所以線(xiàn)程2無(wú)法執(zhí)行成功
實(shí)現(xiàn)方式二:CAS
在實(shí)現(xiàn)方式一的基礎(chǔ)上做了簡(jiǎn)化,版本號(hào)法其實(shí)是用版本來(lái)表示版本是否變化,其次在更新的時(shí)候每次除了數(shù)據(jù)以外,版本也要跟著更新,既然每次更新都要更新版本,如果我查到的版本跟我更新時(shí)的版本一致,證明就沒(méi)有人更新。但是大家看一下我們當(dāng)前的業(yè)務(wù),我們每一次業(yè)務(wù),其實(shí)在查詢(xún)版本的時(shí)候庫(kù)存也都跟著查出來(lái)了,更新的時(shí)候庫(kù)存也要更新,可以發(fā)現(xiàn)庫(kù)存跟版本所做的事是一樣的,既然如此為什么不使用庫(kù)存代替版本?我在查詢(xún)的時(shí)候?qū)?kù)存查出來(lái),然后我在更新的時(shí)候當(dāng)前的這個(gè)庫(kù)存跟之前查到的庫(kù)存是不是一樣的,如果一樣,不就同樣可以證明沒(méi)有人修改過(guò)嗎?
用數(shù)據(jù)本身有沒(méi)有變化來(lái)判斷線(xiàn)程是否安全,這種方案就稱(chēng)之為cas(compare and set),先比較然后修改。
核心思路和上面差不多
下面是補(bǔ)充,老師沒(méi)講的。
利用cas進(jìn)行無(wú)鎖化機(jī)制加鎖,var5 是操作前讀取的內(nèi)存值,while中的var1+var2 是預(yù)估值,如果預(yù)估值 == 內(nèi)存值,則代表中間沒(méi)有被人修改過(guò),此時(shí)就將新值去替換 內(nèi)存值
其中do while 是為了在操作失敗時(shí),再次進(jìn)行自旋操作,即把之前的邏輯再操作一次。
int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5;
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
redis緩存與數(shù)據(jù)庫(kù)一致性的問(wèn)題及解決
這篇文章主要介紹了redis緩存與數(shù)據(jù)庫(kù)一致性的問(wèn)題及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06基于Redis實(shí)現(xiàn)雙加密Token的示例代碼
在現(xiàn)代分布式系統(tǒng)中,Token管理是身份驗(yàn)證和授權(quán)的核心部分,本文將深入分析一個(gè)基于Redis的Token管理實(shí)現(xiàn),探討其設(shè)計(jì)思路、關(guān)鍵代碼邏輯以及實(shí)現(xiàn)細(xì)節(jié),通過(guò)對(duì)源碼的逐層剖析,幫助讀者更好地理解Token管理的實(shí)現(xiàn)原理,需要的朋友可以參考下2025-01-01Spring?Boot實(shí)戰(zhàn)解決高并發(fā)數(shù)據(jù)入庫(kù)之?Redis?緩存+MySQL?批量入庫(kù)問(wèn)題
這篇文章主要介紹了Spring?Boot實(shí)戰(zhàn)解決高并發(fā)數(shù)據(jù)入庫(kù)之?Redis?緩存+MySQL?批量入庫(kù)問(wèn)題,本文通過(guò)圖文實(shí)例相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-02-02淺析Redis Sentinel 與 Redis Cluster
本文主要介紹Redis Sentinel 及 Redis Cluster的區(qū)別及用法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-06-06Redis分布式鎖如何設(shè)置超時(shí)時(shí)間
這篇文章主要介紹了Redis分布式鎖如何設(shè)置超時(shí)時(shí)間,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-11-11Satoken+Redis實(shí)現(xiàn)短信登錄、注冊(cè)、鑒權(quán)功能
這篇文章主要介紹了Satoken+Redis實(shí)現(xiàn)短信登錄、注冊(cè)、鑒權(quán)功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2024-01-01Linux系統(tǒng)下安裝Redis數(shù)據(jù)庫(kù)過(guò)程
大家好,本篇文章主要講的是Linux系統(tǒng)下安裝Redis數(shù)據(jù)庫(kù)過(guò)程,感興趣的同學(xué)趕快來(lái)看一看吧,對(duì)你有幫助的話(huà)記得收藏一下,方便下次瀏覽2021-12-12