無論是日常工作中降铸,還是面試問題中,并發(fā)扣庫存都是一個很常見的場景摇零,正好業(yè)務(wù)里有這樣的場景推掸,可以對這類問題做一下總結(jié)。
一驻仅、場景描述
負(fù)責(zé)的項目里面有2個扣庫存的場景:
1.產(chǎn)品1:線上招募人員的產(chǎn)品谅畅,招募是有人數(shù)限制的,每招募成功1人噪服,扣減庫存1毡泻,直到庫存為0,自動停止招募粘优。
2.產(chǎn)品2:用戶秒殺產(chǎn)品仇味,用戶在同一個時間點呻顽,同時搶一件有庫存的商品。
一次扣庫存邪铲,可以分解為以下3個動作:
step1:查詢最新庫存(query
)
step2:檢查庫存是否足夠(內(nèi)存計算)
......
step3:更新庫存(update
)
二芬位、問題
1.單用戶重復(fù)提交未做冪等
用戶同時提交了2次扣庫存操作,因未做冪等带到,導(dǎo)致一次購買扣減2次庫存昧碉。
2.多用戶并發(fā)場景下庫存超賣
2個用戶搶1個庫存,查詢到的庫存都是1揽惹,檢查庫存足夠被饿,然后都去扣減庫存,庫存變成-1搪搏,發(fā)生了超賣狭握。
三、常見解法
對于第一個問題:
1.前端做防重機制防止用戶二次提交請求疯溺。
2.后端做冪等處理论颅,用戶請求帶業(yè)務(wù)token,進(jìn)行校驗囱嫩,重復(fù)token直接返回恃疯。
對于第二個問題:
1.直接扣庫存,不預(yù)檢查庫存update stock_table set stock = stock - 1 where stock-1 >= 0 and id = xxx
墨闲,缺點是不通用今妄,比如業(yè)務(wù)上要求庫存除了有reduce
還有add
操作。
2.悲觀鎖鸳碧,將扣庫存操作(step1->3
)變成只能串行盾鳞,缺點是同一時間只能有一個用戶來操作庫存,導(dǎo)致并發(fā)量不高(無論是通過synchronized
關(guān)鍵字瞻离、數(shù)據(jù)庫鎖比如select for update
腾仅、分布式鎖等各種方式加鎖,本質(zhì)都是一樣的)
3.樂觀鎖CAS方案:相比較方法1套利,庫存增加版本字段version
推励,在更新庫存時比較版本號例如update stock_table set version = old_version + 1,stock = stock - 1 where version = query_version and id = xxx
,只有版本號沒有變化日裙,才能更新庫存成功吹艇,如果版本號發(fā)生變化,則更新庫存失敗并進(jìn)行重試昂拂。
還有一些優(yōu)化比如
1.庫存放到redis
等緩存中受神,在redis
中進(jìn)行庫存的查詢、扣減格侯,利用內(nèi)存數(shù)據(jù)庫的特性提高讀QPS鼻听。
我當(dāng)前負(fù)責(zé)的一個應(yīng)用正是把庫存放在了內(nèi)存數(shù)據(jù)庫中來提高讀QPS财著,效果還是很顯著的
2.對DB進(jìn)行水平擴展(分庫分表方案)來提升讀寫QPS等等
根據(jù)經(jīng)驗:
大部分簡單業(yè)務(wù)場景下,方法1完全夠用了撑碴,甚至一些對并發(fā)并不是特別高撑教、業(yè)務(wù)容忍少量超賣場景下,直接扣庫存醉拓,無需檢查庫存是否stock-1 >= 0
伟姐,但是要注意如果扣減庫存后,發(fā)現(xiàn)業(yè)務(wù)失敗亿卤,可能需要做恢復(fù)庫存操作愤兵。
四、如何驗證
最簡單粗暴的方法就是構(gòu)造大流量壓測:
1.第一個冪等問題排吴,對單用戶請求大流量壓測秆乳,基本都能發(fā)現(xiàn)問題。
2.第二個多用戶并發(fā)問題钻哩,多個用戶的請求大流量壓測屹堰,也能發(fā)現(xiàn)問題。
擴展下:
1個商品有多個庫存街氢,怎么處理扯键?