• 仿大众点评——秒杀系统部分02


    秒杀系统优化

    接口限流和安全措施

    • 令牌桶限流
    • 单用户访问频率限流
    • 抢购接口隐藏

    接口限流:
    在面临高并发的请购请求时,我们如果不对接口进行限流,可能会对后台系统造成极大的压力。尤其是对于下单的接口,过多的请求打到数据库会对系统的稳定性造成影响。

    令牌桶限流

    令牌桶算法与漏桶算法:漏桶算法能够强行限制数据的传输速率,而令牌桶算法在能够限制数据的平均传输速率外,还允许某种程度的突发传输。在令牌桶算法中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的门限,因此它适合于具有突发特性的流量。

    • 使用Guava的RateLimiter实现令牌桶限流接口:Guava是Google开源的Java工具类,里面包罗万象,也提供了限流工具类RateLimiter,该类里面实现了令牌桶算法。
    // Guava令牌桶:每秒放行10个请求
        RateLimiter rateLimiter = RateLimiter.create(10);
        @PostMapping("seckill/{id}")
        public Result seckillVoucher(@PathVariable("id") Long voucherId) {
            // 阻塞式获取令牌
            //LOGGER.info("等待时间" + rateLimiter.acquire());
            // 非阻塞式获取令牌
            if (!rateLimiter.tryAcquire(1000, TimeUnit.MILLISECONDS)) {
                LOGGER.warn("你被限流了,真不幸,直接返回失败");
                return Result.fail("购买失败,库存不足");
            }
            return voucherOrderService.seckillVoucher(voucherId);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在接口中,可以看到有两种使用方法:

    阻塞式获取令牌:请求进来后,若令牌桶里没有足够的令牌,就在这里阻塞住,等待令牌的发放。
    非阻塞式获取令牌:请求进来后,若令牌桶里没有足够的令牌,会尝试等待设置好的时间(这里写了1000ms),其会自动判断在1000ms后,这个请求能不能拿到令牌,如果不能拿到,直接返回抢购失败。如果timeout设置为0,则等于阻塞时获取令牌。

    • Jmeter压测:
      阻塞式获取令牌结果:
      在这里插入图片描述
      在这里插入图片描述
      非阻塞式获取令牌结果:
      在这里插入图片描述
      在这里插入图片描述
    抢购接口隐藏

    抢购接口隐藏(接口加盐)的具体做法:

    • 每次点击秒杀按钮,先从服务器获取一个秒杀验证值(接口内判断是否到秒杀时间)。
    • Redis以缓存用户ID和商品ID为Key,秒杀地址为Value缓存验证值
    • 用户请求秒杀商品的时候,要带上秒杀验证值进行校验。

    在该项目中增加两个接口:

    • 获取验证值接口:该接口要求传用户id和商品id,返回验证值,并且该验证值
    /**
         * 获取验证值
         * @return
         */
        @GetMapping("getVerifyHash")
        public String getVerifyHash(@RequestParam(value = "sid") Integer sid,
                                    @RequestParam(value = "userId") Integer userId) {
            String hash;
            try {
                hash = voucherOrderService.getVerifyHash(sid, userId);
            } catch (Exception e) {
                LOGGER.error("获取验证hash失败,原因:[{}]", e.getMessage());
                return "获取验证hash失败";
            }
            return String.format("请求抢购验证hash值为:%s", hash);
        }
    
    @Override
        public String getVerifyHash(Integer sid, Integer userId) throws Exception {
    
            // 验证是否在抢购时间内
            LOGGER.info("验证是否在抢购时间内");
    
            // 检查用户合法性
            User user = userMapper.selectById(userId.longValue());
            if (user == null) {
                throw new Exception("用户不存在");
            }
            LOGGER.info("用户信息:[{}]", user.toString());
    
            // 检查商品合法性
            Voucher stock = voucherMapper.selectById(sid);
            if (stock == null) {
                throw new Exception("商品不存在");
            }
            LOGGER.info("商品信息:[{}]", stock.toString());
    
            // 生成hash
            String verify = SALT + sid + userId;
            String verifyHash = DigestUtils.md5DigestAsHex(verify.getBytes());
    
            // 将hash和用户商品信息存入redis
            String hashKey = RedisConstants.HASH_KEY + "_" + sid + "_" + userId;
            stringRedisTemplate.opsForValue().set(hashKey, verifyHash, 3600, TimeUnit.SECONDS);
            LOGGER.info("Redis写入:[{}] [{}]", hashKey, verifyHash);
            return verifyHash;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    postman测试:
    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述

    • 携带验证值下单接口:用户在前台拿到了验证值后,点击下单按钮,前端携带着特征值,即可进行下单操作。
    /**
         * 要求验证的抢购接口
         * @param sid
         * @return
         */
        @RequestMapping(value = "/createOrderWithVerifiedUrl", method = {RequestMethod.GET})
        @ResponseBody
        public String createOrderWithVerifiedUrl(@RequestParam(value = "sid") Integer sid,
                                                 @RequestParam(value = "userId") Integer userId,
                                                 @RequestParam(value = "verifyHash") String verifyHash) {
            int stockLeft;
            try {
                stockLeft = voucherOrderService.createVerifiedOrder(sid, userId, verifyHash);
                LOGGER.info("购买成功,剩余库存为: [{}]", stockLeft);
            } catch (Exception e) {
                LOGGER.error("购买失败:[{}]", e.getMessage());
                return e.getMessage();
            }
            return String.format("购买成功,剩余库存为:%d", stockLeft);
        }
    
    public int createVerifiedOrder(Integer sid, Integer userId, String verifyHash) throws Exception {
    
            // 验证是否在抢购时间内
            LOGGER.info("请自行验证是否在抢购时间内,假设此处验证成功");
    
            // 验证hash值合法性
            String hashKey = RedisConstants.HASH_KEY + "_" + sid + "_" + userId;
            String verifyHashInRedis = stringRedisTemplate.opsForValue().get(hashKey);
            if (!verifyHash.equals(verifyHashInRedis)) {
                throw new Exception("hash值与Redis中不符合");
            }
            LOGGER.info("验证hash值合法性成功");
    
            // 检查用户合法性
            User user = userMapper.selectById(userId.longValue());
            if (user == null) {
                throw new Exception("用户不存在");
            }
            LOGGER.info("用户信息验证成功:[{}]", user.toString());
    
            // 检查商品合法性
            Voucher stock = voucherMapper.selectById(sid);
            if (stock == null) {
                throw new Exception("商品不存在");
            }
            LOGGER.info("商品信息验证成功:[{}]", stock.toString());
    
            //乐观锁更新库存
            LOGGER.info("乐观锁更新库存成功");
    
            //创建订单
            LOGGER.info("创建订单成功");
    
            return 1;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    postman测试:

    • 未携带验证值或验证值错误,结果如下:
      在这里插入图片描述
      在这里插入图片描述
    • 携带验证值,结果如下:
      在这里插入图片描述
      在这里插入图片描述
    单用户限制频率

    用redis给每个用户做访问统计,带上商品id,对单个商品做访问统计,实现一个对用户的访问频率限制,我们在用户申请下单时,检查用户的访问次数,超过访问次数,则不让他下单!

    /**
         * 要求验证的抢购接口 + 单用户限制访问频率
         * @param sid
         * @return
         */
        @RequestMapping(value = "/createOrderWithVerifiedUrlAndLimit", method = {RequestMethod.GET})
        @ResponseBody
        public String createOrderWithVerifiedUrlAndLimit(@RequestParam(value = "sid") Integer sid,
                                                         @RequestParam(value = "userId") Integer userId,
                                                         @RequestParam(value = "verifyHash") String verifyHash) {
            // 阻塞式获取令牌
            //LOGGER.info("等待时间" + rateLimiter.acquire());
            // 非阻塞式获取令牌
            /*if (!rateLimiter.tryAcquire(1000, TimeUnit.MILLISECONDS)) {
                LOGGER.warn("你被限流了,真不幸,直接返回失败");
                return "购买失败,库存不足";
            }*/
            int stockLeft;
            try {
                int count = userService.addUserCount(userId);
                LOGGER.info("用户截至该次的访问次数为: [{}]", count);
                boolean isBanned = userService.getUserIsBanned(userId);
                if (isBanned) {
                    return "购买失败,超过频率限制";
                }
                stockLeft = voucherOrderService.createVerifiedOrder(sid, userId, verifyHash);
                LOGGER.info("购买成功,剩余库存为: [{}]", stockLeft);
            } catch (Exception e) {
                LOGGER.error("购买失败:[{}]", e.getMessage());
                return e.getMessage();
            }
            return String.format("购买成功,剩余库存为:%d", stockLeft);
        }
    
    
    
     @Override
        public int addUserCount(Integer userId) throws Exception {
            String limitKey = RedisConstants.LIMIT_KEY + "_" + userId;
            String limitNum = stringRedisTemplate.opsForValue().get(limitKey);
            int limit = -1;
            if (limitNum == null) {
                stringRedisTemplate.opsForValue().set(limitKey, "0", 3600, TimeUnit.SECONDS);
            } else {
                limit = Integer.parseInt(limitNum) + 1;
                stringRedisTemplate.opsForValue().set(limitKey, String.valueOf(limit), 3600, TimeUnit.SECONDS);
            }
            return limit;
        }
    
        @Override
        public boolean getUserIsBanned(Integer userId) {
            String limitKey = RedisConstants.LIMIT_KEY + "_" + userId;
            String limitNum = stringRedisTemplate.opsForValue().get(limitKey);
            if (limitNum == null) {
                LOGGER.error("该用户没有访问申请验证值记录,疑似异常");
                return true;
            }
            return Integer.parseInt(limitNum) > 10;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    Jmeter做并发访问接口:
    在这里插入图片描述
    在这里插入图片描述

    在这里插入图片描述

  • 相关阅读:
    算法通关村17关 | 透析跳跃游戏
    第一章NoSQL数据库简介
    如何用MATLAB对CSI数据进行预处理(卡尔曼滤波篇)
    【k8s总结】
    简单几个方法教你怎么把PDF压缩小,试试你就知道
    阿里云的CIPU
    minikube 快速使用入门 - pod - 外传
    【vue】main.js中全局挂载方法、组件:
    大模型高效微调-LoRA原理详解和训练过程深入分析
    模板测试和深度测试在cocoscreator中的应用
  • 原文地址:https://blog.csdn.net/mao____mao/article/details/127885460