并發(fā)減庫(kù)存,怎么保證不超賣?

Java技術(shù)棧
www.javastack.cn
關(guān)注閱讀更多優(yōu)質(zhì)文章
作者:廢物大師兄
來(lái)源:www.cnblogs.com/cjsblog/p/9135118.html
秒殺的場(chǎng)景有很多,比如:搶購(gòu)、搶票、搶紅包等等??傊?,就是在極短時(shí)間內(nèi)有大量的請(qǐng)求。
我們都知道,這種系統(tǒng)設(shè)計(jì)的大方向就是限流,即通過(guò)層層過(guò)濾,最終只讓相對(duì)較少的請(qǐng)求進(jìn)入到核心業(yè)務(wù)處理層。
這里不談秒殺設(shè)計(jì),不談使用隊(duì)列等使請(qǐng)求串行化,就談下怎么用鎖來(lái)保證數(shù)據(jù)正確,就是已經(jīng)到減庫(kù)存那一步了,在這一步中如果保證不超賣。
用隊(duì)列的話,可以是Java自動(dòng)的隊(duì)列,也可以用Redis的LPUSH RPOP
加鎖有兩個(gè)層面:一個(gè)是程序?qū)用?,另一個(gè)是數(shù)據(jù)庫(kù)層面。

分布式鎖
關(guān)于Redis分布式鎖,可以閱讀這篇文章:Spring Boot Redis 實(shí)現(xiàn)分布式鎖
其實(shí),這里加分布式鎖就是將多線程請(qǐng)求轉(zhuǎn)成單線程請(qǐng)求,因?yàn)槊看沃挥幸粋€(gè)線程獲得鎖并執(zhí)行,其余都被阻塞了。
這里有一點(diǎn)需要注意,就是當(dāng)你應(yīng)用了事務(wù)的話可能會(huì)存在問(wèn)題,請(qǐng)看下面的代碼

可能有人會(huì)這樣寫(xiě),第一眼看起來(lái)挺好的,沒(méi)問(wèn)題啊,但仔細(xì)實(shí)踐證明是由問(wèn)題的。
我們知道,mysql默認(rèn)的事務(wù)隔離級(jí)別是REPEATABLE-READ
關(guān)于事務(wù)隔離級(jí)別這塊兒,可以參考《事務(wù)隔離級(jí)別和傳播機(jī)制》這篇文章。
在這種隔離級(jí)別下,同一個(gè)事務(wù)中多次讀取,返回的數(shù)據(jù)是一樣的
同時(shí),Spring聲明式事務(wù)默認(rèn)的傳播特性REQUIRED

Spring聲明式事務(wù)是Spring AOP最好的例子,Spring是通過(guò)AOP代理的方式來(lái)實(shí)現(xiàn)事務(wù)的,也就是說(shuō)在調(diào)用reduceStock()方法的之前就已經(jīng)開(kāi)啟了事務(wù)。另外,關(guān)注公眾號(hào)Java技術(shù)棧,在后臺(tái)回復(fù):面試,可以獲取我整理的 Spring 系列面試題和答案,非常齊全。
那么,在并發(fā)情況下可能會(huì)存在這樣的情況,假設(shè)線程T1和T2都執(zhí)行到這里,于是它們都開(kāi)啟了事務(wù)S1和S2,T1先執(zhí)行,T2后執(zhí)行,
由于T2執(zhí)行的時(shí)候事務(wù)已經(jīng)創(chuàng)建了,根據(jù)隔離級(jí)別,這個(gè)時(shí)候事務(wù)S2讀取不到S1已提交的數(shù)據(jù),于是就會(huì)出現(xiàn)T1和T2讀取到的值是一樣的,即T2讀取的是T1更新前的庫(kù)存數(shù)據(jù)。
關(guān)于這一點(diǎn),大家可以自己寫(xiě)個(gè)代碼測(cè)試一下,下面是一段參考:

