第八章 流量削峰技术

第八章 流量削峰技术

源码地址:https://gitee.com/xu3619/miaosha

本章目标

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

抛缺陷

  • 秒杀下单接口会被脚本不停的刷
  • 秒杀验证逻辑和秒杀下单接口强关联,代码冗余度高
    • 秒杀下单和对活动是否开始是没有关联的,接口关联过高
  • 秒杀验证逻辑复杂,对交易系统产生无关联负载

秒杀令牌原理及实现

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

代码实现

  • PromoService 接口上实现 generateSecondKillToken 秒杀令牌生成函数
// 生成秒杀用的令牌
String generateSecondKillToken(Integer promoId);
  • PromoServiceImpl
		@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;
        }

        // 判断当前时间是否秒杀活动即将开始或正在进行
        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;
        }

        // 判断商品信息是否存在
        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);
        // 设置令牌过期时间为5分钟
        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, //商品id
                                          @RequestParam(name = "promoId") Integer promoId) //活动id
            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);
    }

抛缺陷

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

秒杀大闸原理及实现

  • 依靠秒杀令牌的授权原理定制化发牌逻辑,做到大闸功能
  • 根据秒杀商品初始库存颁发对应数量令牌,控制大闸流量
  • 用户风控策略前置到秒杀令牌发放中
  • 库存售罄判断前置到秒杀令牌发放中

代码实现

  • 设置一个以秒杀商品初始库存x倍数量作为秒杀大闸,若超出这个数量,则无法发放秒杀令牌
  • PromoServiceImpl
		@Override
    public void publishPromo(Integer promoId) {
        // 通过活动id获取活动
        PromoDO promoDO = promoDOMapper.selectByPrimaryKey(promoId);
        // 如果没有活动直接返回即可
        if (promoDO.getItemId() == null || promoDO.getItemId().intValue() == 0) {
            return;
        }
        ItemModel itemModel = itemService.getItemById(promoDO.getItemId());

        // 将活动商品库存同步到redis中
        redisTemplate.opsForValue().set("promo_item_stock_" + itemModel.getId(), itemModel.getStock());
        // 将大闸的限制数字设置到redis中,比如有100个库存,我们可以发放500个令牌
        redisTemplate.opsForValue().set("promo_door_count_" + promoId, itemModel.getStock().intValue() * 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);
    }		

		//封装下单请求
    @RequestMapping(value = "/createorder", method = {RequestMethod.POST}, consumes = {CONTENT_TYPE_FORMED})
    @ResponseBody
    public CommonReturnType createOrder(@RequestParam(name = "itemId") Integer itemId, //商品id
                                        //秒杀活动id,required = false表示如果秒杀活动还没开始,该参数就会自动隐藏
                                        @RequestParam(name = "promoId", required = false) Integer promoId,
                                        //商品购买数量
                                        @RequestParam(name = "amount") Integer amount,
                                        //秒杀令牌
                                        @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 inRedisPromoToken = (String) redisTemplate.opsForValue().get("promo_token_" + promoId + "_userid_" + userModel.getId() + "_itemid_" + itemId);
            if (inRedisPromoToken == null) {
                throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "秒杀令牌校验失败");
            }
            // 将前端传过来的秒杀token和redis内保存的秒杀token作比较
            if (!StringUtils.equals(promoToken, inRedisPromoToken)) {
                // 令牌校验失败
                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);

                // 再去完成对应的下单事务型消息机制
                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);
    }

本地或分布式

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

分布式队列

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

本地队列的好处

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猿小羽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值