19 秒杀系统 | 流量削峰技术 | 秒杀令牌

流量削峰三大技术

  • 秒杀令牌
  • 秒杀大闸
  • 队列泄洪

引入削峰技术之前方案的缺点

  • 秒杀下单接口会被脚本不停的刷新,所谓秒杀接口其实就是一个暴露在公网的 URL /order/create,如果用户知道自己的 token,要秒杀的商品的 id,很容易就能写个脚本不停的刷,这样会影响正常用户的下;即便在秒杀活动还没开始的时候,也存在被黄牛用户不停的刷的可能(有了秒杀令牌机制,在活动开始前,秒杀令牌是发不出去的,没有活动令牌,/order/create 就不会成功);
  • 秒杀验证逻辑和秒杀下单接口强关联,代码冗余度高,下单的逻辑和活动是否开始的逻辑是没有关联的,哪怕活动没有开始,仍然可以以普通商品的方式下单,即便活动开始了,校验活动是否开始的逻辑也不应该在下单接口的逻辑中,
  • 秒杀验证逻辑复杂,对交易系统产生无关联负载,交易接口要解决的是,生成对应的交易号,落单,并且扣减对应的库存;校验用户的合法状态,活动的状态,其实都不是下单接口要做的事情;

秒杀令牌原理

  • 秒杀接口需要依靠令牌才能进入,秒杀接口需要新增一个入参,表示前端用户获得的秒杀令牌,令牌合法之后,才能进入秒杀下单的逻辑;
  • 秒杀的令牌,由秒杀活动模块(PromoService)负责生成,和交易系统无关,交易系统只是验证令牌的可靠性,以此判断 HTTP 请求能否进入秒杀接口;
  • 秒杀活动模块(PromoService)对秒杀令牌的生成全权处理,逻辑收口,即秒杀活动模块全权负责秒杀令牌的生成周期以及生成方式;
  • 秒杀下单前,需要先获得秒杀令牌才能进行秒杀下单;

秒杀令牌实现

生成秒杀令牌
  • 校验活动的合法性,活动是否开始;
  • 校验用户信息和商品信息,并且在下单接口中,把这部分逻辑删除;
  • 生成秒杀令牌,并存入 Redis 中,针对一个活动、一个商品,每个用户只能获得一个令牌;如果用户多次下单,每次下单,该用户对应的秒杀令牌都会更新;

    
    
  1. @Override
  2. public String generateSecondKillToken(Integer promoId, Integer itemId, Integer userId) {
  3. // 0. 获取活动信息
  4. PromoDO promoDO = promoDOMapper.selectByPrimaryKey(promoId);
  5. PromoModel promoModel = convertFromPromoDO(promoDO);
  6. if (promoModel == null) {
  7. return null;
  8. }
  9. if (promoModel.getStartTime().isAfterNow()) {
  10. promoModel.setStatus( 1);
  11. } else if (promoModel.getEndTime().isBeforeNow()) {
  12. promoModel.setStatus( 3);
  13. } else {
  14. promoModel.setStatus( 2);
  15. }
  16. // 1. 判断活动是否正在进行
  17. if (promoModel.getStatus().intValue() != 2) {
  18. return null;
  19. }
  20. // 2. 校验商品信息和用户信息
  21. ItemModel itemModel = itemService.getItemByIdInCache(itemId);
  22. if (itemModel == null) {
  23. return null;
  24. }
  25. UserModel userModel = userService.getUserByIdInCache(userId);
  26. if (userModel == null) {
  27. return null;
  28. }
  29. // 3. 生成秒杀令牌,并存入 Redis 中
  30. String token = UUID.randomUUID().toString().replace( "-", "");
  31. redisTemplate.opsForValue(). set( "promo_token_" + promoId + "_userid_" + userId + "_itemid_" + itemId, token);
  32. redisTemplate.expire( "promo_token_" + promoId + "_userid_" + userId + "_itemid_" + itemId, 5, TimeUnit.MINUTES);
  33. return token;
  34. }
