前言
在商品秒殺活動中,比如商品庫存只有100,但是在搶購活動中可能有200人同時搶購,這樣就出現了並發,在100件商品下單完成庫存為0了還有可能繼續下單成功,就出現了超賣。
為了解決這個問題,今天我主要講一下用redis隊列的方式處理。redis有list類型,list類型其實就是一個雙向鏈表。通過lpush,pop操作從鏈表的頭部或者尾部添加刪除元素。這使得list即可以用作棧,也可以用作隊列。先進先出,一端進,一端出,這就是隊列。在隊列里前一個走完之后,后一個才會走,所以redis的隊列能完美的解決超賣並發的問題。
解決秒殺超賣問題的方法還有比如:1.使用mysql的事務加排他鎖來解決;2.使用文件鎖實現。3.使用redis的setnx來實現鎖機制等。
實現原理
將商品庫存循環lpush到num里,然后在下單的時候通過rpop每次取出1件商品,當num的值為0時,停止下單。
第1步創建表
一共有三張表,分別是:訂單表、商品表、日志表。
1.訂單表
CREATE TABLE `ims_order` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_sn` char(32) NOT NULL, `user_id` int(11) NOT NULL, `status` int(11) NOT NULL DEFAULT '0', `goods_id` int(11) NOT NULL DEFAULT '0', `sku_id` int(11) NOT NULL DEFAULT '0', `number` int(11) NOT NULL, `price` int(10) NOT NULL COMMENT '價格:單位為分', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='訂單表';
2.商品表
CREATE TABLE `ims_hotmallstore_goods` ( `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, `name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '商品名稱', `money` decimal(10, 2) NOT NULL COMMENT '售價', `sales` int(11) NOT NULL COMMENT '銷量', `num` int(11) NOT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; -- ---------------------------- -- Records of ims_hotmallstore_goods -- ---------------------------- INSERT INTO `ims_hotmallstore_goods` VALUES (1, '商品1', 1000.00, 10, 10);
第2步代碼
<?php header("Content-type:text/html;charset=utf-8"); class MyPDO { protected static $_instance = null; protected $dbName = ''; protected $dsn; protected $dbh; /** * 構造 * * @return MyPDO */ private function __construct($dbHost, $dbUser, $dbPasswd, $dbName, $dbCharset) { try { $this->dsn = 'mysql:host='.$dbHost.';dbname='.$dbName; $this->dbh = new PDO($this->dsn, $dbUser, $dbPasswd); $this->dbh->exec('SET character_set_connection='.$dbCharset.', character_set_results='.$dbCharset.', character_set_client=binary'); } catch (PDOException $e) { exit($e->getMessage()); } } /** * 防止克隆 * */ private function __clone() { } /** * Singleton instance * * @return Object */ public static function getInstance($dbHost, $dbUser, $dbPasswd, $dbName, $dbCharset) { if (self::$_instance === null) { self::$_instance = new self($dbHost, $dbUser, $dbPasswd, $dbName, $dbCharset); } return self::$_instance; } /** * Query 查詢 */ public function query($strSql, $queryMode = 'All', $debug = false) { if ($debug === true) $this->debug($strSql); $recordset = $this->dbh->query($strSql); if ($recordset) { $recordset->setFetchMode(PDO::FETCH_ASSOC); if ($queryMode == 'All') { $result = $recordset->fetchAll(); } elseif ($queryMode == 'Row') { $result = $recordset->fetch(); } } else { $result = null; } return $result; } /** * Insert 插入 */ public function insert($table, $arrayDataValue, $debug = false) { $strSql = "INSERT INTO `$table` (`".implode('`,`', array_keys($arrayDataValue))."`) VALUES ('".implode("','", $arrayDataValue)."')"; if ($debug === true) $this->debug($strSql); $result = $this->dbh->exec($strSql); return $result; } /** * 執行語句 */ public function execSql($strSql, $debug = false) { if ($debug === true) $this->debug($strSql); $result = $this->dbh->exec($strSql); return $result; } /** * debug * * @param mixed $debuginfo */ private function debug($debuginfo) { var_dump($debuginfo); exit(); } } class Test { private static $instance = null; // 用單列模式 實例化Redis public static function Redis() { if (self::$instance == null) { $redis=new Redis(); $redis->connect('127.0.0.1',6379); self::$instance = $redis; } return self::$instance; } public function getOne($sql) { $db = MyPDO::getInstance('localhost', 'root', '168168', 'test', 'utf8'); $data = $db->query($sql)[0]; return $data; } public function exec($sql) { $db = MyPDO::getInstance('localhost', 'root', '168168', 'test', 'utf8'); return $db->execSql($sql); } public function insert($table,$data) { $db = MyPDO::getInstance('localhost', 'root', '168168', 'test', 'utf8'); return $db->insert($table,$data); } // 將商品庫存循環到lpush的num里 public function doPageSaveNum() { $redis=self::Redis(); $goods_id=1; $sql="select id, num,money from ims_hotmallstore_goods where id=".$goods_id; $goods = $this->getOne($sql); //print_r($goods);die; if(!empty($goods['num'])) { for ($i=1; $i<=$goods['num']; $i++) { $redis->lpush('num',$i); } die('成功!庫存數:'.$goods['num']); } else { die('數據庫已無庫存'); } } // 搶購下單 public function doPageGoodsStore() { $redis=self::Redis(); $goods_id=1; $user_id = mt_rand(1,100); if ($redis->sismember('user_list_'.$goods_id,$user_id)) { echo '已下單'; return false; ; } $count=$redis->rpop('num'); //每次從num取出1,防止超賣 if($count==0) { $this->echoMsg(0,'已無庫存'); } //加入已購買用戶集合,防止重復購買 $redis->sAdd('user_list_'.$goods_id,$user_id); $sql="select id, num, money from ims_hotmallstore_goods where id=".$goods_id; $goods = $this->getOne($sql); $this->doPageGoodsOrder($user_id,$goods,1); } public function orderNo() { return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8); } // 下單更新庫存 public function doPageGoodsOrder($user_id,$goods,$goods_number) { $orderNo=$this->orderNo(); $number=$goods['num']-$goods_number; if($number<0) { $this->echoMsg(0,'已沒有庫存'); } //mysql判斷已購買用戶 (自行處理) //... $order['user_id']=$user_id; $order['goods_id']=$goods['id']; $order['number']=$goods_number; $order['price']=$goods['money']; $order['status']=1; $order['sku_id']=2; $order['order_sn']=$orderNo; $order['create_time']=date('Y-m-d H:i:s'); $this->insert('ims_order',$order); $sql="update ims_hotmallstore_goods set num=num-".$goods_number." where num>0 and id=".$goods['id']; $res=$this->exec($sql); // echo $sql;die; if(!empty($res)) { echo "庫存扣減成功,庫存剩下:$number".PHP_EOL; return false; } else { $redis=self::Redis(); $redis->lpush('num',$goods_number); //扣庫存失敗,把庫存加回 $redis->SREM('user_list_'.$goods_id,$user_id); //已購買用戶集合移除 $this->echoMsg(0,'庫存扣減失敗'); } } // 保存日志 public function echoMsg($status,$msg,$exit = true) { if($exit == true) { die($msg); } else { echo $msg; } } } if(!isset($_GET['i'])) { exit('缺失參數i'); } // 調用--將商品庫存循環到lpush的num里 if($_GET['i']==1) { $model = new Test; $model->doPageSaveNum(); } // 調用--高並發搶購下單 if($_GET['i']==2) { $model = new Test; $model->doPageGoodsStore(); } if($_GET['i']==3) { $model = new Test; for ($i=1; $i<=100; $i++) { $model->doPageGoodsStore(); } } //http://127.0.0.1/qianggou/test.php?i=1 // ab -n 2000 -c 500 http://127.0.0.1/qianggou/test.php?i=2 // (-n發出2000個請求,-c模擬500並發,請求數要大於或等於並發數。相當2000人同時訪問,后面是測試url )
第3步並發測試
1.先手動執行: http://127.0.0.1/web/index.php?i=1
,將商品庫存循環保存到lpush的num里。
2.這里我用Apache的ab測試,安裝方法本文最后做補充。打開終端,然后執行: ab -n 1000 -c 200 http://127.0.0.1/web/index.php?i=2
(-n發出1000個請求,-c模擬200並發,請求數要大於或等於並發數。相當1000人同時訪問,后面是測試url )
3.查看數據是否超發