php 用redis实现限时抢购,并且防止超卖和重复购买
前言
在商品秒殺活動中,比如商品庫存只有100,但是在搶購活動中可能有200人同時搶購,這樣就出現(xiàn)了并發(fā),在100件商品下單完成庫存為0了還有可能繼續(xù)下單成功,就出現(xiàn)了超賣。
為了解決這個問題,今天我主要講一下用redis隊列的方式處理。redis有l(wèi)ist類型,list類型其實就是一個雙向鏈表。通過lpush,pop操作從鏈表的頭部或者尾部添加刪除元素。這使得list即可以用作棧,也可以用作隊列。先進先出,一端進,一端出,這就是隊列。在隊列里前一個走完之后,后一個才會走,所以redis的隊列能完美的解決超賣并發(fā)的問題。
解決秒殺超賣問題的方法還有比如:1.使用mysql的事務加排他鎖來解決;2.使用文件鎖實現(xiàn)。3.使用redis的setnx來實現(xiàn)鎖機制等。
實現(xiàn)原理
將商品庫存循環(huán)lpush到num里,然后在下單的時候通過rpop每次取出1件商品,當num的值為0時,停止下單。
第1步創(chuàng)建表
一共有三張表,分別是:訂單表、商品表、日志表。
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;
}
/**
* 執(zhí)行語句
*/
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);
}
// 將商品庫存循環(huán)到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('成功!庫存數(shù):'.$goods['num']);
} else {
die('數(shù)據(jù)庫已無庫存');
}
}
// 搶購下單
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('缺失參數(shù)i');
}
// 調用--將商品庫存循環(huán)到lpush的num里
if($_GET['i']==1) {
$model = new Test;
$model->doPageSaveNum();
}
// 調用--高并發(fā)搶購下單
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發(fā)出2000個請求,-c模擬500并發(fā),請求數(shù)要大于或等于并發(fā)數(shù)。相當2000人同時訪問,后面是測試url )
第3步并發(fā)測試
1.先手動執(zhí)行:http://127.0.0.1/web/index.php?i=1,將商品庫存循環(huán)保存到lpush的num里。
2.這里我用Apache的ab測試,安裝方法本文最后做補充。打開終端,然后執(zhí)行:ab -n 1000 -c 200 http://127.0.0.1/web/index.php?i=2
(-n發(fā)出1000個請求,-c模擬200并發(fā),請求數(shù)要大于或等于并發(fā)數(shù)。相當1000人同時訪問,后面是測試url )
3.查看數(shù)據(jù)是否超發(fā)
總結
以上是生活随笔為你收集整理的php 用redis实现限时抢购,并且防止超卖和重复购买的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 路由器应该怎么修改密码大唐820路由器如
- 下一篇: 如何制定团队制度