增加生成秒杀令牌的接口
  • 每次调用下单接口前都要调用这个接口;

    
    
  1. @RequestMapping(value = "/generatetoken", method = {RequestMethod.POST}, consumes = {CONTENT_TYPE_FORMED})
  2. @ResponseBody
  3. public CommonReturnType generateToken (@RequestParam(name = "itemId") Integer itemId,
  4. @RequestParam(name = "promoId") Integer promoId) throws BusinessException {
  5. String token = httpServletRequest.getParameterMap().get( "token")[ 0];
  6. if (StringUtils.isEmpty(token)) {
  7. throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "用户还未登录,不能下单");
  8. }
  9. UserModel userModel = (UserModel) redisTemplate.opsForValue().get(token);
  10. if (userModel == null) {
  11. throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "用户还未登录,不能下单");
  12. }
  13. // 生成秒杀令牌
  14. String promoToken = promoService.generateSecondKillToken(promoId, itemId, userModel.getId());
  15. if (promoToken == null) {
  16. throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "生成秒杀令牌失败");
  17. }
  18. return CommonReturnType.create(promoToken);
  19. }
下单接口增加对秒杀令牌的校验逻辑

    
    
  1. @RequestMapping(value = "/createorder", method = { RequestMethod. POST}, consumes = { CONTENT_TYPE_FORMED})
  2. @ResponseBody
  3. public CommonReturnType createOrder( @RequestParam(name = "itemId") Integer itemId,
  4. @RequestParam(name = "amount") Integer amount,
  5. @RequestParam(name = "promoId", required = false) Integer promoId,
  6. @RequestParam(name = "promoToken", required = false) String promoToken)
  7. throws BusinessException {
  8. // 校验用户是否登录
  9. String token = httpServletRequest. getParameterMap(). get( "token")[ 0];
  10. if ( StringUtils. isEmpty(token)) {
  11. throw new BusinessException( EmBusinessError. USER_NOT_LOGIN, "用户还未登录,不能下单");
  12. }
  13. UserModel userModel = ( UserModel) redisTemplate. opsForValue(). get(token);
  14. if (userModel == null) {
  15. throw new BusinessException( EmBusinessError. USER_NOT_LOGIN, "用户还未登录,不能下单");
  16. }
  17. // 校验秒杀令牌是否正确
  18. if (promoId != null) {
  19. String inRedisPromoToken = ( String)redisTemplate. opsForValue()
  20. . get( "promo_token_" + promoId + "_userid_" + userModel. getId() + "_itemid_" + itemId);
  21. if (inRedisPromoToken == null) {
  22. throw new BusinessException( EmBusinessError. PARAMETER_VALIDATION_ERROR, "秒杀令牌校验失败");
  23. }
  24. if (! StringUtils. equals(promoToken, inRedisPromoToken)) {
  25. throw new BusinessException( EmBusinessError. PARAMETER_VALIDATION_ERROR, "秒杀令牌校验失败");
  26. }
  27. }
  28. // 判断库存是否已经售罄,若对应的售罄 key 存在,则直接返回下单失败
  29. if (redisTemplate. hasKey( "promo_item_stock_invalid_" + itemId)) {
  30. throw new BusinessException( EmBusinessError. STOCK_NOT_ENOUGH);
  31. }
  32. // 在 RocketMQ 的事务型消息中完成下单操作
  33. String stockLogId = itemService. initStockLog(itemId, amount);
  34. if(!mqProducer. transactionAsyncReduceStock(userModel. getId(), promoId, itemId, amount, stockLogId)) {
  35. throw new BusinessException( EmBusinessError. UNKNOWN_ERROR, "下单失败");
  36. }
  37. return CommonReturnType. create( null);
  38. }

秒杀令牌的缺陷

  • 在活动刚开始的时候,比如有 1亿个用户下单,就会生成 1 亿个秒杀令牌;
  • 秒杀令牌的生成是耗性能的;
  • 即便 1 亿个用户都得到了秒杀令牌,也不是 1 亿个用户都能得到抢占库存的先机;
  • 可以使用秒杀大闸技术优化系统性能;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值