1. 程式人生 > 實用技巧 >PHP+Redis+MySQL商品秒殺與超賣!

PHP+Redis+MySQL商品秒殺與超賣!

如果你家店裡某商品庫存只有100件,現在店慶活動5折優惠大酬賓,假如現在有200個人瘋狂湧入你家店裡,為了避免發生瘋搶和踩踏事件發生,店長您採取了排隊限購的辦法,1人限購1件,排隊先到先買,賣完為止。

其實我們也可以採取排隊限購的辦法解決網店秒殺活動商品超賣的問題。今天我們給大家講解採用PHP+Redis+MySQL解決商品秒殺活動中超賣問題。

實現原理

把商品庫存數量加到redis佇列的num裡,下單的時候通過rpop從佇列中每次取1件商品,當num為0時,停止下單。

下面我們來看具體實現過程。

建立資料表

我們一共準備3張表,分別是:商品表、訂單表、日誌表。

1.商品表

CREATE TABLE `ms_goods` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(128) DEFAULT NULL COMMENT '商品名稱',
  `price` decimal(10,2) DEFAULT NULL COMMENT '商品價格',
  `pic` varchar(128) DEFAULT NULL COMMENT '商品圖片',
  `inventory` int(11) DEFAULT NULL COMMENT '庫存',
  `created_at` datetime DEFAULT NULL,
  `updated_at` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of ms_goods
-- ----------------------------
INSERT INTO `ms_goods` VALUES ('1', 'Apple iPhone 11 (A2223) 64GB 黑色 移動聯通電信4G手機 雙卡雙待', '5499.00', null, '100', '2019-09-20 16:21:05', '2019-09-20 16:21:08');

我們在商品表中新增商品Apple iPhone 11,設定庫存為100。

2.訂單表

CREATE TABLE `ms_order` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `order_sn` varchar(32) DEFAULT NULL COMMENT '訂單號',
  `user_id` int(11) DEFAULT NULL COMMENT '購買者ID',
  `status` tinyint(1) DEFAULT '0' COMMENT '訂單狀態1-已下單,2-已處理,3-已發貨,4-已收貨,5-訂單完成',
  `goods_id` int(11) DEFAULT '0' COMMENT '商品id',
  `o_num` int(11) DEFAULT NULL COMMENT '購買數量',
  `price` int(10) DEFAULT NULL COMMENT '價格,分',
  `created_at` datetime DEFAULT NULL,
  `updated_at` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

3.日誌表

CREATE TABLE `ms_order_log` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `status` int(11) DEFAULT '0',
  `msg` text,
  `created_at` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

加入庫存佇列

我們在Redis中加入商品庫存佇列。由商品表中我們可知商品Apple iPhone 11庫存有100件。我們可以寫個指令碼將商品庫存加入到Redis佇列中。

for($i=1; $i <= 100; $i++){
    $redis->lpush('num', $i);
}

執行完成後,我們可以看到redis佇列。



下單購買

我們建立下單檔案Order.php

首先是連線redis和mysql的程式碼。

class Order
{
    private static $redis = null;
    private static $pdo = null;
    public static function Redis()
    {
        if (self::$redis == null) {
            $redis = new Redis();
            $redis->connect('127.0.0.1',6379);
            self::$redis = $redis;
        }
        return self::$redis;
    }
    public static function mysql()
    {
        $dbhost = '127.0.0.1'; //資料庫伺服器
        $dbport = 3306; //埠
        $dbname = 'demo'; //資料庫名稱
        $dbuser = 'root'; //使用者名稱
        $dbpass = ''; //密碼
        // 連線
        try {
            $db = new PDO('mysql:host='.$dbhost.';port='.$dbport.';dbname='.$dbname, $dbuser, $dbpass);
            $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); //設定錯誤模式
            $db->query('SET NAMES utf8;');
            self::$pdo = $db;
        } catch (PDOException $e) {
            $this->log(0, '連線資料庫失敗!');
            exit;
        }
        return self::$pdo;
    }
}

