接口限流:
在面临高并发的请购请求时,我们如果不对接口进行限流,可能会对后台系统造成极大的压力。尤其是对于下单的接口,过多的请求打到数据库会对系统的稳定性造成影响。
令牌桶算法与漏桶算法:漏桶算法能够强行限制数据的传输速率,而令牌桶算法在能够限制数据的平均传输速率外,还允许某种程度的突发传输。在令牌桶算法中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的门限,因此它适合于具有突发特性的流量。
// 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);}
在接口中,可以看到有两种使用方法:
阻塞式获取令牌:请求进来后,若令牌桶里没有足够的令牌,就在这里阻塞住,等待令牌的发放。
非阻塞式获取令牌:请求进来后,若令牌桶里没有足够的令牌,会尝试等待设置好的时间(这里写了1000ms),其会自动判断在1000ms后,这个请求能不能拿到令牌,如果不能拿到,直接返回抢购失败。如果timeout设置为0,则等于阻塞时获取令牌。
抢购接口隐藏(接口加盐)的具体做法:
在该项目中增加两个接口:
/*** 获取验证值* @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);}@Overridepublic 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());// 生成hashString verify = SALT + sid + userId;String verifyHash = DigestUtils.md5DigestAsHex(verify.getBytes());// 将hash和用户商品信息存入redisString hashKey = RedisConstants.HASH_KEY + "_" + sid + "_" + userId;stringRedisTemplate.opsForValue().set(hashKey, verifyHash, 3600, TimeUnit.SECONDS);LOGGER.info("Redis写入:[{}] [{}]", hashKey, verifyHash);return verifyHash;}
postman测试:
/*** 要求验证的抢购接口* @param sid* @return*/@RequestMapping(value = "/createOrderWithVerifiedUrl", method = {RequestMethod.GET})@ResponseBodypublic 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;}
postman测试:
用redis给每个用户做访问统计,带上商品id,对单个商品做访问统计,实现一个对用户的访问频率限制,我们在用户申请下单时,检查用户的访问次数,超过访问次数,则不让他下单!
/*** 要求验证的抢购接口 + 单用户限制访问频率* @param sid* @return*/@RequestMapping(value = "/createOrderWithVerifiedUrlAndLimit", method = {RequestMethod.GET})@ResponseBodypublic 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);}@Overridepublic 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;}@Overridepublic 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;}
Jmeter做并发访问接口: