PHP並發搶購解決方案
- 2021 年 3 月 29 日
- 筆記
- php, SQL, 學習筆記, 並發
Mysql版
邏輯步驟
- mysql存儲引擎使用Innodb
- 開始事務,查詢商品庫存並加上共享鎖
- 判斷庫存是否足夠,進行商品/訂單/用戶等操作
- 提交事務,完成下單搶購
程式碼參考
// 關閉自動提交
$this->db_conn->autocommit(FALSE);//開啟事務
// //獲取商品庫存
$query_sql = 'select stock from goods where id ='.$goods_id .' lock in share mode'; //加mysql共享鎖[提交前不允許其他事務修改]
$stock = $this->query($query_sql);
if ($stock < $num) {
return $this->log('庫存不足_'.$stock);
}
//減庫存
$sql1 = 'update goods set stock = stock-'.$num.' where id='.$goods_id;
if (!$this->db_conn->query($sql1)) {
$this->db_conn->rollback();
return $this->log('商品庫存更新失敗sql: '.$sql1.'==>stock: '.$stock);
}
//創建訂單
$order_sn = date('YmdHis') . rand(1000,9999);
$sql2 = 'insert into `order` (order_sn,goods_id,goods_num,user_id) values ("'.$order_sn.'",'.$goods_id.','.$num.','.$user_id.')';
if (!$this->db_conn->query($sql2)) {
$this->db_conn->rollback();
return $this->log('創建訂單失敗: '.$sql2.'==>stock: '.$stock);
}
//提交事務
$this->db_conn->commit();
return true;
Redis版
redis事務/watch/setnx (不限購)
邏輯步驟
- 以商品id生成key,redis獲取庫存,開啟redis監控key和redis事務
- 首次獲取失敗: 數據查詢商品庫存存入redis,
$redis->set($key,$stock,['nx','ex'=>60])'
,(nx
參數: 不存在時才設置,ex
: 時效)
- 非首次獲取成功: 判斷庫存是否足夠
- 開啟資料庫事務,減去庫存,創建訂單
$redis->exec();
redis執行,失敗資料庫rollback
;成功資料庫commit
;
程式碼參考
//連接redis
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->auth('zylwan@Redis123'); //密碼驗證
if (!$redis) {
return $this->log('redis連接失敗');
}
$redis->select(9);//選擇資料庫9
$stock_key = 'goods_id_stock_'.$goods_id; //商品庫存key
$stock = $redis->get($stock_key);
//監控key
$redis->watch($stock_key); //監控下單過程中key是否被修改
//開啟事務
$redis->multi();
if ($stock === false) { //首次設置key
$query_sql = 'select stock from goods where id='.$goods_id;
stock = $this->query($query_sql);
$redis->set($stock_key,$stock,['nx','ex'=>60]); //nx參數: 當key不存在時才設置
}
if ($stock < $num) {
return $this->log('商品庫存不足num: '.$num.'==>stock: '.$stock);
}
$redis->decr($stock_key);//redis減庫存
// 關閉mysql自動提交
$this->db_conn->autocommit(FALSE);//開啟sql事務
$sql1 = 'update goods set stock = stock-'.$num.' where id='.$goods_id; //減mysql庫存
if (!$this->db_conn->query($sql1)) {
$this->db_conn->rollback();
return $this->log('商品庫存更新失敗sql: '.$sql1.'==>stock: '.$stock);
}
//開單
$order_sn = date('YmdHis') . rand(1000,9999);
$sql2 = 'insert into `order` (order_sn,goods_id,goods_num,user_id) values ("'.$order_sn.'",'.$goods_id.','.$num.','.$user_id.')';
if (!$this->db_conn->query($sql2)) {
$this->db_conn->rollback();
return $this->log('創建訂單失敗: '.$sql2.'==>stock: '.$stock);
}
$res = $redis->exec(); //執行redis事務
if ($res === false) { //watch監控到key被修改,redis操作沒有執行==>mysql操作回滾
$this->db_conn->rollback();
return $this->log('redis操作失敗: '.'==>stock: '.$stock);
}
// 都成功執行,提交事務
$this->db_conn->commit();
return true;
redis list/hash (限購)
邏輯步驟
- 搶購前將商品庫存放到隊列list中[庫存隊列] (庫存=隊列長度)
- 搶購開始,將用戶id放到hash隊列中[排隊hash隊列],已存在:跳過.不存在:繼續
- 減庫存操作
lpop(list)
:
- 成功=>資料庫下單等,存儲用戶id到搶購成功list隊列中[搶購成功hash隊列]
- 失敗=>無庫存,搶光了
程式碼參考
$user_id = 1;
$wait_key = "user_wait";//用戶搶購請求hash隊列
$user_key = "user";//用戶搶購成功list隊列
$stock_key = "goods_stock";//商品庫存隊列[在搶購開始前生成]
$result =$redis->hset($wait_key, $user_id, $user_id); //搶購用戶排隊,user_id去重
if ($result) { //排隊成功 => 開始搶購
$count = $redis->lpop($stock_key); //扣減庫存
/**
* list列表的原子性確保了此處並發時的串列
* 即: 確保了下面if判斷中只有與庫存數量相等的人數可以進入到else中搶購下單
*/
if (!$count) { //扣減失敗 => 搶光了
z_log('已經搶光了哦_'.$user_id);
} else { //扣減成功
$redis->lpush($user_key, $user_id);
$result =$redis->hset($user_key, $user_id, $user_id);
//
//.....資料庫下單操作...
//
echo'搶購成功';
}
} else { // 重複排隊=>排隊失敗
echo'請勿重複請求';
}
原文地址: //www.zhuyilong.我愛你/tech/php_concurrent_buying.html