秒杀下单和普通下单的主要区别:

普通下单:查询库存,判断库存,如果有库存则创建订单,如果没有库存则提示库存不足

秒杀下单:秒杀期间一般人数比较大,且访问集中,导致并发大,如果还按照普通下单逻辑,当库存很多的时候没有问题,但是当库存只剩下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

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