本文為原創文章,轉載希望注明出處。
搶購業務數據庫需要考慮的點如下:
一、超賣現象
場景如下:
庫存數是5。現在3個用戶來購買,a用戶購買2個,b用戶購買3個,c用戶購買1個。合起來就是准備購買6個。
如果三個用戶是同時並發購買,會出現怎樣的情況呢?
每個用戶進行減庫存的時候,語句類似於:
update goods set amount=amount-購買數量 where goods_id=xxx。
mysql會鎖定這一行數據(使用innodb存儲引擎),數據庫加的是排他鎖。根據排他鎖的特點:其他線程不能讀、不能寫此行數據。
排他鎖情況下,那么其他用戶就是等待狀態了。
1、A用戶執行update的時候,鎖定庫存數據。update執行完畢后,減去了2個后,mysql自動釋放鎖。
2、b用戶執行,減去了3個。此時,已經賣掉5個庫存了。庫存數為0了。
3、但是c用戶接着執行,Update goods set amount=amount-1 where goods_id=xxx
結果庫存數量變成-1了。
思考:把庫存數量字段的類型,設計成正數類型,不允許出現負數,會怎么樣呢?
測驗結果:數據庫會直接報錯。通不過。
解決辦法:只有庫存數量,大於或等於購買數量的時候,才能去減庫存。其他情況,提示信息,庫存不足。
sql語句如下:
update goods set amount=amount-購買數量 where goods_id=xxx AND amount>=購買數量
這樣,輪到c執行的時候,由於使用了amount>=購買數量做限制條件,update語句返回的受影響的行數是0,意味着執行失敗了。直接提示,庫存不足。
二、並發搶購造成的速度慢問題
1、實現方式對比:悲觀鎖與樂觀鎖
第一種問題中描述的超賣現象,其實是並發搶購時出現的情況。用到的是數據庫內帶的加排他鎖方式,阻止了其他線程讀取、訪問數據,這要等待操作完畢后其他線程才能操作,這種方式是悲觀鎖方式。這樣會等待下去。
使用數據庫的悲觀鎖,是避免了數據並發更新,但是,加鎖畢竟是很耗服務器資源的,用戶要等待下去。所以並不能達到好的性能和高並發。
業界使用樂觀鎖的辦法來解決:使用數據庫的樂觀鎖是通用解決辦法。通用鎖實現了版本控制。不會進行排斥掉。減少資源的消耗。
樂觀鎖是相對悲觀鎖而言的,使用的是更加寬松的鎖定方式。
樂觀鎖,通俗說就是:修改數據的時候,不給數據加鎖。
既然不加鎖,其他線程也是可以訪問、修改數據。但是,修改的時候會獲取一個版本號,只有版本號符合了,才算更新成功。
不成功的,都算搶購失敗。
2、樂觀鎖的具體實現方式
樂觀鎖的機制與代碼版本庫svn很相似,這種方式,叫做多版本記錄方式。
如果在我提交代碼之前用本地代碼的版本號與服務器做對比,如果本地版本號小於服務器上最新版本號,則提交失敗,產生沖突代碼,讓用戶決定選擇哪個版本繼續使用。
邏輯描述:
if(之前讀取到行的版本號+1=數據庫此行現在的版本號+1){
//符合預期,數據庫的數據沒有給其他用戶修改掉。可以直接寫入數據了
}else{
//數據已經被修改了。所以當前的版本已經落后了。不能進行更新
}
例子:
給表goods加一個版本字段version,用來記錄行數據值的版本號。
版本號version字段,設計成一個正整數,比如是時間戳。每次修改后,要將version字段的值加1: 1496916794、1496916795、1496916796、1496916797、1496916798.................
讀數據的時候,順便將版本號的值讀取出來。update時,做版本號對比,如下:
1、先讀取這個商品的信息,順便將版本號讀取出來
select amount,version from t_goods where goods_id=8899;
2、更新數據
update goods set amount=amount-2,version=version+1 where goods_id=8899 and version=#{version} and amount>=2;
sql解釋:
#{version}是之前select讀取到的版本號,填入進去,意思是只能修改這樣的。
修改的時候,限制條件-必須版本號等於原來的版本號才能去修改。否則不能修改。更新成功的同時,版本號要加1。
優點:使用數據庫的樂觀鎖是通用解決辦法。通用鎖實現了版本控制。線程之間同時操作,不加鎖,線程不用等待了。減少了數據庫資源的消耗。
缺點:會增加cpu的計算開銷。不過值得這樣做,由於沒有加鎖進行阻塞,用戶不用等待結果,很快能等到執行結果了,用戶體驗更好。搶購的並發數其實提高了。
三、減庫存和下單保持在一個事務內
如果不在一個事務內,可能出現兩種現象:
1、訂單入庫失敗、減庫存成功。發現訂單入庫失敗,減庫存就不要繼續進行下去了。
2、訂單入庫成功、減庫存失敗。實際下了20個訂單,庫存卻沒有減。數據不一致了。
四、虛擬庫存和真實庫存兩套方案
考慮幾種情況:
1、有些人下單完后,最終並不會去付款。如果一下單就馬上減庫存,很多人下單,最終並不會去付款,可能導致庫存數最后為0,別的用戶無法下單了。而實際中倉庫中卻有庫存在,這樣庫存數據是不准確的。
2、什么時候減庫存? 是下單完成減庫存、還是付款完后減庫存呢?
付款后,才減庫存,可能出現的現象:用戶下完單,接着去付款,結果庫存不夠了。這樣用戶體驗很不好。
下完單就減庫存,能夠保證用戶下單只要付款,就一定能買到這個商品。用戶體驗較好。
針對一些人下單后,不付款,占着庫存資源,其他人無法下單。這個問題好解決,給付款設置一個有效期限,比如30分鍾。超過這個時間,庫存就釋放掉了。
具體技術實現辦法:下單后,馬上減去庫存。另外設置一個定時腳本,掃描超過30分未支付的訂單,把訂單中的商品數量返回到庫存中去。
為什么使用虛擬庫存和真實庫存兩套方案?
假設庫存數是50,a訂單購買了5個件商品,支付完畢,庫存數減去5,庫存數變成了45件。由於還沒有發貨,實際庫存中還有50件商品。這樣會出現混淆了。
使用兩套庫存記錄方案是有必要的!
- 下單-操作虛擬庫存數
- 商品發貨出庫-操作真實庫存數
真實庫存數,記錄下倉庫中這件商品真有多少件。真實庫存,其實非常方便內部人員查看,它只有商品出庫,這個庫存才減。
虛擬庫存,用來應對商品購買的。表明,還有多少數量可以給用戶去購買。並不表示倉庫中的庫存數。
五、頻繁讀庫存的壓力
因為,每次點擊,都要讀取庫存,判斷:有沒有庫存。如果讀庫存走的是數據庫判斷,很多人來搶購的情況下,數據庫的壓力會很大。
假設是1萬個用戶同時訪問搶購頁面。數據庫接受的訪問次數是1萬個並發。
用戶還要進行刷新頁面操作,由於每次刷新都會走數據庫判斷庫存。數量會更大。數據庫的壓力就更大了。
所以最好是,把庫存總數,緩存在redis中去。
內存中緩存的庫存數量,只用來做讀判斷。這樣壓力扛住了。而更改數據庫的庫存總數了,程序馬上要把庫存總數,同步到緩存中去。
系統抗壓力問題
一、如何限流
二、如何防止惡意刷數據。
防止的就是寫代碼去頻繁請求,為了識別是機器還是人工。加友好一點的驗證碼。