php + redis实现秒杀系统
秒杀下单
秒杀下单和普通下单的主要区别:
普通下单:查询库存,判断库存,如果有库存则创建订单,如果没有库存则提示库存不足
秒杀下单:秒杀期间一般人数比较大,且访问集中,导致并发大,如果还按照普通下单逻辑,当库存很多的时候没有问题,但是当库存只剩下1个时,如果此时有10个人同时下单,则会出现查询库存时每个人都查询到库存还剩1个,则每个人都下单成功,而实际上就只有1个库存,导致超卖9个,所以秒杀商品时要解决的一个重要问题就是商品超卖的问题
下面我将基于laravel的基础之上,通过redis乐观锁的和redis队列两种方式来实现秒杀时的下单逻辑
方法一:redis乐观锁实现秒杀系统
一、后台设置秒杀商品,同时设置秒杀数量
//添加商品库存
public function addGoodsStock(Request $request)
{
$goods_id = $request->input('goods_id');
$store = $request->input('store');
//设置商品库存的key
$key = 'seckill_goods_id_'.$goods_id;
$res = Redis::set($key,$store);
return $res;
}
二、下单(如果不限制一个用户只能抢购一次,把对用户的判断内容去掉即可)
//用户秒杀商品
public function buy(Request $request)
{
$uid = $request->input('uid');
$goods_id = $request->input('goods_id');
//商品库存key
$key = 'seckill_goods_id_'.$goods_id;
//监听对应的key,事务提交之前,如果key被修改,则事务被打断
Redis::watch($key);
//获取商品库存
$store = Redis::get($key);
//抢购成功用户集合key
$setKey = 'userGoodsSuccess';
//判断该用户是否已经抢购过
//该用户id是否在抢购用户集合中
$userBuyStatus = Redis::sismember($setKey,$uid);
if($userBuyStatus){
return '您已抢过!';
}
if($store){
//记录用户信息,更新库存
//保证这一组命令,要么全部成功,要么都不成功
Redis::multi();//开始事务
Redis::decr($key);//减少库存
//将用户id添加到抢购成功用户集合中
Redis::sadd($setKey,$uid);
$result = Redis::exec();//提交,判断当前的key是否被某个客户端修改了
if($result){
//操作数据库,修改商品库存销量
DB::table('goods')->where('id', $goods_id)->decrement('stock', 1, ['sale' => DB::raw('`sale`+1')]);
//创建订单信息
return '抢购成功!';
}else{
return '抢购失败,请重试!';
}
}else{
return '已抢光!';
}
}
redis乐观锁的原理其实就是mvcc原理,基于版本的控制,读取数据的时候会有一个版本号,修改数据的时候会判断版本号是否一致,如果一致则允许修改,如果不一致,则说明已被别的请求修改,则事务失败,所以如果并发量不是很大,使用DB数据库也可以使用相同的原理来实现,但是通常不会用,所以这里就不描述了
方法二、redis队列实现秒杀系统
一:初始化库存队列
/**
* redis队列
* 初始化库存队列
* @param Request $request
* @return string
*/
public function init(Request $request)
{
// 商品库存
$goods_id = $request->input('goods_id');
$store = $request->input('store');
$key = 'list_seckill_goods_id_'.$goods_id;
//防止对已经设置过的商品库存进行覆盖
if(!empty(Redis::llen($key))) {
return '已经设置了库存';
}
// 初始化缓存,删除抢购用户id队列key和成功信息保存key,这个是抢购时生成的,防止错乱,初始化删除
$userListKey = 'user_goods_id_'.$goods_id;
Redis::command('del', [$userListKey, 'success']);
// 将商品存入Redis链表中
for($i = 1; $i <= $store; $i++) {
Redis::lpush($key, $i);
}
// 设置过期时间
// Redis::expire($key, 120);
echo '商品存入队列成功,数量:'.Redis::llen($key);
}
二、抢购
/**
* redis队列
* 抢购
* @param Request $request
* @return string
*/
public function start(Request $request)
{
// 模拟随机登录用户
$uid = mt_rand(1, 9999);
// $uid = $request->input('uid');
$goods_id = $request->input('goods_id');
$key = 'list_seckill_goods_id_'.$goods_id;
// 从链表的头部删除一个元素,返回删除的元素,因为pop操作是原子性,即使很多用户同时到达,也是依次执行
$count = Redis::lpop($key);
if (!$count) {
return '已抢光!';
}
//已抢购用户id队列
$userListKey = 'user_goods_id_'.$goods_id;
//判断该用户是否已经抢购过
//该用户id是否在抢购用户集合中
$userBuyStatus = Redis::sismember($userListKey,$uid);
if($userBuyStatus){
return '您已抢过!';
}
//将用户id添加到抢购成功用户集合中
Redis::sadd($userListKey,$uid);
$msg = '抢到的人为:'.$uid.',抢到商品的顺序为第'.$count.'个';
Redis::lpush('success', $msg);
//操作数据库,修改商品库存销量
DB::table('goods')->where('id', $goods_id)->decrement('stock', 1, ['sale' => DB::raw('`sale`+1')]);
//创建订单信息
return '恭喜您抢购成功!';
}
方法二是通过redis队列的方式实现秒杀逻辑,利用的是redis队列的原子性,即使是多个请求同时到达也是按顺序一个一个进行
实现秒杀系统防止超卖的方法还有很多,比如:如果不考虑性能的情况下,可以将MySQL数据库的库存字段设置为无符号unsigned,这样当库存为负数时sql就无法执行,但是这样的话,mysql数据库将直接承受并发压力,或者如上面所说mysql通过mvcc的设计模式,一样可以解决超卖的问题,同样并发压力都由mysql直接承担
所以:redis乐观锁和redis队列的方式扔是现在比较流行且性能较好的实现方式
————————————————
转载链接:https://blog.csdn.net/weixin_45310179/article/details/120782194
更多推荐
所有评论(0)