聚焦Java性能优化 打造亿级流量秒杀系统【学习笔记】08_流量削峰技术

本章目标

  • 掌握秒杀令牌的原理和使用方式
  • 掌握秒杀大闸的原理和使用方式
  • 掌握队列泄洪的原理和使用方式

抛缺陷

  • 秒杀下单接口会被脚本不停的刷
  • 秒杀验证逻辑和秒杀下单接口强关联,代码冗余度高

秒杀下单和对活动是否开始是没有关联的,接口关联过高

  • 秒杀验证逻辑复杂,对交易系统产生无关联负载

9-2 秒杀令牌实现

  • 秒杀接口需要依靠令牌才能进入
  • 秒杀的令牌由秒杀活动模块负责生成
  • 秒杀活动模块对秒杀令牌生成全权处理,逻辑收口
  • 秒杀下单前需要先获得秒杀令牌

代码实现

PromoService接口上实现generateSecondKillToken秒杀令牌生成函数

//生成秒杀用的令牌
    String generateSecondKillToken(Integer promoId,Integer itemId,Integer userId);

PromoServiceImpl

public String generateSecondKillToken(Integer promoId,Integer itemId,Integer userId) {

        PromoDO promoDO = promoDOMapper.selectByPrimaryKey(promoId);

        //promoDo(dataObject) -> PromoModel
        PromoModel promoModel = convertFromDataObject(promoDO);
        if(promoModel == null) {
            return null;
        }
        //判断当前时间是否秒杀活动即将开始或正在进行
        DateTime now = new DateTime();
        if(promoModel.getStartDate().isAfterNow()) {
            promoModel.setStatus(1);
        }else if(promoModel.getEndDate().isBeforeNow()) {
            promoModel.setStatus(3);
        }else {
            promoModel.setStatus(2);
        }
        //判断活动是否正在进行
        if(promoModel.getStatus().intValue()!=2){
            return null;
        }

        //判断item信息是否存在
        ItemModel itemModel = itemService.getItemByIdInCache(itemId);
        if(itemModel == null) {
            return null;
        }
        //判断用户信息是否存在
        UserModel userModel = userService.getUserByIdInCache(userId);
        if(userModel == null) {
            return null;
        }
        //生成token并且存入redis设置5分组有效期
        String token = UUID.randomUUID().toString().replace("-","");
        redisTemplate.opsForValue().set("promo_token_"+promoId+"_userid_"+userId+"_itemid_"+itemId,token);
        redisTemplate.expire("promo_token_"+promoId+"_userid_"+userId+"_itemid_"+itemId,5, TimeUnit.MINUTES);

        return token;
    }

OrderController

    //生成秒杀令牌
    @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.PARAMETER_VALIDATION_ERROR,"生成令牌失败");
        }
        //返回对应的结果
        return CommonReturnType.create(promoToken);
    }

**抛缺陷:**秒杀令牌只要活动一开始就无限制生成,影响系统性能

9-4 秒杀大闸原理及实现

  • 依靠秒杀令牌的授权原理定制化发牌逻辑,做大闸功能

  • 根据秒杀商品初始化库存颁发对应数量令牌,控制大闸流量

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

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

设置一个以秒杀商品初始库存x倍数量作为秒杀大闸,若超出这个数量,则无法发放秒杀令牌

 //将大闸限制数字设置到redis内
        redisTemplate.opsForValue().set("promo_door_count_"+promoId,itemModel.getStock().intValue()*5);

抛缺陷:

  • 浪涌流量涌入后系统无法应对
  • 多库存,多商品等令牌限制能力弱

9-5 队列泄洪原理

  • 排队有些时候比并发更高效(例如redis单线程模型,innodb mutex key等)

innodb在数据库操作时要加上行锁,mutex key是竞争锁,阿里sql优化了mutex key结构,当判断存在多个线程竞争锁时,会设置队列存放SQL语句

  • 依靠排队去限制并发流量

  • 依靠排队和下游拥塞窗口程度调整队列释放流量大小

  • 支付宝银行网关队列举例

支付宝有多种支付渠道,在大促活动开始时,支付宝的网关有上亿级别的流量,银行的网关无法支持这种大流量,支付宝会将支付请求放到自己的队列中,根据银行网关可以承受的tps流量调整拥塞窗口,去泄洪

队列泄洪代码实现

OrderController


private ExecutorService executorService;

 @PostConstruct
public void init(){
    //定义一个只有20个可工作线程的线程池
   executorService = Executors.newFixedThreadPool(20);
}
 //同步调用线程池的submit方法
//拥塞窗口为20的等待队列,用来队列化泄洪
 Future<Object> future = executorService.submit(new Callable<Object>() {
   @Override
   public Object call() throws Exception {
      //加入库存流水init状态
       String stockLogId = itemService.initStockLog(itemId,amount);
       //再去完成对应的下单事务型消息机制
       if(!mqProducer.transactionAsyncReduceStock(userModel.getId(),itemId,promoId,amount,stockLogId)){
           throw new BusinessException(EmBusinessError.UNKNOWN_ERROR,"下单失败");
          }
          return null;
        }
        });

        try {
            future.get();
        } catch (InterruptedException e) {
            throw new BusinessException(EmBusinessError.UNKNOWN_ERROR);
        } catch (ExecutionException e) {
            throw new BusinessException(EmBusinessError.UNKNOWN_ERROR);
        }
        return CommonReturnType.create(null);
}

9-7 本地或分布式

  • 本地:将队列维护在本地内存中
  • 分布式:将队列设置到redis内

比如说我们有100台机器,假设每台机器设置20个队列,那我们的拥塞窗口就是2000,但是由于负载均衡的关系,很难保证每台机器都能够平均收到对应的createOrder的请求,那如果将这2000个排队请求放入redis中,每次让redis去实现以及去获取对应拥塞窗口设置的大小,这种就是分布式队列

本地队列的好处就是完全维护在内存当中的,因此其对应的没有网络请求的消耗,只要JVM不挂,应用是存活的,那本地队列的功能就不会失效。因此企业级开发应用还是推荐使用本地队列,本地队列的性能以及高可用性对应的应用性和广泛性。可以使用外部的分布式集中队列,当外部集中队列不可用时或者请求时间超时,可以采用降级的策略,切回本地的内存队列。
—————————————————————————————————
【本课程已整理完毕】

01_电商秒杀商品回顾
02_云端部署
03_分布式扩展
04_查询性能优化技术之多级缓存
05_查询性能优化技术之页面静态化
06_交易性能优化技术之缓存库存
07_交易性能优化技术之事务型消息
08_流量削峰技术
09_防刷限流技术
10_课程总结

—————————————————————————————————

你可能感兴趣的:(流量秒杀系统项目)