秒杀活动:

秒杀场景一般会在电商网站或(APP/小程序)举行一些活动或者节假日在12306网站上抢票时遇到。对于一些稀缺或者特价商品,一般会在约定时间点对其进行限量销售,因为这些商品的特殊性,会吸引大量用户前来抢购,并且会在约定的时间点同时在秒杀页面进行抢购。

秒杀场景特点:

秒杀时大量用户会在同一时间同时进行抢购,网站瞬时访问流量激增。
秒杀一般是访问请求数量远远大于库存数量,只有少部分用户能够秒杀成功。
秒杀业务流程比较简单,一般就是下订单减库存。

秒杀架构设计理念:

限流: 鉴于只有少部分用户能够秒杀成功,所以要限制大部分流量,只允许少部分流量进入服务后端。
削峰:对于秒杀系统瞬时会有大量用户涌入,所以在抢购一开始会有很高的瞬间峰值。高峰值流量是压垮系统很重要的原因,所以如何把瞬间的高流量变成一段时间平稳的流量也是设计秒杀系统很重要的思路。实现削峰的常用的方法有利用缓存和消息中间件等技术。
异步处理:秒杀系统是一个高并发系统,采用异步处理模式可以极大地提高系统并发量,其实异步处理就是削峰的一种实现方式。
内存缓存:秒杀系统最大的瓶颈一般都是数据库读写,由于数据库读写属于磁盘IO,性能很低,如果能够把部分数据或业务逻辑转移到内存缓存,效率会有极大地提升。
可拓展:当然如果我们想支持更多用户,更大的并发,最好就将系统设计成弹性可拓展的,如果流量来了,拓展机器就好了。像淘宝、京东等双十一活动时会增加大量机器应对交易高峰。

设计思路:

将请求拦截在系统上游,降低下游压力:秒杀系统特点是并发量极大,但实际秒杀成功的请求数量却很少,所以如果不在前端拦截很可能造成数据库读写锁冲突,甚至导致死锁,最终请求超时。
充分利用缓存:利用缓存可极大提高系统读写速度。
消息队列:消息队列可以削峰,将拦截大量并发请求,这也是一个异步处理过程,后台业务根据自己的处理能力,从消息队列中主动的拉取请求消息进行业务处理。

前端方案 :浏览器端(js)

页面静态化:将活动页面上的所有可以静态的元素全部静态化,并尽量减少动态元素。通过CDN来抗峰值。
禁止重复提交:用户提交之后按钮置灰,禁止重复提交
用户限流:在某一时间段内只允许用户提交一次请求,比如可以采取IP限流

后端方案

利用redis实现简单的秒杀系统
Redis是一个分布式缓存系统,支持多种数据结构,我们可以利用Redis轻松实现一个强大的秒杀系统。
利用redis记录读取实时库存。一旦库存不足,立即抛出异常。反馈给前端。如果库存足够,通过rpc调用通知订单微服务系统生成订单。得到订单系统的生成成功的反馈以后,在秒杀微服务系统中生成一个本地订单记录,在数据库增销量减库存。
Redis 提供了 INCR/DECR/SETNX 命令,把RMW三个操作转变为一个原子操作
Redis 是使用单线程串行处理客户端的请求来操作命令,所以当 Redis 执行某个命令操作时,其他命令是无法执行的,这相当于命令操作是互斥执行的

生成秒杀订单: 点击查看代码

//生成秒杀订单
public function createLimitOrder($storeId, $userId, $unionId, $subStoreId, $activityId, $skuId, $limitPrice, $selectedNum, $consigneeId, $reservationTime, $message)
    {
        //...
        //通过redis 检查是否还有充足库存,如果不足,则抛出异常
        $this->controlWithRedisStock($storeId, $activityInfo, $skuId, $selectedNum);
        //...
        //调用rpc 使order微服务系统生成订单,并返回orderId。
        $orderId = $this->orderRpc->createOrder($storeId, $userId, $subStoreId, $activityInfo, $skuId, $activityPrice, $selectedNum, $consigneeId, $reservationTime, $message);
        try {
            Db::beginTransaction();
            if ($orderId > 0) {
                //记录本地秒杀订单,和订单系统的orderId绑定联系起来。
                $id = $this->addLocalOrder($storeId, $userId, $orderId, $skuId, $selectedNum, $consigneeId, $activityInfo);
                $this->addSales($storeId, $activityId, $skuId, $selectedNum); //做已经出售增加 (库存减少) //下单进行此操作
            }
        } catch (\Throwable $e) {
            throw new ErrException(ExceptionParseData::parseData($e));
            Db::rollback();
        }
        Db::commit();

        return $orderId;
    }

redis控制库存:点击查看代码

private function controlWithRedisStock($storeId, $activityInfo, $skuId, $selectedNum)
    {
        //redis 计数 (原子操作)---start---------------解决高并发问题------------------ 
        if($activityInfo['inf'] == LimitCFG::STOCK_INF){  //无限库存
            return true;
        }

        $key = $this->getRedisStockKey($storeId, $activityInfo["id"], $skuId);
        $redis_stock = $this->getOrSetRedisStockValByKey($key, $activityInfo['stock']);
        if ($redis_stock <= 0) { //初步抵挡一下:这边的redis_stock 有可能不是最新的了。因为有一些请求已经同时通过了这一步,还没有来及更新它。
            throw new ErrException(ExceptionCode::E12141); //  ["12141", "库存不足了,无法下单"]
        }
        //这句是  原子性的 --- 程序到这里开始变成 串行()。
        $redis_stock = $this->decRedisStockByKey($key, $selectedNum);

        if ($redis_stock < 0) {
           //如果减多了再加回来
            $this->addRedisStockByKey($key, $selectedNum);
            throw new ErrException(ExceptionCode::E12141); //  ["12141", "库存不足了,无法下单"]
        }
        return true;
        //redis 计数  (原子操作)---end---------------
    }

 public function getOrSetRedisStockValByKey($key, $dbStock)
    {

        $this->commonRedis->setNx($key, $dbStock);

        $redis_stock = $this->commonRedis->get($key);
        // var_dump($redis_stock);
        return $redis_stock;
    }

      public function decRedisStockByKey($key, $selectedNum)
    {
       return  $this->commonRedis->decrBy($key, $selectedNum);  //原子操作,返回的是 减过之后的值。有用,后续还要判断
        
    }