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

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

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