搶購(秒殺)業務的技術要點


 

 本文為原創文章,轉載希望注明出處。

 

 

搶購業務數據庫需要考慮的點如下:

 

一、超賣現象

 

場景如下:

 

   庫存數是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中去。

 

  內存中緩存的庫存數量,只用來做讀判斷。這樣壓力扛住了。而更改數據庫的庫存總數了,程序馬上要把庫存總數,同步到緩存中去。

 

 

 

 

 

系統抗壓力問題

 

一、如何限流

二、如何防止惡意刷數據。

 

      防止的就是寫代碼去頻繁請求,為了識別是機器還是人工。加友好一點的驗證碼。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM