Redis實戰11-實現優惠券秒殺下單 一:基本的秒殺實現 二:分析上面代碼是否存在問題 超賣問題分析: 超賣問題場景的解決方案 1:版本號法 2:CAS法 小總結

本篇,咱們來實現優惠券秒殺下單功能。通過本篇學習,我們將會有如下收穫:

1:優惠券領券業務邏輯;

2:分析在高併發情況下,出現超賣問題產生的原因;

3:解決超賣問題兩種方案:版本號法及CAS法

4:樂觀鎖弊端改進方案;

本文涉及內容比較多,篇幅會比較長,同時有大量截圖。希望大家能耐心看完。好了,話不對說,咱們開始go go go~

一:基本的秒殺實現

下單時候需要判斷:

1:秒殺是否開始或結束,如果尚未開始或者已經結束則無法下單;

2:庫存是否充足,不充足無法下單

業務:

根據上圖邏輯,我們可以得到代碼相關邏輯:

1:查下優惠券、2:判斷是否秒殺開始;3:判斷秒殺是否結束;4:判斷庫存是否充足;5:扣減庫存;6:創建訂單;

相關代碼如下:

二:分析上面代碼是否存在問題

我們使用JMeter模擬200個用戶去秒殺搶優惠券。運行結果:

異常是45.5%。這個不對啊,按照我們預期的應該是50%的用戶失敗纔對。這45.5%,說明優惠券超賣出了9個。是嗎?我們來查查優惠券表:

庫存爲-9.再來查詢訂單表:發現訂單是109條。在高併發的情況下,還真的是超賣出了9個呢。

來分析爲什麼會出現這種情況呢?

來看看代碼,扣減庫存的相關代碼:

我們來分享下扣除庫存流程:

兩個線程來搶,假設當前就庫存就剩下一個了。線程1和線程2來搶這個庫存。流程如下:

在高併發的情況下,線程誰先執行,還真不好說。在高併發情況下,可能執行的順序就如下圖:

超賣問題分析:

T1的時候,線程1執行從數據庫查詢操作,查詢結果爲1;然後CPU讓出,線程2來執行,在T2時候,線程2也去執行數據庫查詢操作,查詢結果也是1.然後線程2,讓出CPU,T3時候,線程1得到了CPU執行權,執行扣除庫存操作。T4時候線程得到了CPU執行權,同樣執行扣除庫存操作。當兩個線程都執行完成後,數據庫中的庫存就成了-1了。

這只是有2個線程,當高併發的時候,有多個線程來查詢庫存,扣除庫存。如果出現了上面情況,就會出現超賣情況。

超賣問題場景的解決方案

超賣問題就是典型的多線程安全問題,針對這一問題常見的解決方案就是加鎖。鎖分爲樂觀鎖和悲觀鎖。我們來看看:

悲觀鎖:認爲線程安全問題一定會發生的,因此在操作數據之前,先獲取鎖,確保線程串行執行。

例如:Synchronized、Lock都是悲觀鎖。

因爲讓線程串行了,所以,悲觀鎖的效率低。

樂觀鎖:認爲線程安全問題不一定會發生,因此不加鎖,只是在更新數據的時候,判斷有沒有其他線程對數據做了修改。

如果沒有修改,則認爲是安全的,自己才更新數據;

如果已經被其他線程修改了說明發生了安全問題,此時可以重試或者拋出異常。

樂觀鎖的關鍵是判斷之前查詢得到的數據是否被修改過,常見的方式有兩種:

1:版本號法

每當數據被修改,版本號就+1

我們來看看還是上面多線程搶優惠券情況下,版本號法執行流程:

線程1,執行扣除庫存後,版本號+1後,就是2。如下圖:

我們再來看看線程2執行流程:

版本號法優化:

我們從上圖的邏輯中可以看出,在查詢庫存的時候,同時把版本號也查詢出來,在更新的時候,庫存-1,版本號也-1.where條件是版本號=查詢庫存的時候的版本號。我們只需要觀察版本號和庫存關係:同時查詢出來、同時-1.那麼,我們可不可以優化下,只使用一個字段來實現呢?答案是可以的:我們就把庫存作爲版本號概念,在更新的時候,where 條件中的version=查詢庫存的時候的版本號這個條件換成:where id =10 and stock = #{stock}。這樣就剩下一個字段。