鑒于這種情況呢,可以將庫(kù)存放到Redis中,我們直接讀寫(xiě)Redis,這樣可以避免受數(shù)據(jù)庫(kù)事務(wù)的影響,當(dāng)然這也會(huì)帶來(lái)新的問(wèn)題,不再討論。另外,關(guān)注公眾號(hào)Java技術(shù)棧,在后臺(tái)回復(fù):面試,可以獲取我整理的 Redis 系列面試題和答案,非常齊全。
數(shù)據(jù)庫(kù)樂(lè)觀鎖
CAS(compare and swap)比較并交換
在Java中,一個(gè)線程想修改某個(gè)變量的值,那么第一步是將變量的值從主內(nèi)存中讀取到自己工作內(nèi)存中,然后修改,最后寫(xiě)回主內(nèi)存。
Java中通過(guò)Unsafe中compareAndSwapObject這樣的方法類實(shí)現(xiàn)的,它直接調(diào)用CPU指令。

數(shù)據(jù)庫(kù)中也有CAS,樂(lè)觀鎖就是一種CAS。
經(jīng)典的樂(lè)觀鎖實(shí)現(xiàn):
數(shù)據(jù)增加一個(gè)版本標(biāo)識(shí),一般是通過(guò)為數(shù)據(jù)庫(kù)表增加一個(gè)數(shù)字類型的 “version” 字段來(lái)實(shí)現(xiàn)。當(dāng)讀取數(shù)據(jù)時(shí),將version字段的值一同讀出,數(shù)據(jù)每更新一次,對(duì)此version值加一。
當(dāng)我們提交更新的時(shí)候,判斷數(shù)據(jù)庫(kù)表對(duì)應(yīng)記錄的當(dāng)前版本信息與第一次取出來(lái)的version值進(jìn)行比對(duì),如果數(shù)據(jù)庫(kù)表當(dāng)前版本號(hào)與第一次取出來(lái)的version值相等,則予以更新,否則認(rèn)為是過(guò)期數(shù)據(jù)。
更新的時(shí)候帶上版本號(hào),只有當(dāng)前版本號(hào)與更新之前查詢時(shí)的版本一致,才會(huì)更新


ABA問(wèn)題
這里順便多提一句,CAS中的ABA問(wèn)題
假設(shè),原先的值是A,線程-1讀取到的值是A,想把它改成D,但是在此期間,有可能其他線程已經(jīng)多次修改過(guò)這個(gè)值,只不過(guò)最后當(dāng)線程-1準(zhǔn)備將A改成D的時(shí)候,它發(fā)現(xiàn)恰好還是A,以為沒(méi)有人改過(guò),其實(shí)這時(shí)候的A已經(jīng)不是原來(lái)的A了。
也就是說(shuō),盡管修改之前做了比較,當(dāng)然,仍然會(huì)出現(xiàn)如下情況:

產(chǎn)生原因
ABA問(wèn)題導(dǎo)致的原因,是CAS過(guò)程中只簡(jiǎn)單進(jìn)行了“值”的校驗(yàn),有些情況下,“值”雖然相同,卻已經(jīng)不是原來(lái)的數(shù)據(jù)了。
優(yōu)化方向
CAS不能只比對(duì)“值”,還必須確保的是原來(lái)的數(shù)據(jù),才能修改成功。
常見(jiàn)實(shí)踐
“版本號(hào)”的比對(duì),一個(gè)數(shù)據(jù)一個(gè)版本,版本變化,即使值相同,也不應(yīng)該修改成功。
不僅要關(guān)注值,還要關(guān)注是不是原來(lái)的對(duì)象

參考:
https://www.sohu.com/a/150900817_178889
https://blog.csdn.net/zhjunjun93/article/details/78560700
https://blog.csdn.net/rexct392358928/article/details/52230737






關(guān)注Java技術(shù)??锤喔韶?/strong>


