//假设用户 $uid 购买 good_id 号商品 $amount 份。
$uid = rand(1,10);
$amount = rand(1,5);
$goods_id = rand(1,6);
$time = time();
//简单实现。
//1 查询商品库存 ,求出此商品剩余库存数。
$sql = "select stock_avail from sec_goods where id = $goods_id";
$stock_avail = Yii::app()->db->createCommand($sql)->queryScalar();
//如果库存足够,则生成定单,扣减商品库存
if( $stock_avail > $amount ){ //份额足够。
$sn = date("YmdHis")."-".$uid."-".$goods_id.rand(1000,9999);
$sql = "insert into sec_order set sn = '$sn',user_id = $uid, goods_id = $goods_id, create_at = $time,num = $amount";
$bool = Yii::app()->db->createCommand($sql)->execute();
if( !$bool ){ throw new Exception("执行失败".$sql); }
$sql = "update sec_goods set stock_avail = stock_avail - $amount where id= $goods_id";
$bool = Yii::app()->db->createCommand($sql)->execute();
if( !$bool ){ throw new Exception("执行失败".$sql); }
}
上述代码,单次跑是ok ,但是 , 一理高并发跑,就容易出现问题。
初始化商品后。
使用 ab -n 1000 -c 1000 http://www.test.com/test 模拟一千次请求后。
出现超卖(爆单)问题。
解决方案
利用 mysql 的行锁 FOR UPDATE 语句和事务的隔离性。注意的是FOR UPDATE仅适用于InnoDB,且必须在事务(BEGIN/COMMIT)中才能生效。
代码如下。
try {
$trans = Yii::app()->db->beginTransaction();
$sql = "select stock_avail from sec_goods where id = $goods_id for update"; //一量查询后,仅当前事务可以对此行数据进行更改,其它事务修改时会被阻塞。
$stock_avail = Yii::app()->db->createCommand($sql)->queryScalar();
if( $stock_avail >= $amount ){ //份额足够。
$sn = date("YmdHis")."-".$uid."-".$goods_id.rand(1000,9999);
$sql = "insert into sec_order set sn = '$sn',user_id = $uid, goods_id = $goods_id, create_at = $time,num = $amount";
$bool = Yii::app()->db->createCommand($sql)->execute();
if( !$bool ){ throw new Exception("执行失败".$sql); }
$sql = "update sec_goods set stock_avail = stock_avail - $amount where id= $goods_id";
$bool = Yii::app()->db->createCommand($sql)->execute();
if( !$bool ){ throw new Exception("执行失败".$sql); }
}
$trans->commit();
} catch (Exception $e) {
//日志记录
$trans->rollback();
}
重新初始化数据,再跑 ab -n 1000 -c 1000 http://www.test.com/test 请求后。
没有出现超卖问题。
---------------------------------------------------------------------
进一步优化,虽然不出现超卖问题,但大量的请求,每次都要查询数据库,给数据库带来了太多不必要的压力。常见项目中,前面会限制单用户单位时间内请求次数, 以及使用redis 队列pop 操作的原子性来实现秒杀。更多请---》 点击查看php使用redis做秒杀