接著就是搶購下單。我們從商品可以中取出商品資訊,然後從redis佇列num中rpop出列一個商品數,接著馬上處理商品購買的過程。

// 搶購下單
    public function goodsOrder()
    {
        $redis = self::Redis();
        $db = self::mysql();
        $goodsId = 1;
        $sql = "select id,inventory,price from ms_goods where id=".$goodsId;
        $stmt = $db->query($sql);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        $redis = self::Redis();
        $count = $redis->rpop('num');//每次從num取出1
        if($count == 0){
            $this->log(0, 'no num redis');
            echo '已沒有庫存';
        } else {
            $this->doOrder($row, 1);
        }
    }

上述程式碼中,如果redis佇列數量變成0了,就是沒有庫存了,這個時候不做訂單處理了,如果不是0就要更新庫存,生成訂單。

// 下單更新庫存
    public function doOrder($goods, $goodsNum)
    {
        $orderNo = $this->orderNo();
        $number = $goods['inventory'] - $goodsNum;
        if ($number < 0) {
            $this->log(0, '已沒有庫存');
            echo '已沒有庫存';
            return false;
        }
        $db = self::mysql();
        try {
            $db->beginTransaction(); //啟動事務
            $sql = "INSERT INTO `hw_order` (user_id,order_sn,status,goods_id,o_num,price,created_at) VALUES (:user_id,:order_sn,:status,:goods_id,:sku_id,:o_num,:price,:created_at)";
            $stmt = $db->prepare($sql);
            $stmt->execute([
                ':user_id' => rand(1, 500),
                ':order_sn' => $orderNo,
                ':status' => 1,
                ':goods_id' => $goods['id'],
                ':o_num' => $goodsNum,
                ':price' => $goods['price'] * 100,
                ':created_at' => date('Y-m-d H:i:s'),
            ]);
            $sql2 = "update hw_goods set inventory=inventory-".$goodsNum." where inventory>0 and id=".$goods['id'];
            $res = $db->exec($sql2);
            
            $db->commit(); //提交事務
            $this->log(1, '下單和庫存扣減成功');
        } catch (Exception $e) {
            $db->rollBack(); //回滾事務
            $this->log(0, '下單失敗');
        }
    }

在下單過程中,我們採用了MySQL的事物機制,每次當訂單表中寫入訂單資料並且商品表扣除庫存-1成功,才算下單完成。

最後附上生產訂單號的程式碼,以及日誌記錄程式碼。

// 生成訂單號
    public function orderNo()
    {
        return date('Ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
    }
    // 儲存日誌
    public function log($status, $msg)
    {
        $db = self::mysql();
        $sql = "INSERT INTO `ms_order_log` (status,msg,created_at) VALUES (:status,:msg,:created_at)";
        $stmt = $db->prepare($sql);
        $stmt->execute([
            ':msg' => $msg,
            ':status' => $status,
            ':created_at' => date('Y-m-d H:i:s')
        ]);
    }

呼叫下單程式碼:

$order = new Order();
$order->goodsOrder();

詳細程式碼請點選文章上部的下載按鈕,移動版使用者不提供下載。

併發測試

我們Apache的ab測試,ab是apachebench命令的縮寫,是Apache自帶的壓力測試工具,假如你安裝了Apache軟體後,在他的bin目錄下可以找到ab這個程式。

保證你的order.php在你的站點能訪問到,然後啟動ab測試,輸入以下命令:

ab -n 1000 -c 200 http://localhost/order.php

(-n發出1000個請求,-c模擬200併發,請求數要大於或等於併發數。相當1000人同時訪問,後面是測試url )。

執行結果如圖:



驗證結果

分別檢視商品表ms_goods,檢驗庫存欄位inventory是否由100變成0了。

檢視訂單表ms_order,查詢該商品的訂單總數是否為100。

檢視日誌表ms_order_log,查詢狀態status為1的訂單日誌記錄是否是100條,其餘的狀態均為0。

經驗證,庫存為0,訂單總數為100,並沒有出現超賣的現象。