秒杀是很常见的一个应用场景,那么高并发竞争下如何解决超卖的问题呢?
先来看下一般做法以及问题是如何发生的:
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,然后都通过了判断库存这一步,进行了减库存操作,这个时候就会出现库存为负数超卖的情况。
测试工具测试后:
而所谓的解决方法的关键,就是要保证 查库存和减库存 操作是原子性的。
方法一:
还是利用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做这种功能不可取。
方法二:
利用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的单个操作是原子性的,所以不管有多少请求过来,都是需要排队一个个执行的。
测试结果: