1. 程式人生 > >解決搶購秒殺抽獎等大流量併發入庫導致的庫存負數的問題

解決搶購秒殺抽獎等大流量併發入庫導致的庫存負數的問題

我們知道資料庫處理sql是一條條處理的,假設購買商品的流程是這樣的:

sql1:查詢商品庫存

if(庫存數量 > 0)
{
  //生成訂單...
  sql2:庫存-1
}

當沒有併發時,上面的流程看起來是如此完美,假設同時兩個人下單,而庫存只有1個了,在sql1階段兩個人查詢到的庫存都是>0的,於是最終都執行了sql2,庫存最後變為-1,超售了,要麼補庫存,要麼等使用者投訴吧。

解決這個問題比較流行的思路:

1.用額外的單程序處理一個佇列,下單請求放到佇列裡,一個個處理,就不會有併發的問題了,但是要額外的後臺程序以及延遲問題,不予考慮。

2.資料庫樂觀鎖,大致的意思是先查詢庫存,然後立馬將庫存+1,然後訂單生成後,在更新庫存前再查詢一次庫存,看看跟預期的庫存數量是否保持一致,不一致就回滾,提示使用者庫存不足。

3.根據update結果來判斷,我們可以在sql2的時候加一個判斷條件update ... where 庫存>0,如果返回false,則說明庫存不足,並回滾事務。

4.藉助檔案排他鎖,在處理下單請求的時候,用flock鎖定一個檔案,如果鎖定失敗說明有其他訂單正在處理,此時要麼等待要麼直接提示使用者"伺服器繁忙"

本文要說的是第4種方案,大致程式碼如下:

阻塞(等待)模式

<?php
$fp = fopen("lock.txt", "w+");
if(flock($fp,LOCK_EX))
{
  //..處理訂單
  flock($fp,LOCK_UN);
}
fclose($fp);
?>

非阻塞模式

<?php
$fp = fopen("lock.txt", "w+");
if(flock($fp,LOCK_EX | LOCK_NB))
{
  //..處理訂單
  flock($fp,LOCK_UN);
}
else
{
  echo "系統繁忙,請稍後再試";
}

fclose($fp);
?>
1、一般避免併發情況可以通過:宣告synchronized、資料庫加鎖、樂觀/悲觀鎖、ThreadLocal物件等來實現 

2、像你這種情況,個人建議: 
1)使用悲觀鎖 
  a)基於jdbc實現的資料庫加鎖如下: 
     select * from account where name="Erica" for update.在更新的過程中,資料庫處於加鎖狀態,任何其他的針對本條資料的操作都將被延遲。本次事務提交後解鎖。 

  b)基於hibernate悲觀鎖的具體實現如下: 
     String sql="查詢語句"; 
     Query query = session.createQuery(sql); 
     query.setLockMode("物件",LockModel.UPGRADE); 
從系統的效能上來考慮,對於單機或小系統而言,這並不成問題,然而如果是在網路上的系統,同時間會有許多聯機,假設有數以百計或上千甚至更多的併發訪問出現,那後果可能難以想象 

2)使用樂觀鎖 
    悲觀鎖大多數情況下依靠資料庫的鎖機制實現,以保證操作最大程度的獨佔性。但隨之而來的就是資料庫效能的大量開銷,特別是對長事務而言,這樣的開銷往往無法承受。 

    樂觀鎖 大多是基於資料版本 (Version)記錄機制實現。何謂資料版本?即為資料增加一個版本標識,在基於資料庫表的版本解決方案中,一般是通過為資料庫表增加一個“version”欄位來 實現。 讀取出資料時,將此版本號一同讀出,之後更新時,對此版本號加一。此時,將提交資料的版本資料與資料庫表對應記錄的當前版本資訊進行比對,如果提交的資料版本號大於資料庫表當前版本號,則予以更新,否則認為是過期資料。 

舉例說明: 
A客戶讀取賬戶餘額1000元,並連帶讀取版本號為5的話, 
B客戶此時也讀取賬號餘額1000元,版本號也為5, 
A客戶在領款後賬戶餘額為500,此時將版本號加1,版本號目前為6,而資料庫中版本號為5,所以予以更新,更新資料庫後,資料庫此時餘額為500,版本號為6, 
B客戶領款後要變更資料庫,其版本號為5,但是資料庫的版本號為6,此時不予更新, 
B客戶資料重新讀取資料庫中新的資料並重新進行業務流程才變更資料庫。 


    需要注意的是:樂觀鎖機制往往基於系統中的資料儲存邏輯,因此也具備一定的局 限性,如在上例中,由於樂觀鎖機制是在我們的系統中實現,來自外部系統的使用者餘額更新操作(即有其他系統使用了跟本系統相同的資料庫,並對其操作)不受我們系統的控制,因此可能會造成髒資料被更新到資料庫中。