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

 

 本文爲原創文章,轉載但願註明出處。php

 

 

搶購業務數據庫須要考慮的點以下:mysql

 

1、超賣現象redis

 

場景以下:sql

 

   庫存數是5。如今3個用戶來購買,a用戶購買2個,b用戶購買3個,c用戶購買1個。合起來就是準備購買6個。數據庫

 

   若是三個用戶是同時併發購買,會出現怎樣的狀況呢?緩存

 

  每一個用戶進行減庫存的時候,語句相似於:服務器

 

update goods set amount=amount-購買數量 where goods_id=xxx。

  

 

mysql會鎖定這一行數據(使用innodb存儲引擎),數據庫加的是排他鎖。根據排他鎖的特色:其餘線程不能讀、不能寫此行數據。併發

 

排他鎖狀況下,那麼其餘用戶就是等待狀態了。svn

 

   一、A用戶執行update的時候,鎖定庫存數據。update執行完畢後,減去了2個後,mysql自動釋放鎖。高併發

 

   二、b用戶執行,減去了3個。此時,已經賣掉5個庫存了。庫存數爲0了。

 

    三、可是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,意味着執行失敗了。直接提示,庫存不足。

 

 

 

2、併發搶購形成的速度慢問題

 

 

一、實現方式對比:悲觀鎖與樂觀鎖

 

第一種問題中描述的超賣現象,實際上是併發搶購時出現的狀況。用到的是數據庫內帶的加排他鎖方式,阻止了其餘線程讀取、訪問數據,這要等待操做完畢後其餘線程才能操做,這種方式是悲觀鎖方式。這樣會等待下去。

 

 

使用數據庫的悲觀鎖,是避免了數據併發更新,可是,加鎖畢竟是很耗服務器資源的,用戶要等待下去。因此並不能達到好的性能和高併發。

 

業界使用樂觀鎖的辦法來解決:使用數據庫的樂觀鎖是通用解決辦法。通用鎖實現了版本控制。不會進行排斥掉。減小資源的消耗。

 

樂觀鎖是相對悲觀鎖而言的,使用的是更加寬鬆的鎖定方式。

 

樂觀鎖,通俗說就是:修改數據的時候,不給數據加鎖。

 

既然不加鎖,其餘線程也是能夠訪問、修改數據。可是,修改的時候會獲取一個版本號,只有版本號符合了,纔算更新成功。

 

不成功的,都算搶購失敗。

 

 

 

二、樂觀鎖的具體實現方式

 

 

  樂觀鎖的機制與代碼版本庫svn很類似,這種方式,叫作多版本記錄方式。

 

  若是在我提交代碼以前用本地代碼的版本號與服務器作對比,若是本地版本號小於服務器上最新版本號,則提交失敗,產生衝突代碼,讓用戶決定選擇哪一個版本繼續使用。



  邏輯描述:

 

if(以前讀取到行的版本號+1=數據庫此行如今的版本號+1){

 

      //符合預期,數據庫的數據沒有給其餘用戶修改掉。能夠直接寫入數據了

 

}else{

 

      //數據已經被修改了。因此當前的版本已經落後了。不能進行更新

}

 

例子:

 

給表goods加一個版本字段version,用來記錄行數據值的版本號。

 

版本號version字段,設計成一個正整數,好比是時間戳。每次修改後,要將version字段的值加1:  149691679四、149691679五、149691679六、149691679七、1496916798.................

 

讀數據的時候,順便將版本號的值讀取出來。update時,作版本號對比,以下:

 

一、先讀取這個商品的信息,順便將版本號讀取出來

select  amount,version from t_goods where goods_id=8899;

  

二、更新數據

 

update  goods 
set amount=amount-2,version=version+1
where goods_id=8899  and version=#{version}  and amount>=2;

  

 

sql解釋:

 

#{version}是以前select讀取到的版本號,填入進去,意思是隻能修改這樣的。

 

修改的時候,限制條件-必須版本號等於原來的版本號才能去修改。不然不能修改。更新成功的同時,版本號要加1。

 

優勢:使用數據庫的樂觀鎖是通用解決辦法。通用鎖實現了版本控制。線程之間同時操做,不加鎖,線程不用等待了。減小了數據庫資源的消耗。

 

缺點:會增長cpu的計算開銷。不過值得這樣作,因爲沒有加鎖進行阻塞,用戶不用等待結果,很快能等到執行結果了,用戶體驗更好。搶購的併發數其實提升了。

 

 

 

3、減庫存和下單保持在一個事務內

 

若是不在一個事務內,可能出現兩種現象:

 

一、訂單入庫失敗、減庫存成功。發現訂單入庫失敗,減庫存就不要繼續進行下去了。

 

二、訂單入庫成功、減庫存失敗。實際下了20個訂單,庫存卻沒有減。數據不一致了。

 

 

4、虛擬庫存和真實庫存兩套方案

 

考慮幾種狀況:

 

一、有些人下單完後,最終並不會去付款。若是一下單就立刻減庫存,不少人下單,最終並不會去付款,可能致使庫存數最後爲0,別的用戶沒法下單了。而實際中倉庫中卻有庫存在,這樣庫存數據是不許確的。

二、何時減庫存? 是下單完成減庫存、仍是付款完後減庫存呢?

 

 付款後,才減庫存,可能出現的現象:用戶下完單,接着去付款,結果庫存不夠了。這樣用戶體驗很很差。

 

 下完單就減庫存,可以保證用戶下單隻要付款,就必定能買到這個商品。用戶體驗較好。

 

 針對一些人下單後,不付款,佔着庫存資源,其餘人沒法下單。這個問題好解決,給付款設置一個有效期限,好比30分鐘。超過這個時間,庫存就釋放掉了。

 

 具體技術實現辦法:下單後,立刻減去庫存。另外設置一個定時腳本,掃描超過30分未支付的訂單,把訂單中的商品數量返回到庫存中去。

 

 

  爲何使用虛擬庫存和真實庫存兩套方案?

 

  假設庫存數是50,a訂單購買了5個件商品,支付完畢,庫存數減去5,庫存數變成了45件。因爲尚未發貨,實際庫存中還有50件商品。這樣會出現混淆了。

 

  使用兩套庫存記錄方案是有必要的!

 

  •   下單-操做虛擬庫存數
  •    商品發貨出庫-操做真實庫存數

 

     真實庫存數,記錄下倉庫中這件商品真有多少件。真實庫存,其實很是方便內部人員查看,它只有商品出庫,這個庫存才減。

     虛擬庫存,用來應對商品購買的。代表,還有多少數量能夠給用戶去購買。並不表示倉庫中的庫存數。

 

 

5、頻繁讀庫存的壓力

 

  由於,每次點擊,都要讀取庫存,判斷:有沒有庫存。若是讀庫存走的是數據庫判斷,不少人來搶購的狀況下,數據庫的壓力會很大。

 

  假設是1萬個用戶同時訪問搶購頁面。數據庫接受的訪問次數是1萬個併發。

 

  用戶還要進行刷新頁面操做,因爲每次刷新都會走數據庫判斷庫存。數量會更大。數據庫的壓力就更大了。

 

  因此最好是,把庫存總數,緩存在redis中去。

 

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

 

 

 

 

 

系統抗壓力問題

 

1、如何限流

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

 

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

相關文章
相關標籤/搜索