再有人問(wèn)你分布式鎖是什么,就把這篇文章發(fā)給他
現(xiàn)在面試,一般都會(huì)聊聊分布式系統(tǒng)這塊的東西。通常面試官都會(huì)從服務(wù)框架(Spring Cloud、Dubbo)聊起,一路聊到分布式事務(wù)、分布式鎖、ZooKeeper 等知識(shí)。
所以咱們就來(lái)聊聊分布式鎖這塊的知識(shí),先具體的來(lái)看看 Redis 分布式鎖的實(shí)現(xiàn)原理。
說(shuō)實(shí)話,如果在公司里落地生產(chǎn)環(huán)境用分布式鎖的時(shí)候,一定是會(huì)用開(kāi)源類(lèi)庫(kù)的,比如 Redis 分布式鎖,一般就是用 Redisson 框架就好了,非常的簡(jiǎn)便易用。
大家如果有興趣,可以去 Redisson 的官網(wǎng),看看如何在項(xiàng)目中引入 Redisson 的依賴,然后基于 Redis 實(shí)現(xiàn)分布式鎖的加鎖與釋放鎖。
下面給大家看一段簡(jiǎn)單的使用代碼片段,先直觀的感受一下:
怎么樣,上面那段代碼,是不是感覺(jué)簡(jiǎn)單的不行!此外,人家還支持 Redis 單實(shí)例、Redis 哨兵、Redis Cluster、redis master-slave 等各種部署架構(gòu),都可以給你***實(shí)現(xiàn)。
Redisson 實(shí)現(xiàn) Redis 分布式鎖的底層原理
好的,接下來(lái)就通過(guò)一張手繪圖,給大家說(shuō)說(shuō) Redisson 這個(gè)開(kāi)源框架對(duì) Redis 分布式鎖的實(shí)現(xiàn)原理。
加鎖機(jī)制
咱們來(lái)看上面那張圖,現(xiàn)在某個(gè)客戶端要加鎖。如果該客戶端面對(duì)的是一個(gè) Redis Cluster 集群,他首先會(huì)根據(jù) Hash 節(jié)點(diǎn)選擇一臺(tái)機(jī)器。
這里注意,僅僅只是選擇一臺(tái)機(jī)器!這點(diǎn)很關(guān)鍵!緊接著,就會(huì)發(fā)送一段 Lua 腳本到 Redis 上,那段 Lua 腳本如下所示:
為啥要用 Lua 腳本呢?因?yàn)橐淮筵鐝?fù)雜的業(yè)務(wù)邏輯,可以通過(guò)封裝在 Lua 腳本中發(fā)送給 Redis,保證這段復(fù)雜業(yè)務(wù)邏輯執(zhí)行的原子性。
那么,這段 Lua 腳本是什么意思呢?這里 KEYS[1] 代表的是你加鎖的那個(gè) Key,比如說(shuō):RLock lock = redisson.getLock("myLock");這里你自己設(shè)置了加鎖的那個(gè)鎖 Key 就是“myLock”。
ARGV[1] 代表的就是鎖 Key 的默認(rèn)生存時(shí)間,默認(rèn) 30 秒。ARGV[2] 代表的是加鎖的客戶端的 ID,類(lèi)似于下面這樣:8743c9c0-0795-4907-87fd-6c719a6b4586:1。
給大家解釋一下,***段 if 判斷語(yǔ)句,就是用“exists myLock”命令判斷一下,如果你要加鎖的那個(gè)鎖 Key 不存在的話,你就進(jìn)行加鎖。如何加鎖呢?很簡(jiǎn)單,用下面的命令:hset myLock。
8743c9c0-0795-4907-87fd-6c719a6b4586:1 1,通過(guò)這個(gè)命令設(shè)置一個(gè) Hash 數(shù)據(jù)結(jié)構(gòu),這行命令執(zhí)行后,會(huì)出現(xiàn)一個(gè)類(lèi)似下面的數(shù)據(jù)結(jié)構(gòu):
上述就代表“8743c9c0-0795-4907-87fd-6c719a6b4586:1”這個(gè)客戶端對(duì)“myLock”這個(gè)鎖 Key 完成了加鎖。
接著會(huì)執(zhí)行“pexpire myLock 30000”命令,設(shè)置 myLock 這個(gè)鎖 Key 的生存時(shí)間是 30 秒。好了,到此為止,加鎖完成了。
鎖互斥機(jī)制
那么在這個(gè)時(shí)候,如果客戶端 2 來(lái)嘗試加鎖,執(zhí)行了同樣的一段 Lua 腳本,會(huì)咋樣呢?
很簡(jiǎn)單,***個(gè) if 判斷會(huì)執(zhí)行“exists myLock”,發(fā)現(xiàn) myLock 這個(gè)鎖 Key 已經(jīng)存在了。
接著第二個(gè) if 判斷,判斷一下,myLock 鎖 Key 的 Hash 數(shù)據(jù)結(jié)構(gòu)中,是否包含客戶端 2 的 ID,但是明顯不是的,因?yàn)槟抢锇氖强蛻舳? 1 的 ID。
所以,客戶端 2 會(huì)獲取到 pttl myLock 返回的一個(gè)數(shù)字,這個(gè)數(shù)字代表了 myLock 這個(gè)鎖 Key 的剩余生存時(shí)間。
比如還剩 15000 毫秒的生存時(shí)間。此時(shí)客戶端 2 會(huì)進(jìn)入一個(gè) while 循環(huán),不停的嘗試加鎖。
watch dog 自動(dòng)延期機(jī)制
客戶端 1 加鎖的鎖 Key 默認(rèn)生存時(shí)間才 30 秒,如果超過(guò)了 30 秒,客戶端 1 還想一直持有這把鎖,怎么辦呢?
簡(jiǎn)單!只要客戶端 1 一旦加鎖成功,就會(huì)啟動(dòng)一個(gè) watch dog 看門(mén)狗,他是一個(gè)后臺(tái)線程,會(huì)每隔 10 秒檢查一下,如果客戶端 1 還持有鎖 Key,那么就會(huì)不斷的延長(zhǎng)鎖 Key 的生存時(shí)間。
可重入加鎖機(jī)制
那如果客戶端 1 都已經(jīng)持有了這把鎖了,結(jié)果可重入的加鎖會(huì)怎么樣呢?比如下面這種代碼:
這時(shí)我們來(lái)分析一下上面那段 Lua 腳本。***個(gè)if判斷肯定不成立,“exists myLock”會(huì)顯示鎖 Key 已經(jīng)存在了。
第二個(gè) if 判斷會(huì)成立,因?yàn)?myLock 的 Hash 數(shù)據(jù)結(jié)構(gòu)中包含的那個(gè) ID,就是客戶端 1 的那個(gè) ID,也就是“8743c9c0-0795-4907-87fd-6c719a6b4586:1”。
此時(shí)就會(huì)執(zhí)行可重入加鎖的邏輯,他會(huì)用:incrby myLock 8743c9c0-0795-4907-87fd-6c71a6b4586:1 1,通過(guò)這個(gè)命令,對(duì)客戶端 1 的加鎖次數(shù),累加 1。
此時(shí) myLock 數(shù)據(jù)結(jié)構(gòu)變?yōu)橄旅孢@樣:
大家看到了吧,那個(gè) myLock 的 Hash 數(shù)據(jù)結(jié)構(gòu)中的那個(gè)客戶端 ID,就對(duì)應(yīng)著加鎖的次數(shù)。
釋放鎖機(jī)制
如果執(zhí)行 lock.unlock(),就可以釋放分布式鎖,此時(shí)的業(yè)務(wù)邏輯也是非常簡(jiǎn)單的。其實(shí)說(shuō)白了,就是每次都對(duì) myLock 數(shù)據(jù)結(jié)構(gòu)中的那個(gè)加鎖次數(shù)減 1。
如果發(fā)現(xiàn)加鎖次數(shù)是 0 了,說(shuō)明這個(gè)客戶端已經(jīng)不再持有鎖了,此時(shí)就會(huì)用:“del myLock”命令,從 Redis 里刪除這個(gè) Key。
然后呢,另外的客戶端 2 就可以嘗試完成加鎖了。這就是所謂的分布式鎖的開(kāi)源 Redisson 框架的實(shí)現(xiàn)機(jī)制。
一般我們?cè)谏a(chǎn)系統(tǒng)中,可以用 Redisson 框架提供的這個(gè)類(lèi)庫(kù)來(lái)基于 Redis 進(jìn)行分布式鎖的加鎖與釋放鎖。
上述 Redis 分布式鎖的缺點(diǎn)
上面那種方案***的問(wèn)題,就是如果你對(duì)某個(gè) Redis Master 實(shí)例,寫(xiě)入了 myLock 這種鎖 Key 的 Value,此時(shí)會(huì)異步復(fù)制給對(duì)應(yīng)的 Master Slave 實(shí)例。
但是這個(gè)過(guò)程中一旦發(fā)生 Redis Master 宕機(jī),主備切換,Redis Slave 變?yōu)榱?Redis Master。
接著就會(huì)導(dǎo)致,客戶端 2 來(lái)嘗試加鎖的時(shí)候,在新的 Redis Master 上完成了加鎖,而客戶端 1 也以為自己成功加了鎖。
此時(shí)就會(huì)導(dǎo)致多個(gè)客戶端對(duì)一個(gè)分布式鎖完成了加鎖。這時(shí)系統(tǒng)在業(yè)務(wù)語(yǔ)義上一定會(huì)出現(xiàn)問(wèn)題,導(dǎo)致各種臟數(shù)據(jù)的產(chǎn)生。
所以這個(gè)就是 Redis Cluster,或者是 redis master-slave 架構(gòu)的主從異步復(fù)制導(dǎo)致的 Redis 分布式鎖的***缺陷:在 Redis Master 實(shí)例宕機(jī)的時(shí)候,可能導(dǎo)致多個(gè)客戶端同時(shí)完成加鎖。
七張圖徹底講清楚 ZooKeeper 分布式鎖的實(shí)現(xiàn)原理
下面再給大家聊一下 ZooKeeper 實(shí)現(xiàn)分布式鎖的原理。同理,我是直接基于比較常用的 Curator 這個(gè)開(kāi)源框架,聊一下這個(gè)框架對(duì) ZooKeeper(以下簡(jiǎn)稱(chēng) ZK)分布式鎖的實(shí)現(xiàn)。
一般除了大公司是自行封裝分布式鎖框架之外,建議大家用這些開(kāi)源框架封裝好的分布式鎖實(shí)現(xiàn),這是一個(gè)比較快捷省事兒的方式。
ZooKeeper 分布式鎖機(jī)制
接下來(lái)我們一起來(lái)看看,多客戶端獲取及釋放 ZK 分布式鎖的整個(gè)流程及背后的原理。
首先大家看看下面的圖,如果現(xiàn)在有兩個(gè)客戶端一起要爭(zhēng)搶 ZK 上的一把分布式鎖,會(huì)是個(gè)什么場(chǎng)景?
如果大家對(duì) ZK 還不太了解的話,建議先自行百度一下,簡(jiǎn)單了解點(diǎn)基本概念,比如 ZK 有哪些節(jié)點(diǎn)類(lèi)型等等。
參見(jiàn)上圖。ZK 里有一把鎖,這個(gè)鎖就是 ZK 上的一個(gè)節(jié)點(diǎn)。然后呢,兩個(gè)客戶端都要來(lái)獲取這個(gè)鎖,具體是怎么來(lái)獲取呢?
咱們就假設(shè)客戶端 A 搶先一步,對(duì) ZK 發(fā)起了加分布式鎖的請(qǐng)求,這個(gè)加鎖請(qǐng)求是用到了 ZK 中的一個(gè)特殊的概念,叫做“臨時(shí)順序節(jié)點(diǎn)”。
簡(jiǎn)單來(lái)說(shuō),就是直接在"my_lock"這個(gè)鎖節(jié)點(diǎn)下,創(chuàng)建一個(gè)順序節(jié)點(diǎn),這個(gè)順序節(jié)點(diǎn)有 ZK 內(nèi)部自行維護(hù)的一個(gè)節(jié)點(diǎn)序號(hào)。
比如說(shuō),***個(gè)客戶端來(lái)搞一個(gè)順序節(jié)點(diǎn),ZK 內(nèi)部會(huì)給起個(gè)名字叫做:xxx-000001。
然后第二個(gè)客戶端來(lái)搞一個(gè)順序節(jié)點(diǎn),ZK 可能會(huì)起個(gè)名字叫做:xxx-000002。
大家注意一下,***一個(gè)數(shù)字都是依次遞增的,從 1 開(kāi)始逐次遞增。ZK 會(huì)維護(hù)這個(gè)順序。
所以這個(gè)時(shí)候,假如說(shuō)客戶端 A 先發(fā)起請(qǐng)求,就會(huì)搞出來(lái)一個(gè)順序節(jié)點(diǎn),大家看下面的圖,Curator 框架大概會(huì)弄成如下的樣子:
大家看,客戶端 A 發(fā)起一個(gè)加鎖請(qǐng)求,先會(huì)在你要加鎖的 node 下搞一個(gè)臨時(shí)順序節(jié)點(diǎn),這一大坨長(zhǎng)長(zhǎng)的名字都是 Curator 框架自己生成出來(lái)的。
然后,那個(gè)***一個(gè)數(shù)字是"1"。大家注意一下,因?yàn)榭蛻舳?A 是***個(gè)發(fā)起請(qǐng)求的,所以給他搞出來(lái)的順序節(jié)點(diǎn)的序號(hào)是"1"。
接著客戶端 A 創(chuàng)建完一個(gè)順序節(jié)點(diǎn)。還沒(méi)完,他會(huì)查一下"my_lock"這個(gè)鎖節(jié)點(diǎn)下的所有子節(jié)點(diǎn),并且這些子節(jié)點(diǎn)是按照序號(hào)排序的,這個(gè)時(shí)候他大概會(huì)拿到這么一個(gè)集合:
接著客戶端 A 會(huì)走一個(gè)關(guān)鍵性的判斷,就是說(shuō):唉!兄弟,這個(gè)集合里,我創(chuàng)建的那個(gè)順序節(jié)點(diǎn),是不是排在***個(gè)啊?
如果是的話,那我就可以加鎖了啊!因?yàn)槊髅魑揖褪?**個(gè)來(lái)創(chuàng)建順序節(jié)點(diǎn)的人,所以我就是***個(gè)嘗試加分布式鎖的人啊!
Bingo!加鎖成功!大家看下面的圖,再來(lái)直觀的感受一下整個(gè)過(guò)程:
接著假如說(shuō),客戶端 A 都加完鎖了,客戶端 B 過(guò)來(lái)想要加鎖了,這個(gè)時(shí)候他會(huì)干一樣的事兒:先是在"my_lock"這個(gè)鎖節(jié)點(diǎn)下創(chuàng)建一個(gè)臨時(shí)順序節(jié)點(diǎn),此時(shí)名字會(huì)變成類(lèi)似于:
大家看看下面的圖:
客戶端 B 因?yàn)槭堑诙€(gè)來(lái)創(chuàng)建順序節(jié)點(diǎn)的,所以 ZK 內(nèi)部會(huì)維護(hù)序號(hào)為"2"。
接著客戶端 B 會(huì)走加鎖判斷邏輯,查詢"my_lock"鎖節(jié)點(diǎn)下的所有子節(jié)點(diǎn),按序號(hào)順序排列,此時(shí)他看到的類(lèi)似于:
同時(shí)檢查自己創(chuàng)建的順序節(jié)點(diǎn),是不是集合中的***個(gè)?明顯不是啊,此時(shí)***個(gè)是客戶端 A 創(chuàng)建的那個(gè)順序節(jié)點(diǎn),序號(hào)為"01"的那個(gè)。所以加鎖失敗!
加鎖失敗了以后,客戶端 B 就會(huì)通過(guò) ZK 的 API 對(duì)他的順序節(jié)點(diǎn)的上一個(gè)順序節(jié)點(diǎn)加一個(gè)監(jiān)聽(tīng)器。ZK 天然就可以實(shí)現(xiàn)對(duì)某個(gè)節(jié)點(diǎn)的監(jiān)聽(tīng)。
如果大家還不知道 ZK 的基本用法,可以百度查閱,非常的簡(jiǎn)單??蛻舳?B 的順序節(jié)點(diǎn)是:
他的上一個(gè)順序節(jié)點(diǎn),不就是下面這個(gè)嗎?
即客戶端 A 創(chuàng)建的那個(gè)順序節(jié)點(diǎn)!所以,客戶端 B 會(huì)對(duì)
這個(gè)節(jié)點(diǎn)加一個(gè)監(jiān)聽(tīng)器,監(jiān)聽(tīng)這個(gè)節(jié)點(diǎn)是否被刪除等變化!大家看下面的圖:
接著,客戶端 A 加鎖之后,可能處理了一些代碼邏輯,然后就會(huì)釋放鎖。那么,釋放鎖是個(gè)什么過(guò)程呢?
其實(shí)很簡(jiǎn)單,就是把自己在 ZK 里創(chuàng)建的那個(gè)順序節(jié)點(diǎn),也就是:
這個(gè)節(jié)點(diǎn)給刪除。刪除了那個(gè)節(jié)點(diǎn)之后,ZK 會(huì)負(fù)責(zé)通知監(jiān)聽(tīng)這個(gè)節(jié)點(diǎn)的監(jiān)聽(tīng)器,也就是客戶端 B 之前加的那個(gè)監(jiān)聽(tīng)器,說(shuō):兄弟,你監(jiān)聽(tīng)的那個(gè)節(jié)點(diǎn)被刪除了,有人釋放了鎖。
此時(shí)客戶端 B 的監(jiān)聽(tīng)器感知到了上一個(gè)順序節(jié)點(diǎn)被刪除,也就是排在他之前的某個(gè)客戶端釋放了鎖。
此時(shí),就會(huì)通知客戶端 B 重新嘗試去獲取鎖,也就是獲取"my_lock"節(jié)點(diǎn)下的子節(jié)點(diǎn)集合,此時(shí)為:
集合里此時(shí)只有客戶端 B 創(chuàng)建的唯一的一個(gè)順序節(jié)點(diǎn)了!然后呢,客戶端 B 判斷自己居然是集合中的***個(gè)順序節(jié)點(diǎn),Bingo!可以加鎖了!直接完成加鎖,運(yùn)行后續(xù)的業(yè)務(wù)代碼即可,運(yùn)行完了之后再次釋放鎖。
其實(shí)如果有客戶端 C、客戶端 D 等 N 個(gè)客戶端爭(zhēng)搶一個(gè) ZK 分布式鎖,原理都是類(lèi)似的:
- 大家都是上來(lái)直接創(chuàng)建一個(gè)鎖節(jié)點(diǎn)下的一個(gè)接一個(gè)的臨時(shí)順序節(jié)點(diǎn)。
- 如果自己不是***個(gè)節(jié)點(diǎn),就對(duì)自己上一個(gè)節(jié)點(diǎn)加監(jiān)聽(tīng)器。
- 只要上一個(gè)節(jié)點(diǎn)釋放鎖,自己就排到前面去了,相當(dāng)于是一個(gè)排隊(duì)機(jī)制。
而且用臨時(shí)順序節(jié)點(diǎn)的另外一個(gè)用意就是,如果某個(gè)客戶端創(chuàng)建臨時(shí)順序節(jié)點(diǎn)之后,不小心自己宕機(jī)了也沒(méi)關(guān)系,ZK 感知到那個(gè)客戶端宕機(jī),會(huì)自動(dòng)刪除對(duì)應(yīng)的臨時(shí)順序節(jié)點(diǎn),相當(dāng)于自動(dòng)釋放鎖,或者是自動(dòng)取消自己的排隊(duì)。
***,咱們來(lái)看下用 Curator 框架進(jìn)行加鎖和釋放鎖的一個(gè)過(guò)程:
其實(shí)用開(kāi)源框架就是這點(diǎn)好,方便。這個(gè) Curator 框架的 ZK 分布式鎖的加鎖和釋放鎖的實(shí)現(xiàn)原理,就是上面我們說(shuō)的那樣子。
但是如果你要手動(dòng)實(shí)現(xiàn)一套那個(gè)代碼的話。還是有點(diǎn)麻煩的,要考慮到各種細(xì)節(jié),異常處理等等。所以大家如果考慮用 ZK 分布式鎖,可以參考下本文的思路。
每秒上千訂單場(chǎng)景下的分布式鎖高并發(fā)優(yōu)化實(shí)踐
接著就給大家聊一個(gè)有意思的話題:每秒上千訂單場(chǎng)景下,如何對(duì)分布式鎖的并發(fā)能力進(jìn)行優(yōu)化?
首先,我們一起來(lái)看看這個(gè)問(wèn)題的背景?前段時(shí)間有個(gè)朋友在外面面試,然后有一天找我聊說(shuō):有一個(gè)國(guó)內(nèi)不錯(cuò)的電商公司,面試官給他出了一個(gè)場(chǎng)景題:
假如下單時(shí),用分布式鎖來(lái)防止庫(kù)存超賣(mài),但是是每秒上千訂單的高并發(fā)場(chǎng)景,如何對(duì)分布式鎖進(jìn)行高并發(fā)優(yōu)化來(lái)應(yīng)對(duì)這個(gè)場(chǎng)景?
他說(shuō)他當(dāng)時(shí)沒(méi)答上來(lái),因?yàn)闆](méi)做過(guò)沒(méi)什么思路。其實(shí)我當(dāng)時(shí)聽(tīng)到這個(gè)面試題心里也覺(jué)得有點(diǎn)意思,因?yàn)槿绻俏襾?lái)面試候選人的話,應(yīng)該會(huì)給的范圍更大一些。
比如,讓面試的同學(xué)聊一聊電商高并發(fā)秒殺場(chǎng)景下的庫(kù)存超賣(mài)解決方案,各種方案的優(yōu)缺點(diǎn)以及實(shí)踐,進(jìn)而聊到分布式鎖這個(gè)話題。
因?yàn)閹?kù)存超賣(mài)問(wèn)題是有很多種技術(shù)解決方案的,比如悲觀鎖,分布式鎖,樂(lè)觀鎖,隊(duì)列串行化,Redis 原子操作,等等吧。
但是既然那個(gè)面試官兄弟限定死了用分布式鎖來(lái)解決庫(kù)存超賣(mài),我估計(jì)就是想問(wèn)一個(gè)點(diǎn):在高并發(fā)場(chǎng)景下如何優(yōu)化分布式鎖的并發(fā)性能。
我覺(jué)得,面試官提問(wèn)的角度還是可以接受的,因?yàn)樵趯?shí)際落地生產(chǎn)的時(shí)候,分布式鎖這個(gè)東西保證了數(shù)據(jù)的準(zhǔn)確性,但是他天然并發(fā)能力有點(diǎn)弱。
剛好我之前在自己項(xiàng)目的其他場(chǎng)景下,確實(shí)是做過(guò)高并發(fā)場(chǎng)景下的分布式鎖優(yōu)化方案,因此正好是借著這個(gè)朋友的面試題,把分布式鎖的高并發(fā)優(yōu)化思路,給大家來(lái)聊一聊。
庫(kù)存超賣(mài)現(xiàn)象是怎么產(chǎn)生的?
先來(lái)看看如果不用分布式鎖,所謂的電商庫(kù)存超賣(mài)是啥意思?大家看看下面的圖:
這個(gè)圖,其實(shí)很清晰了,假設(shè)訂單系統(tǒng)部署在兩臺(tái)機(jī)器上,不同的用戶都要同時(shí)買(mǎi) 10 臺(tái) iPhone,分別發(fā)了一個(gè)請(qǐng)求給訂單系統(tǒng)。
接著每個(gè)訂單系統(tǒng)實(shí)例都去數(shù)據(jù)庫(kù)里查了一下,當(dāng)前 iPhone 庫(kù)存是 12 臺(tái)。倆大兄弟一看,樂(lè)了,12 臺(tái)庫(kù)存大于了要買(mǎi)的 10 臺(tái)數(shù)量啊!
于是乎,每個(gè)訂單系統(tǒng)實(shí)例都發(fā)送 SQL 到數(shù)據(jù)庫(kù)里下單,然后扣減了 10 個(gè)庫(kù)存,其中一個(gè)將庫(kù)存從 12 臺(tái)扣減為 2 臺(tái),另外一個(gè)將庫(kù)存從 2 臺(tái)扣減為 -8 臺(tái)。
現(xiàn)在完了,庫(kù)存出現(xiàn)了負(fù)數(shù)!淚奔啊,沒(méi)有 20 臺(tái) iPhone 發(fā)給兩個(gè)用戶啊!這可如何是好。
用分布式鎖如何解決庫(kù)存超賣(mài)問(wèn)題?
我們用分布式鎖如何解決庫(kù)存超賣(mài)問(wèn)題呢?其實(shí)很簡(jiǎn)單,回憶一下上次我們說(shuō)的那個(gè)分布式鎖的實(shí)現(xiàn)原理:
同一個(gè)鎖 Key,同一時(shí)間只能有一個(gè)客戶端拿到鎖,其他客戶端會(huì)陷入***的等待來(lái)嘗試獲取那個(gè)鎖,只有獲取到鎖的客戶端才能執(zhí)行下面的業(yè)務(wù)邏輯。
代碼大概就是上面那個(gè)樣子,現(xiàn)在我們來(lái)分析一下,為啥這樣做可以避免庫(kù)存超賣(mài)?
大家可以順著上面的那個(gè)步驟序號(hào)看一遍,馬上就明白了。
從上圖可以看到,只有一個(gè)訂單系統(tǒng)實(shí)例可以成功加分布式鎖,然后只有他一個(gè)實(shí)例可以查庫(kù)存、判斷庫(kù)存是否充足、下單扣減庫(kù)存,接著釋放鎖。
釋放鎖之后,另外一個(gè)訂單系統(tǒng)實(shí)例才能加鎖,接著查庫(kù)存,一下發(fā)現(xiàn)庫(kù)存只有 2 臺(tái)了,庫(kù)存不足,無(wú)法購(gòu)買(mǎi),下單失敗。不會(huì)將庫(kù)存扣減為 -8 的。
有沒(méi)其他方案解決庫(kù)存超賣(mài)問(wèn)題?
當(dāng)然有啊!比如悲觀鎖,分布式鎖,樂(lè)觀鎖,隊(duì)列串行化,異步隊(duì)列分散,Redis 原子操作,等等,很多方案,我們對(duì)庫(kù)存超賣(mài)有自己的一整套優(yōu)化機(jī)制。
但是前面說(shuō)過(guò)了,這篇文章就聊一個(gè)分布式鎖的并發(fā)優(yōu)化,不是聊庫(kù)存超賣(mài)的解決方案,所以庫(kù)存超賣(mài)只是一個(gè)業(yè)務(wù)場(chǎng)景而已。
分布式鎖的方案在高并發(fā)場(chǎng)景下
好,現(xiàn)在我們來(lái)看看,分布式鎖的方案在高并發(fā)場(chǎng)景下有什么問(wèn)題?
問(wèn)題很大啊!兄弟,不知道你看出來(lái)了沒(méi)有。分布式鎖一旦加了之后,對(duì)同一個(gè)商品的下單請(qǐng)求,會(huì)導(dǎo)致所有客戶端都必須對(duì)同一個(gè)商品的庫(kù)存鎖 Key 進(jìn)行加鎖。
比如,對(duì) iPhone 這個(gè)商品的下單,都必對(duì)“iphone_stock”這個(gè)鎖 Key 來(lái)加鎖。這樣會(huì)導(dǎo)致對(duì)同一個(gè)商品的下單請(qǐng)求,就必須串行化,一個(gè)接一個(gè)的處理。
大家再回去對(duì)照上面的圖反復(fù)看一下,應(yīng)該能想明白這個(gè)問(wèn)題。
假設(shè)加鎖之后,釋放鎖之前,查庫(kù)存→創(chuàng)建訂單→扣減庫(kù)存,這個(gè)過(guò)程性能很高吧,算他全過(guò)程 20 毫秒,這應(yīng)該不錯(cuò)了。
那么 1 秒是 1000 毫秒,只能容納 50 個(gè)對(duì)這個(gè)商品的請(qǐng)求依次串行完成處理。
比如一秒鐘來(lái) 50 個(gè)請(qǐng)求,都是對(duì) iPhone 下單的,那么每個(gè)請(qǐng)求處理 20 毫秒,一個(gè)一個(gè)來(lái),*** 1000 毫秒正好處理完 50 個(gè)請(qǐng)求。
大家看一眼下面的圖,加深一下感覺(jué)。
所以看到這里,大家起碼也明白了,簡(jiǎn)單的使用分布式鎖來(lái)處理庫(kù)存超賣(mài)問(wèn)題,存在什么缺陷。
缺陷就是同一個(gè)商品多用戶同時(shí)下單的時(shí)候,會(huì)基于分布式鎖串行化處理,導(dǎo)致沒(méi)法同時(shí)處理同一個(gè)商品的大量下單的請(qǐng)求。
這種方案,要是應(yīng)對(duì)那種低并發(fā)、無(wú)秒殺場(chǎng)景的普通小電商系統(tǒng),可能還可以接受。
因?yàn)槿绻l(fā)量很低,每秒就不到 10 個(gè)請(qǐng)求,沒(méi)有瞬時(shí)高并發(fā)秒殺單個(gè)商品的場(chǎng)景的話,其實(shí)也很少會(huì)對(duì)同一個(gè)商品在 1 秒內(nèi)瞬間下 1000 個(gè)訂單,因?yàn)樾‰娚滔到y(tǒng)沒(méi)那場(chǎng)景。
如何對(duì)分布式鎖進(jìn)行高并發(fā)優(yōu)化?
好了,終于引入正題了,那么現(xiàn)在怎么辦呢?
面試官說(shuō),我現(xiàn)在就卡死,庫(kù)存超賣(mài)就是用分布式鎖來(lái)解決,而且一秒對(duì)一個(gè) iPhone 下上千訂單,怎么優(yōu)化?
現(xiàn)在按照剛才的計(jì)算,你 1 秒鐘只能處理針對(duì) iPhone 的 50 個(gè)訂單。其實(shí)說(shuō)出來(lái)也很簡(jiǎn)單,相信很多人看過(guò) Java 里的 ConcurrentHashMap 的源碼和底層原理,應(yīng)該知道里面的核心思路,就是分段加鎖!
把數(shù)據(jù)分成很多個(gè)段,每個(gè)段是一個(gè)單獨(dú)的鎖,所以多個(gè)線程過(guò)來(lái)并發(fā)修改數(shù)據(jù)的時(shí)候,可以并發(fā)的修改不同段的數(shù)據(jù)。不至于說(shuō),同一時(shí)間只能有一個(gè)線程獨(dú)占修改 ConcurrentHashMap 中的數(shù)據(jù)。
另外,Java 8 中新增了一個(gè) LongAdder 類(lèi),也是針對(duì) Java 7 以前的 AtomicLong 進(jìn)行的優(yōu)化,解決的是 CAS 類(lèi)操作在高并發(fā)場(chǎng)景下,使用樂(lè)觀鎖思路,會(huì)導(dǎo)致大量線程長(zhǎng)時(shí)間重復(fù)循環(huán)。
LongAdder 中也是采用了類(lèi)似的分段 CAS 操作,失敗則自動(dòng)遷移到下一個(gè)分段進(jìn)行 CAS 的思路。
其實(shí)分布式鎖的優(yōu)化思路也是類(lèi)似的,之前我們是在另外一個(gè)業(yè)務(wù)場(chǎng)景下落地了這個(gè)方案到生產(chǎn)中,不是在庫(kù)存超賣(mài)問(wèn)題里用的。
但是庫(kù)存超賣(mài)這個(gè)業(yè)務(wù)場(chǎng)景不錯(cuò),很容易理解,所以我們就用這個(gè)場(chǎng)景來(lái)說(shuō)一下。
大家看看下面的圖:
這就是分段加鎖。假如你現(xiàn)在 iPhone 有 1000 個(gè)庫(kù)存,那么你完全可以給拆成 20 個(gè)庫(kù)存段。
要是你愿意,可以在數(shù)據(jù)庫(kù)的表里建 20 個(gè)庫(kù)存字段,比如 stock_01,stock_02,類(lèi)似這樣的,也可以在 Redis 之類(lèi)的地方放 20 個(gè)庫(kù)存 Key。
總之,就是把你的 1000 件庫(kù)存給他拆開(kāi),每個(gè)庫(kù)存段是 50 件庫(kù)存,比如 stock_01 對(duì)應(yīng) 50 件庫(kù)存,stock_02 對(duì)應(yīng) 50 件庫(kù)存。
接著,每秒 1000 個(gè)請(qǐng)求過(guò)來(lái)了,好!此時(shí)其實(shí)可以是自己寫(xiě)一個(gè)簡(jiǎn)單的隨機(jī)算法,每個(gè)請(qǐng)求都是隨機(jī)在 20 個(gè)分段庫(kù)存里,選擇一個(gè)進(jìn)行加鎖。
Bingo!這樣就好了,同時(shí)可以有最多 20 個(gè)下單請(qǐng)求一起執(zhí)行,每個(gè)下單請(qǐng)求鎖了一個(gè)庫(kù)存分段,然后在業(yè)務(wù)邏輯里面,就對(duì)數(shù)據(jù)庫(kù)或者是 Redis 中的那個(gè)分段庫(kù)存進(jìn)行操作即可,包括查庫(kù)存→判斷庫(kù)存是否充足→扣減庫(kù)存。
這相當(dāng)于什么呢?相當(dāng)于一個(gè) 20 毫秒,可以并發(fā)處理掉 20 個(gè)下單請(qǐng)求,那么 1 秒,也就可以依次處理掉 20 * 50 = 1000 個(gè)對(duì) iPhone 的下單請(qǐng)求了。
一旦對(duì)某個(gè)數(shù)據(jù)做了分段處理之后,有一個(gè)坑大家一定要注意:就是如果某個(gè)下單請(qǐng)求,咔嚓加鎖,然后發(fā)現(xiàn)這個(gè)分段庫(kù)存里的庫(kù)存不足了,此時(shí)咋辦?
這時(shí)你得自動(dòng)釋放鎖,然后立馬換下一個(gè)分段庫(kù)存,再次嘗試加鎖后嘗試處理。這個(gè)過(guò)程一定要實(shí)現(xiàn)。
分布式鎖并發(fā)優(yōu)化方案有什么不足?
不足肯定是有的,***的不足,很不方便,實(shí)現(xiàn)太復(fù)雜了:
- 首先,你得對(duì)一個(gè)數(shù)據(jù)分段存儲(chǔ),一個(gè)庫(kù)存字段本來(lái)好好的,現(xiàn)在要分為 20 個(gè)庫(kù)存字段。
- 其次,你在每次處理庫(kù)存的時(shí)候,還得自己寫(xiě)隨機(jī)算法,隨機(jī)挑選一個(gè)分段來(lái)處理。
- ***,如果某個(gè)分段中的數(shù)據(jù)不足了,你還得自動(dòng)切換到下一個(gè)分段數(shù)據(jù)去處理。
這個(gè)過(guò)程都是要手動(dòng)寫(xiě)代碼實(shí)現(xiàn)的,還是有點(diǎn)工作量,挺麻煩的。
不過(guò)我們確實(shí)在一些業(yè)務(wù)場(chǎng)景里,因?yàn)橛玫搅朔植际芥i,然后又必須要進(jìn)行鎖并發(fā)的優(yōu)化,又進(jìn)一步用到了分段加鎖的技術(shù)方案,效果當(dāng)然是很好的了,一下子并發(fā)性能可以增長(zhǎng)幾十倍。
該優(yōu)化方案的后續(xù)改進(jìn):以我們本文所說(shuō)的庫(kù)存超賣(mài)場(chǎng)景為例,你要是這么玩,會(huì)把自己搞的很痛苦!再次強(qiáng)調(diào),我們這里的庫(kù)存超賣(mài)場(chǎng)景,僅僅只是作為演示場(chǎng)景而已。
作者:中華石杉
中華石杉:十余年 BAT 架構(gòu)經(jīng)驗(yàn),一線互聯(lián)網(wǎng)公司技術(shù)總監(jiān)。帶領(lǐng)上百人團(tuán)隊(duì)開(kāi)發(fā)過(guò)多個(gè)億級(jí)流量高并發(fā)系統(tǒng)?,F(xiàn)將多年工作中積累下的研究手稿、經(jīng)驗(yàn)總結(jié)整理成文,傾囊相授。微信公眾號(hào):石杉的架構(gòu)筆記(ID:shishan100)。