其實,上面這個思路就是大名鼎鼎的CAS思想,也就是第二種常見的方案。

2:CAS法

我們來看看CAS法邏輯圖:

知識小擴展:

針對CAS中自旋壓力過大,我們可以使用Longadder這個類來解決。在Java8中提供了一個對AtomicLong改進的一個類:LongAdder.大量線程併發更新一個原子性的時候,天然的問題就是自旋,會導致併發性能問題,當然這個也比我們直接使用sync來得好。所以可以利用這個類,LongAdder來進行優化。

如果獲取某個值,則會對cell和base值進行遞增,最後返回一個完整的值。

好了,秒殺超賣問題分析完了,解決方案也有了。那麼接下來,我們就來實現解決超賣問題的代碼。

其實,我們只需要修改扣減庫存的邏輯,只添加一個where條件即可。如下圖:

修改完成之後,我們再使用JMeter模擬200個用戶去秒殺搶優惠券。運行結果:

異常竟然是89.9%。比沒修改前,異常率還增加了。我們再來看看結果樹情況:

一上來,就庫存不足了。我們z看看數據庫中,庫存情況:

優惠券領券了21張。爲什麼會出現這種情況呢?200個人來搶購100張優惠券,竟然纔有21個人搶到了。這個肯定不是我們想要的結果。這個是什麼原因導致的呢?其實這個就涉及到了CAS樂觀鎖的弊端了。我們重新分析:

如上圖,假設剛開始,就有3個線程同時搶奪資源,其中線程3先執行了更新,將100更新成了99,然後線程1和線程2,就更新失敗了。三個線程,只有一個更新成功了,就如同,我們在結果樹上看到的一樣。如下圖:

那麼失敗的這兩個,就搶不到了,導致我們庫存有剩餘。但是,咱們從真正的業務上來說,搶不到的依據是庫存等於0,纔算搶不到,而不是說我搶到之後,在修改的時候,別人不能夠在搶成功了。我們線程1和線程2在搶的時候,庫存還剩餘99啊,這個是不符合實際業務的。這就是樂觀鎖方案的問題所在--成功率太低了。那麼,我們對樂觀鎖法進行改進。

樂觀鎖法弊端改進

改進思路:在更新的時候,不再判斷庫存是否等於我手裏的庫存值。而是判斷,庫存是否大於0.如果大於,就執行扣除操作。

修改扣除庫存相關代碼:

修改完成之後,我們再使用JMeter模擬200個用戶去秒殺搶優惠券。運行結果:

從上圖中,我們看到異常率是50%。符合我們的預期。我們看看數據庫中的庫存:

訂單表中也是100條訂單。商品沒有超賣,訂單數量也正常。這樣是不是很完美解決了超賣問題?

答案:否。我們可以看到,這個方案,直接是由數據庫來處理的。我們知道,數據庫本來就是比較寶貴的資源,在高併發情況下,這種方案,肯定是不行的。我們繼續往下學習。

小總結

我們來總結下超賣這樣線程安全問題,解決方案有哪些?

下一篇預告:

在下一篇中咱們將實現另外一個功能:一人一單的功能。在下一篇中,您將有如下收穫:

1:悲觀鎖、樂觀鎖的使用場景;

2:synchronized關鍵字,在不同位置,鎖的顆粒度是不同的,怎麼優化呢;

3:toString方法之後,不能保證唯一,如果要保證唯一,需要在調用String的intern方法;

4:對spring事務有更深入瞭解-解決spring事務失效一種情況;

5:spring boot怎麼開啓對AspectJ的支持。
結束語
大家好,我是凱哥Java(kaigejava),樂於分享技術文章,歡迎大家關注“凱哥Java”,及時瞭解更多。讓我們一起學Java。也歡迎大家有事沒事就來和凱哥聊聊~~~。
如操作有問題歡迎去 我的 個人博客(www#kaigejava#com)留言或者 微號(凱哥Java。Kaigejava或者kaigejava2022)留言交流哦。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章