高并發核心業務-扣減庫存的核心解決方案
需求分析
最近遇到一個問題,就是扣減庫存這一功能,在進行jmeter進行壓測的時候,發現庫存數變成負數。這顯然是不太現實的。所以在思考如何優雅的扣減庫存這一數據的正確性編寫了這一篇文章,有興趣的同學可以一起討論討論。
在這里不談秒殺設計、不談使用隊列等等讓請求串行化這種。秒殺的話有:限流、隊列、異步這些方式,這里一概不談!
高并發下扣減庫存的常見解決方案
我們來談一下怎么使用鎖來保證數據的正確性呢,每次領優惠券都只能領一張,那怎么來防止超發導致庫存變成負數,你可以想到哪幾種方式呢?
方案一:可以通過同步代碼塊Synchronized,lock
代碼如下所示,couponId為用戶購買的id,num就是要領取優惠券的數量,從上面的時序圖可以看到,在第四步校驗是否符合要求時,這時候的庫存是>0的,所以滿足了領劵的條件。在高并發下同時有很多線程來同時請求扣減優惠券,這時候就會造成了上面所提到的庫存變為負數。那使用Synochronized這種方法加鎖可以嗎?答案是:不行!因為synchronized的作用范圍是單個的jvm實例,如果是做了集群分布式的話,那就失效了,并且的話JVM加鎖之后就是串行等待了
public synchronized void reduceCouponStock(long couponId ,Integer num) { //業務邏輯 } 問題:synchronized 作用范圍是單個jvm實例, 如果做了集群分布式等,就失效了,且單機JVM加鎖后就是串行等待問題
方案二:分布式鎖redis,zookeeper
分布式鎖能夠解決上述的問題嗎?答案是:可以的!但是呢分布式鎖有個問題就是過于笨重,導致性能下降。(什么?你不懂?那我就畫圖給你看看),如下圖所示,在節點進行訪問數據庫之前,需要訪問redis/zookeeper管理器,等節點拿到鎖之后就可以訪問到數據庫了。在這里節點再拿鎖的過程中是要消耗通訊成本,還有請求響應的時間。
方案三:直接數據庫更新扣減
第一種下面這種方式可以解決領劵數量為1的情況下,如果領劵的數量大于1這種方式就不行了。比如:庫存為1,領劵數量為2,那領劵數量大于了庫存的數量,庫存還是會變成負數。
update coupon set stock=stock - #{num} where id = #{couponId} and stock>0
第二種方法可以很好的解決庫存為負數的問題,最后的(stock - #{num})>=0說明了庫存數減去領劵數大于0的話,那么這句sql是可以執行成功的,如果庫存數-領劵數是為負數,說明庫存已經不足了。該sql就不會執行成功!
修復了庫存為負數的問題。
update coupon set stock=stock - #{num} where id = #{couponId} and (stock - #{num})>=0 update coupon set stock=stock - #{num} where id = #{couponId} and stock >= #{num}
最多扣減1個優惠券的話,使用這一種就可以了
update coupon set stock=stock-1 where id = #{couponId} and stock>0
從上面的可以延伸出來下面這一句,oldStock為舊的庫存,這樣寫會有什么問題嗎?肯定有的!比如說扣減庫存,如果別人補充了庫存的話,那么就會存在了ABA問題,所以要看業務情況是否有這個限制。比如說:線程C查出來的庫存數量是10個(還沒保存的狀態),在線程C還沒保存的時間段中,線程A扣減了1個,這時庫存數就變成了9個。這時,線程B又更新了10個,就更新成功了。這一時間段庫存有沒有被修改呢?答案是有的。有點繞可以捋一捋。
update coupon set stock=stock-1 where id = #{couponId} and stock = #{oldStock}
【大廠面試題-p7】高并發庫存扣減超賣問題,很多人加了樂觀鎖版本號去解決,那下面三種有什么區別,分別適合哪些場景使用
下面提供三種方案,在不同的技術層級可能想到的解決方案不一樣。
1)update product set stock=stock-1 where id = 1 and stock>0 2)update product set stock=stock-1 where stock=#{原先查詢的庫存} and id = 1 and stock>0 3)update product set stock=stock-1,versioin = version+1 where id = 1 and stock>0 and version=#{原先查詢的版本號}
這個面試題的核心解答就是超賣的問題,就是為了防止庫存變成了負數,下面就來對上述的幾種方案來做一些闡述。
方案一:?也就是第一條語句,id是主鍵索引的前提下,如果每次只是減少1個庫存的話,可以使用該方式,只做數據安全的校驗就可以有效的減庫存,并且性能高,可以避免掉大量沒有用的sql,只要是有庫存的話就可以操作成功了。使用場景就好比如有,高并發場景下的取號器、優惠券發放會扣減庫存等等
方案二:?可以使用業務自身的條件做樂觀鎖,但是會存在ABA問題(該問題上面有說),這種方案的好處就是不用增加version版本字段,如果業務只是扣減庫存不用在意ABA問題的話,可以使用這種方式。但是使用這種方式的話業務的性能就會與方案一差了點,因為庫存變動之后的sql是無效的。
方案三:?增加了版本號主要是為了解決ABA的這個問題,version只能做遞增。使用的場景就比如有:商品的秒殺 、優惠券方法,需要記錄對庫存操作前后的業務問題。
三種方案都各有利弊,要看業務場景而定。
根據深思熟慮,總結扣減庫存所要關注的技術點有
1、當剩余的數量大于當前需要扣減的數量的話,不允許超賣
2、同一個數據的數量存在用戶并發進行扣減的問題,需要保證并發的一致性
3、要保證可用性和性能,性能至少是秒級的
4、一次扣減包含多個目標或數量
5、當扣減有多個數量的時候,其中一個沒有扣減成功,需要進行回滾
6、必須要有扣有還
7、一次扣減可以有多次的返還
8、返還等冪性
單庫場景
對于面試題進行剖析,這一種純數據庫的實現能夠滿足到扣減業務的各項功能的需求,無非就是依賴了兩點。第一點是基于了數據庫的樂觀鎖方式來保證了并發扣減的強一致性,第二點是基于數據庫的事務實現了可以批量進行扣減失敗的回滾。咱們來畫圖看看
如果數據量大導致單庫壓力也很大的話,我們可以做主從分庫分表,服務也可以做成集群等等
主從數據庫場景
采用讀寫分離的方式,主從復制直接使用mysql等數據庫已經有的功能,在改動上非常的小,只需要在扣減服務里面配置兩個數據源。當客戶來查詢剩余的庫存時,扣減服務校驗的時候,讀取從的數據庫就好。真正的數據扣減還是要使用主數據庫的。在讀寫分離之后,根據二八原則,80%為讀流量,主庫降低了壓力80%。但如果采用了讀寫分離也會導致讀取數據不準確的問題,庫存的數量也在實時變更的,最終的實際扣減保證數據準確性。
數據庫+緩存方案
這里我們可以說說使用數據庫+緩存的這種架構來處理扣減庫存的業務,當對磁盤進行數據操作時,向文件的末尾追加寫入性能的話要遠遠大于隨機修改的性能。數據庫同樣也是這樣的,插入要比更新的性能要好。數據庫的更新的話,為了保證對同一條數據并發更新的一致性,通常會加鎖,鎖是很耗性能的。通過這個理論我們可以推演出來一個扣減流程圖
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。