1. 程式人生 > >redis秒殺

redis秒殺

模擬 實現 lpush 高並發 假設 多線程 並發 一個用戶 用戶

redis秒殺

Redis原子性原理 摘要: 1、Redis是單進程單線程的網絡模型,用的是epoll,poll,select網絡模型,這些網絡模型都是單線程處理網絡請求 2、Redis的單線程處理所有的客戶端連接請求,命令讀寫請求。(有些任務比如rdb和aof等操作是fork子進程處理的,不會影響redis主線程處理客戶端的命令) 3、Redis提供的所有API操作,相對於服務端方面都是one by one執行的,命令是一個接著一個執行的,不存在並行執行的情況。 4、Redis客戶端就可能會出現高並發出現錯誤的讀寫數據,下面我們舉個電商秒殺的例子來講解一下。 Redis在並發中的表現 Redis的API是原子性的操作,那麽多個命令在並發中也是原子性的嗎? 看看下面這段代碼: <?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 ‘已經搶光了哦‘; 雖然llen和lpush2個操作如果單獨執行是具備原子性的,然而上面這個業務2個命令組合起來才算是完成一個業務,但是2個命令組合起來就不具備原子性,所有在兩個命令之間其他客戶端會出現讀寫臟數據的情況。 在搶購進行到一定程度,假如現在已經有9個人搶購成功,又來了3個用戶同時搶購,三個用戶假設有先後順序,但是在第一個用戶lpush到redis之前,其他的兩個用戶其實已經獲取push之前的長度9了,這時if條件將會被繞過(條件同時被滿足了),這三個用戶都能搶購成功。而實際上只剩下一件庫存可以搶了。 在高並發下,很多看似不大可能是問題的,都成了實際產生的問題了。要解決“超搶/超賣”的問題,核心在於保證檢查庫存時的操作是依次執行的,再形象的說就是把“多線程”轉成“單線程”。即使有很多用戶同時到達,也是一個個檢查並給與搶購資格,一旦庫存搶盡,後面的用戶就無法繼續了。 我們需要使用redis的原子操作來實現這個“單線程”。首先我們把庫存存在goods_store:1這個列表中,假設有10件庫存,就往列表中push10個數,這個數沒有實際意義,僅僅只是代表一件庫存。搶購開始後,每到來一個用戶,就從goods_store:1中pop一個數,表示用戶搶購成功。當列表為空時,表示已經被搶光了。因為列表的pop操作是原子的,即使有很多用戶同時到達,也是依次執行的。搶購的示例代碼如下: 比如這裏我先把庫存(可用庫存,這裏我強調下哈,一般都是商品詳情頁搶購,後來者進來看到的庫存可能不再是後臺系統配置的10個庫存數了)放入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庫存字段 用戶搶購成功後,上面的我們也可以稍微優化下,比如我們可用將用戶ID存入了order:1列表中。接下來我們可以引導這些用戶去完成訂單的其他步驟,到這裏才涉及到與數據庫的交互。最終只有很少的人走到這一步吧,也就解決的數據庫的壓力問題。 我們再改下上面的代碼: $user_id = \Session::get(‘user_id‘);//當前搶購用戶id /* 模擬搶購操作,搶購前判斷redis隊列庫存量 */ $count=\Redis::lpop(‘goods_store:1‘); if(!$count)   return ‘已經搶光了哦‘; $result = \Redis::lpush(‘order:1‘,$user_id); if($result)   return ‘恭喜您!搶到了哦‘; 為了檢測實際效果,我使用jmeter工具模擬100、200、1000個用戶並發進行搶購,經過大量的測試,最終搶購成功的用戶始終為10,沒有出現“超搶/超賣” ----------------

redis秒殺