PHP並發搶購解決方案

Mysql版

邏輯步驟

  1. mysql存儲引擎使用Innodb
  2. 開始事務,查詢商品庫存並加上共享鎖
  3. 判斷庫存是否足夠,進行商品/訂單/用戶等操作
  4. 提交事務,完成下單搶購

程式碼參考

    // 關閉自動提交
	$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 (不限購)

邏輯步驟

  1. 以商品id生成key,redis獲取庫存,開啟redis監控key和redis事務
    • 首次獲取失敗: 數據查詢商品庫存存入redis,$redis->set($key,$stock,['nx','ex'=>60])',(nx參數: 不存在時才設置,ex: 時效)
    • 非首次獲取成功: 判斷庫存是否足夠
  2. 開啟資料庫事務,減去庫存,創建訂單
  3. $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 (限購)

邏輯步驟

  1. 搶購前將商品庫存放到隊列list中[庫存隊列] (庫存=隊列長度)
  2. 搶購開始,將用戶id放到hash隊列中[排隊hash隊列],已存在:跳過.不存在:繼續
  3. 減庫存操作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