redis秒殺

redis秒殺

Redis原子性原理
 
摘要:
 
一、Redis是單進程單線程的網絡模型,用的是epoll,poll,select網絡模型,這些網絡模型都是單線程處理網絡請求
 
二、Redis的單線程處理全部的客戶端鏈接請求,命令讀寫請求。(有些任務好比rdb和aof等操做是fork子進程處理的,不會影響redis主線程處理客戶端的命令)
 
三、Redis提供的全部API操做,相對於服務端方面都是one by one執行的,命令是一個接着一個執行的,不存在並行執行的狀況。
 
四、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,沒有出現「超搶/超賣」
----------------
相關文章
相關標籤/搜索