秒杀之如何避免超卖

2017-11-02 22:08:16  对羊弹琴

秒杀是很常见的一个应用场景,那么高并发竞争下如何解决超卖的问题呢?

先来看下一般做法以及问题是如何发生的:

try {
    $pdo = new PDO('mysql:host=127.0.0.1:3306;dbname=testdb', 'root', '');
    $sql = 'SELECT num FROM stock_num WHERE id=1';
    $stm = $pdo->prepare($sql);
    $stm->execute();
    $result = $stm->fetchAll(PDO::FETCH_ASSOC);
    if ($result[0]['num'] > 0) {//判断库存
        sleep(1);//放大测试效果
        $sql = 'UPDATE stock_num SET num=num-1';
        $stm = $pdo->prepare($sql);
        if (!$stm->execute()) {
            echo '秒杀失败.';die;
        }
        $sql = 'INSERT INTO orders(uid) values(:uid)';
        $stm = $pdo->prepare($sql);
        $stm->bindParam(':uid', $_POST['uid'], PDO::PARAM_INT);
        if ($stm->execute()) {
            echo '秒杀成功.';
        } else {
            echo '秒杀失败.';
        }
    } else {
        echo '秒杀已结束.';
    }
} catch (PDOException $e) {
    echo '连接失败.';
}

逻辑就是查出商品数量,并减库存然后写入订单。问题就出现在这个查库存和减库存这个操作不是原子性的,比如库存刚好还剩1时,A,B用户都查到库存为1,然后都通过了判断库存这一步,进行了减库存操作,这个时候就会出现库存为负数超卖的情况。

测试工具测试后:

asljgahsfjhlajsfljuoiwrg1520513372png.png

而所谓的解决方法的关键,就是要保证 查库存和减库存 操作是原子性的。

方法一:

还是利用MySQL,利用MySQL的排他锁。

直接上代码:

try {
    $pdo = new PDO('mysql:host=127.0.0.1:3306;dbname=testdb', 'root', '');
    try {
        $pdo->beginTransaction();
        $sql = 'SELECT num FROM stock_num WHERE id=1 FOR UPDATE';//排他锁
        $stm = $pdo->prepare($sql);
        $stm->execute();
        $result = $stm->fetchAll(PDO::FETCH_ASSOC);
        if ($result[0]['num'] > 0) {//判断库存
            sleep(1);//放大测试效果
            $sql = 'UPDATE stock_num SET num=num-1 WHERE id=1';
            $stm = $pdo->prepare($sql);
            if (!$stm->execute()) {
                throw new \Exception;
            }
            $sql = 'INSERT INTO orders(uid) values(:uid)';
            $stm = $pdo->prepare($sql);
            $stm->bindParam(':uid', $_POST['uid'], PDO::PARAM_INT);
            if ($stm->execute()) {
                $pdo->commit();
                echo '秒杀成功.';
            } else {
                throw new \Exception;
            }
        } else {
            echo '秒杀已结束.';
        }
    } catch(\Exception $e) {
        $pdo->rollBack();
        echo '秒杀失败.';
    }
} catch (PDOException $e) {
    echo '连接失败.';
}

因为需要利用MySQL的排他锁,所以要求表引擎为innodb。需要指定是排他锁,否则默认共享锁还是会造成超卖效果。但秒杀这种应用场景请求量大,在这种高并发情况下用mysql做这种功能不可取。

agasfdlakshgjalfsklsf1520516307png.png

方法二:

利用redis的原子性来实现。

redis原子性:
    • 单个操作是原子性的
    • 多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来

直接上代码:

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

if ($redis->lpop('goods')) {
    sleep(1);
    if ($redis->sadd('orders', $_POST['uid'])) {
        echo '秒杀成功.';
    } else {
        $redis->rpush('goods', 1);
        echo '您已秒杀成功.';
    }
} else {
    echo '秒杀结束.';
}

goods是一个list,有多少库存就有多少个成员,成员为true值。因为redis的单个操作是原子性的,所以不管有多少请求过来,都是需要排队一个个执行的。

测试结果:

agasfdlasdfasfdasfdklsf1520517481png.png


评论(0) 最后更新于 2019-03-12 21:37:26