这篇文章主要介绍了关于php结合redis 秒杀商品的详解,有着一定的参考价值,现在分享给大家,有需要的朋友可以参考一下
1 首先,一点点准备工作。
1.1建立商品表,订单表,并初始化数据
订单表。
1.2 将商品数据写入到redis 队列中去。
例如编号1 商品有100件。 就往 goods_1 队列里写100个1 进去。例用pop 操作的原子性(扛并发) 后面购买时,买一个就pop 一个。//代码使用yii 框架,重点在思路,其它框架做少量调整即可。
$redis = self::createRedisObj(); //创建redis 对象,后面提供详细代码
$sql = "select * from sec_goods";
$rows = Yii::app()->db->createCommand($sql)->queryAll();
foreach( $rows as $key => $row ):
$goods_id = $row["id"];
$stock_avail = $row["stock_avail"];
$redis_key = "goods_".$goods_id;
for($i =0 ; $i< $stock_avail; $i++){
$redis->lpush($redis_key , 1);
}
echo $goods_id."llen is ".$redis->lLen($redis_key)."
";
endforeach;
建好后如下图。(真实情况下,后台可能出现库调整,需要对应的去同步redis 中的数据,实际项目中请留意,此处暂且不表)
2 无redis时,常规的购买代码。//用随机值模拟客户,商品,单次购买份数
$uid = rand(1,10);
$amount = rand(1,5);
$goods_id = rand(1,6);
$time = time();
$this->buy($uid , $goods_id , $amount);
public function buy($uid , $goods_id , $amount){
//使用行锁.
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();
return true;
} catch (Exception $e) {
//日志记录
$trans->rollback();
return false;
}
}
然后使用 apache 的 ab 小工具进行测试。
-n 代表请求次数 -c 代表单次并发多少个请求。
ab -n 1000 -c 100 http://xxx
(把上述代码中的事务去掉,再ab 跑时会出现爆单,超卖问题 详情点击)
由于使用了行锁 for update。 基于事务的隔离性,一定是顺序的执行,所以上述代码,也不会出现超卖爆单问题。(10件库存卖出11件),但这样的代码,有个性能问题,就是有多少次并发请求,就会往数据库请求多少次。脆弱的mysql 很快就崩掉了。
3 结redis的秒杀代码。
终于上正菜了。。。。//code 3.1
//用随机值模拟客户,商品,单次购买份数
$uid = rand(1,10);
$amount = rand(1,5);
$goods_id = rand(1,6);
$time = time();
//用redis 校验,此次用户是否可以买。(库存是否充足)
$redis = self::createRedisObj();
$redis_key = "goods_".$goods_id;
$len = $redis->lLen($redis_key); //求队列的长度,也就是商品的库存。
if( $len == 0 ){ exit("抢光了!"); }
else if( $len < $amount){ exit("库存不足!"); }
//验证通过,开始pop 出队列。 pop 一个,相当于买一个。
for( $i =0 ; $i< $amount;$i++){
$redis->rPop( $redis_key );
}
$bool = $this->buy($uid , $goods_id , $amount);
if( !$bool ) { //如果购买失败,则把取出的redis 队列的数据,再压回去。(回充库存)
for( $i =0 ; $i< $amount;$i++){
$redis->lPush( $redis_key , 1);
}
}
//创建redis 对象的。
private static $_redis = null;
/**
* 创建一个redis 对象.
* @return Redis
*/
public static function createRedisObj(){ //2017-11-29 改为单例创建模式.
if( ! self::$_redis){
$redis = new Redis();
$host = “192.168.1.xx”;
$port = "6379";
$redis->connect($host,$port);
self::$_redis = $redis;
}
return self::$_redis;
}
注意一个小细节,通常redis 会结合框架做缓存。 上述例子中,请注意在创redis 对象时,再单独指定一个库。(redis 一般有9个可选),避免服务器清缓存时将数据清空。
致此,上述代码完成了一个基础版本。
------------------------------------------------------------------------------------------------
然而在现象中,会随运营需求不断的产生变化。
随便举一例。
1 产品希望单用户3秒内仅能买一次。 无商品
2 产品希望单用户单个商品最多限买5 件。
碰到这种情况
问题1 处理如下//用随机值模拟客户,商品,单次购买份数
$uid = rand(1,10);
$amount = rand(1,5);
$goods_id = rand(1,6);
$time = time();
$redis = self::createRedisObj();
单用户限3秒内仅允许请求一次///
$lock_key = "uTimeLimit_".$uid;
//按用户名编即可。 如果限用户针对指定商品,则lock_key 按uid+ goods_id 进行唯一编码
if( $redis->get($lock_key)){
$left_time = $redis->ttl($lock_key);
exit($expire ."秒内只允许 $tag 一次!请".$left_time."之后再尝试");
}else {
$redis->setEx($lock_key , $expire , "1" );
}
//
//用redis 校验,此次用户是否可以买。(库存是否充足)
$len = $redis->lLen($redis_key); //求队列的长度,也就是商品的库存。
if( $len == 0 ){
exit("抢光了!");
}else if( $len < $amount){
exit("库存不足!");
}
// 后序购买流程。。。。。
问题2 修改如下。//用随机值模拟客户,商品,单次购买份数
$uid = rand(1,10);
$amount = rand(1,5);
$goods_id = rand(1,6);
$time = time();
$redis = self::createRedisObj();
单用户限5个处理///
//设一个hash 表, "user_buy" 然后 $uid . "_" . $goods_id 来记录购买的份数。
$already_num = $redis->hGet("user_buy",$uid."_".$goods_id)? $redis->hGet("user_buy",$uid."_".$goods_id)
:0; //求出已购买份额
if( $already_num +$amount > 5){ exit("单用户单个商品限购买5个");}
else{
$new_num = $already_num +$amount ;
$redis->hSet("user_buy",$uid."_".$goods_id , $new_num);
}
用redis 校验,此次用户是否可以买。(库存是否充足)
$len = $redis->lLen($redis_key); //求队列的长度,也就是商品的库存。
if( $len == 0 ){ exit("抢光了!"); }
else if( $len < $amount){ exit("库存不足!"); }// 后序购买流程。。。。。
//如果购买失败
$redis->hSet(
"user_buy",$uid."_".$goods_id ,
$redis->hGet("user_buy",$uid."_".$goods_id ) - $amount //失败时,则回复hash 表的数值
);
这两个问题处理完毕,其它类似问题见招拆招。 有心的读者可以发现,这类修改有共同之处,可以适当封装下代码,更好的复用。
相关推荐: