1. 程式人生 > >搶購(秒殺)業務的技術要點

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

這一 提交 sql語句 設計 緩存 具體實現 不足 新版本 其他人

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

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

一、超賣現象

場景如下:

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

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

系統抗壓力問題

一、如何限流

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

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

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