redis實現高併發下的搶購/秒殺功能

1, http://www.cnblogs.com/phpper/p/6716248.htmlphp

https://www.cnblogs.com/phpper/p/7085663.htmlhtml

https://www.cnblogs.com/TankXiao/p/4045439.htmlmysql

悲觀鎖(Pessimistic Lock)

悲觀鎖的特色是先獲取鎖,再進行業務操做,即「悲觀」的認爲獲取鎖是很是有可能失敗的,所以要先確保獲取鎖成功再進行業務操做。一般所說的「一鎖二查三更新」即指的是使用悲觀鎖。一般來說在數據庫上的悲觀鎖須要數據庫自己提供支持,即經過經常使用的select … for update操做來實現悲觀鎖。當數據庫執行select for update時會獲取被select中的數據行的行鎖,所以其餘併發執行的select for update若是試圖選中同一行則會發生排斥(須要等待行鎖被釋放),所以達到鎖的效果。select for update獲取的行鎖會在當前事務結束時自動釋放,所以必須在事務中使用。laravel

這裏須要注意的一點是不一樣的數據庫對select for update的實現和支持都是有所區別的,例如oracle支持select for update no wait,表示若是拿不到鎖馬上報錯,而不是等待,mysql就沒有no wait這個選項。另外mysql還有個問題是select for update語句執行中全部掃描過的行都會被鎖上,這一點很容易形成問題。所以若是在mysql中用悲觀鎖務必要肯定走了索引,而不是全表掃描。redis

 

 

樂觀鎖(Optimistic Lock)

樂觀鎖是否在事務中其實都是無所謂的,其底層機制是這樣:在數據庫內部update同一行的時候是不容許併發的,即數據庫每次執行一條update語句時會獲取被update行的寫鎖,直到這一行被成功更新後才釋放。所以在業務操做進行前獲取須要鎖的數據的當前版本號,而後實際更新數據時再次對比版本號確認與以前獲取的相同,並更新版本號,便可確認這之間沒有發生併發的修改。若是更新失敗便可認爲老版本的數據已經被併發修改掉而不存在了,此時認爲獲取鎖失敗,須要回滾整個業務操做並可根據須要重試整個過程。好吧,在此嘮叨總結下這兩個鎖:sql

總結

  • 樂觀鎖在不發生取鎖失敗的狀況下開銷比悲觀鎖小,可是一旦發生失敗回滾開銷則比較大,所以適合用在取鎖失敗機率比較小的場景,能夠提高系統併發性能數據庫

  • 樂觀鎖還適用於一些比較特殊的場景,例如在業務操做過程當中沒法和數據庫保持鏈接等悲觀鎖沒法適用的地方多線程

 

以前寫過一篇文章,高併發的解決思路(點此進入查看),今天再次抽空整理下實際場景中的具體代碼邏輯實現吧:
搶購/秒殺是現在很常見的一個應用場景,那麼高併發競爭下如何解決超搶(或超賣庫存不足爲負數的問題)呢?併發

常規寫法:oracle

查詢出對應商品的庫存,看是否大於0,而後執行生成訂單等操做,可是在判斷庫存是否大於0處,若是在高併發下就會有問題,致使庫存量出現負數

這裏我就只談redis的解決方案吧...
咱們先來看如下代碼(這裏我以laravel爲例吧)是否能正確解決超搶/賣的問題:

<?php

  $num = 10; //系統庫存量
  $user_id = \Session::get('user_id');//當前搶購用戶id
  $len = \Redis::llen('order:1'); //檢查庫存,order:1 定義爲健名
  if($len >= $num)
    return '已經搶光了哦';

  $result = \Redis::lpush('order:1',$user_id); //把搶到的用戶存入到列表中
  if($result)
    return '恭喜您!搶到了哦';

?>

若是代碼正常運行,按照預期理解的是列表order:1中最多隻能存儲10個用戶的id,由於庫存只有10個。
然而,可是,在使用jmeter工具模擬多用戶併發請求時,最後發現order:1中老是超過5個用戶,也就是出現了「超搶/超賣」。
分析問題就出在這一段代碼:

 $len = \Redis::llen('order:1'); //檢查庫存,order:1 定義爲健名 if($len >= $num)   return '已經搶光了哦';



在搶購進行到必定程度,假如如今已經有9我的搶購成功,又來了3個用戶同時搶購,這時if條件將會被繞過(條件同時被知足了),
這三個用戶都能搶購成功。而實際上只剩下一件庫存能夠搶了。
在高併發下,不少看似不大多是問題的,都成了實際產生的問題了。要解決「超搶/超賣」的問題,
核心在於保證檢查庫存時的操做是依次執行的,再形象的說就是把「多線程」轉成「單線程」。
即便有不少用戶同時到達,也是一個個檢查並給與搶購資格,一旦庫存搶盡,後面的用戶就沒法繼續了。

  咱們須要使用redis的原子操做來實現這個「單線程」。首先咱們把庫存存在goods_store:1這個列表中,假設有10件庫存,就往列表中push10個數,
這個數沒有實際意義,僅僅只是表明一件庫存。搶購開始後,每到來一個用戶,就從goods_store:1中pop一個數,
表示用戶搶購成功。當列表爲空時,表示已經被搶光了。由於列表的pop操做是原子的,即便有不少用戶同時到達,也是依次執行的。搶購的示例代碼以下:

好比這裏我先把庫存放入redis隊列:
$num=10; //庫存
 $len=\Redis::llen('goods_store:1'); //檢查庫存,goods_store:1 定義爲健名
 $count = $num-$len; //實際庫存-被搶購的庫存 = 剩餘可用庫存
 for($i=0;$i<$count;$i++)
   \Redis::lpush('goods_store:1',1);//往goods_store列表中,未搶購以前這裏應該是默認滴push10個庫存數了

 //echo \Redis::llen('goods_store:1');//未搶購以前這裏就是10了
好吧,搶購時間到了:
/* 模擬搶購操做,搶購前判斷redis隊列庫存量 */
 $count=\Redis::lpop('goods_store:1');//lpop是移除並返回列表的第一個元素。
 if(!$count)
    return '已經搶光了哦';
 
 
/* 下面處理搶購成功流程 */
\DB::table('goods')->decrement('num', 1);//減小num庫存字段
相關文章
相關標籤/搜索