接口限流:
在面临高并发的请购请求时,我们如果不对接口进行限流,可能会对后台系统造成极大的压力。尤其是对于下单的接口,过多的请求打到数据库会对系统的稳定性造成影响。
令牌桶算法与漏桶算法:漏桶算法能够强行限制数据的传输速率,而令牌桶算法在能够限制数据的平均传输速率外,还允许某种程度的突发传输。在令牌桶算法中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的门限,因此它适合于具有突发特性的流量。
// 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);
}
@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;
}
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;
}
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;
}
Jmeter做并发访问接口: