10.秒杀削峰泄洪策略

关于削峰技术,常用场景例如秒杀。为什么要流量削峰?之所以叫秒杀,也就是第一秒的时候流量涌入的问题,瞬时流量变大可能对机器造成影响,因此我们需要把第一秒的流量平滑的过度掉,削弱峰值,把流量平滑的过渡到第二秒或者后面,让系统性能有平滑的提升。

对于我们现在没有做任何操作的时候,秒杀下单的接口会被脚本不停地刷。秒杀验证逻辑和秒杀下单接口强关联,代码冗余度高。秒杀验证逻辑复杂,对交易系统产生无关联负载。

因此我们引入了秒杀令牌原理:

秒杀接口需要依靠令牌才能进入。秒杀令牌由秒杀活动模块负责生成。

秒杀活动模块对秒杀令牌生成全权处理,逻辑收口。

秒杀下单前用户需要先获得令牌才能秒杀。

接下来实战一下:

把用户校验的全部写到一个接口中:

//生成秒杀令牌
    @RequestMapping(value = "/generatetoken",method = {RequestMethod.POST},consumes={CONTENT_TYPE_FORMED})
    @ResponseBody
    public CommonReturnType generatetoken(@RequestParam(name="itemId")Integer itemId,
                                        @RequestParam(name="promoId")Integer promoId) throws BusinessException {
        //根据token获取用户信息
        String token = httpServletRequest.getParameterMap().get("token")[0];
        if (StringUtils.isEmpty(token)){
            throw new BusinessException(EmBusinessError.USER_NOT_LOGIN,"用户还未登陆,不能下单");
        }
        UserModel userModel = (UserModel) redisTemplate.opsForValue().get(token);
        if(userModel == null){
            throw new BusinessException(EmBusinessError.USER_NOT_LOGIN,"用户还未登陆,不能下单");
        }
        //获取秒杀访问令牌
        String promoToken = promoService.generateSecondKillToken(promoId , itemId , userModel.getId());

        if (promoToken == null){
            throw new BusinessException(EmBusinessError.UNKNOWN_ERROR , "生成令牌失败");
        }
        return CommonReturnType.create(promoToken);
    }

进一步进行判断,通过发token 

    @Override
    public String generateSecondKillToken(Integer promoId , Integer itemId , Integer userId) {
        //获取对应商品的秒杀活动信息
        PromoDO promoDO = promoDOMapper.selectByPrimaryKey(promoId);
        //dataobject->model
        PromoModel promoModel = convertFromDataObject(promoDO);
        if(promoModel == null){
            return null;
        }
        //判断当前时间是否秒杀活动即将开始或正在进行
        if(promoModel.getStartDate().isAfterNow() || promoModel.getEndDate().isBeforeNow()){
            return null;
        }
        //判断item信息
        ItemModel itemModel =itemService.getItemByIdInCache(itemId);
        if(itemModel == null){
            return null;
        }
        //判断用户信息
        UserModel userModel = userService.getUserByIdInCache(userId);
        if(userModel == null){
            return null;
        }
        String token = UUID.randomUUID().toString().replace("-" , "");
        redisTemplate.opsForValue().set("promo_token_" + promoId + "_user_" + userId + "_item_" + itemId , token);
        redisTemplate.expire("promo_token_" + promoId + "_user_" + userId + "_item_" + itemId , 5 , TimeUnit.MINUTES);
        return token;
    }

如此,前端在下单请求中带这个token,后端下单接口中校验这个token是否合法即可,这样就做到了校验和下单分离。但是现在还有个问题是只要用户请求,都会颁发token,所以我们接下来要做限制,也就是秒杀大闸:

秒杀大闸的原理:

依靠秒杀令牌的授权原理定制化发牌逻辑(控制令牌发放),那么我们就能做到类似大闸的功能。

我们可以根据秒杀商品初始库存颁发对应数量的令牌,控制大闸流量。

用户风控策略前置到秒杀令牌发放中。

库存售罄判断前置到秒杀令牌发放中。

这里就不编码了,实现起来很简单,就是在发布的时候,把允许生成令牌的数量放到redis中,每次生成令牌的时候就-1。

当然,这样还是有缺陷的,假如秒杀商品有10w个,那么对于系统浪涌流量涌入还是无法应对的,且这样的做法只是对于单库存而言,对于多库存多商品的令牌限制能力弱。

接下来解决一下这些问题,也就是队列泄洪功能。

队列泄洪原理,就是排队的策略,因为排队有时比并发更高效。依靠排队来限制并发流量,依靠排队和下游用塞窗口程度调整队列释放流量大小。

    private ExecutorService executorService;

    @PostConstruct
    public void init(){
        executorService = Executors.newFixedThreadPool(20);
    }

    //封装下单请求
    @RequestMapping(value = "/createorder",method = {RequestMethod.POST},consumes={CONTENT_TYPE_FORMED})
    @ResponseBody
    public CommonReturnType createOrder(@RequestParam(name="itemId")Integer itemId,
                                        @RequestParam(name="amount")Integer amount,
                                        @RequestParam(name="promoId",required = false)Integer promoId,
                                        @RequestParam(name="promoToken",required = false)String promoToken
                                        ) throws BusinessException {
        String token = httpServletRequest.getParameterMap().get("token")[0];
        if (StringUtils.isEmpty(token)){
            throw new BusinessException(EmBusinessError.USER_NOT_LOGIN,"用户还未登陆,不能下单");
        }
        UserModel userModel = (UserModel) redisTemplate.opsForValue().get(token);
        if(userModel == null){
            throw new BusinessException(EmBusinessError.USER_NOT_LOGIN,"用户还未登陆,不能下单");
        }
        //校验秒杀是否正确
        if (promoId != null){
            String redisPromoToken = (String) redisTemplate.opsForValue().get("promo_token_" + promoId + "_user_" + userModel.getId() + "_item_" + itemId);
            if (redisPromoToken == null || !StringUtils.equals(redisPromoToken , promoToken)){
                throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR , "秒杀令牌校验失败");
            }

        }
        //同步调用线程池的submit方法
        //用塞窗口为20的等待队列,用来队列化泄洪
        Future<Object> future = executorService.submit(new Callable<Object>() {
            @Override
            public Object call() throws Exception {
                //加入库存流水init状态
                String stockLogId = itemService.initStockLog(itemId , amount);
                //再去完成对应的下单事务型消息机制
                boolean result = mqProducer.transactionAsyncReduceStock(userModel.getId() ,  promoId ,  itemId ,  amount , stockLogId);
                if (!result){
                    throw new BusinessException(EmBusinessError.UNKNOWN_ERROR , "下单失败");
                }
                return null;
            }
        });
        try {
            future.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
            throw new BusinessException(EmBusinessError.UNKNOWN_ERROR , "下单失败");
        } catch (ExecutionException e) {
            e.printStackTrace();
            throw new BusinessException(EmBusinessError.UNKNOWN_ERROR , "下单失败");
        }
        return CommonReturnType.create(null);
    }

其实实现方式很简单,创建一个线程池,把下单放到线程中,block自己等待返回,然后再返回给前端。

以上是本地泄洪的策略,当然我们还有分布式的策略,将队列设置到外部redis中,由分布式队列来管理。这个后面再说。

发布了97 篇原创文章 · 获赞 28 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/haozi_rou/article/details/105450457
10.