23 秒杀系统 | 限流技术 | 令牌桶算法

为什么要设计限流方案

  • 就是限制流量,让一部人用户能下单,一部分用户不能下单,从而避免大流量把系统冲挂了;
  • 流量远比想象的多,即使预估的再多,活动的真实流量也可能比预估的多;
  • 系统活着比挂了要好,系统活着能服务小部分用户,系统挂了一个用户都服务不了;
  • 宁愿只让少数人能用,也不要让所有人都不能用;

几种限流方案

限制并发的方案:全局计数器
  • 限定同一时间只能有 10 个线程能访问接口,最初级的方案,用全局计数器,比如需要限制的接口是下单接口,就在下单接口的 Controller 处加一个计数器,这个计数器是全局的,并且要支持并发下的加减操作,在 Controller 的入口处把计数器减 1,判断计数器的值是不是大于 0,大于 0 才往下走,在 Controller 出口处把 1 加回来;
  • 这个方案太简单粗暴,并且在接口执行时间很长的情况下,比如 10s,其衡量的指标和 TPS 衡量的指标是不对等的,一般不用这种算法;
限制 TPS 或 QPS 的方案
  • 令牌桶算法
  • 漏桶算法

令牌桶算法原理

  • 每秒桶里会新增 10 滴水;
  • 客户端请求来了,会从桶里取 1 滴水;
  • 每秒只能有 10 个客户端请求能取到水,其余的客户端请求无法往下走;
  • 对应的 TPS 就是 10;

漏桶算法原理

  • 桶一开始是满的,10 滴水;
  • 桶以每秒 10 滴水的速度流出;
  • 客户端的一次请求就是往桶里加 1 滴水;
  • 如果桶是满的,客户端请求加的 1 水就加不进去,客户端的请求就不能往下走;
  • 对应的 TPS 就是 10;

令牌桶算法 vs 漏桶算法

  • 漏桶算法是无法应对突发流量的,如果一开始有 10 个请求进来,桶流出的速度如果是 1,那么有 9 个请求就会被拒绝掉;
  • 令牌桶算法可以应对令牌桶令牌个数的突发流量,如果一开始有 10 个请求,没秒往桶里加令牌的速度是 1,那么这 10 个请求都可以被处理;

限流的粒度

接口维度
  • 系统中的单个接口的 TPS;
总体维度
  • 假设系统有 10 个接口,每个接口的 TPS 是 5,那么系统总的 TPS 是达不到 50 的,系统的总流量一般要限制在所有接口的 TPS 总和的 80%;

限流范围

集群限流
  • 依赖 Redis 或其他中间件做统一计数器,往往会产生性能瓶颈;
单机限流
  • 负载均衡的前提下,单机平均限流的效果更好;

基于 Guava 的 RateLimiter 的令牌桶算法的保护

初始化令牌桶
  • 压测得到的下单接口的 TPS 是 350,保护性的在令牌桶里放了 300 个令牌,下单接口能承受的 TPS 就是 300;

    
    
  1. private RateLimiter orderCreateRateLimiter;
  2. @PostConstruct
  3. public void init( ) {
  4. orderCreateRateLimiter = RateLimiter. create( 300);
  5. }
用令牌桶保护下单接口
  • 在下单接口的入口位置,设置令牌桶的保护代码;

    
    
  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. if (!orderCreateRateLimiter. tryAcquire()) {
  10. throw new BusinessException( EmBusinessError. RATE_LIMITER);
  11. }
  12. // 校验用户是否登录
  13. String token = httpServletRequest. getParameterMap(). get( "token")[ 0];
  14. if ( StringUtils. isEmpty(token)) {
  15. throw new BusinessException( EmBusinessError. USER_NOT_LOGIN, "用户还未登录,不能下单");
  16. }
  17. UserModel userModel = ( UserModel) redisTemplate. opsForValue(). get(token);
  18. if (userModel == null) {
  19. throw new BusinessException( EmBusinessError. USER_NOT_LOGIN, "用户还未登录,不能下单");
  20. }
  21. // 校验秒杀令牌是否正确
  22. if (promoId != null) {
  23. String inRedisPromoToken = ( String)redisTemplate. opsForValue()
  24. . get( "promo_token_" + promoId + "_userid_" + userModel. getId() + "_itemid_" + itemId);
  25. if (inRedisPromoToken == null) {
  26. throw new BusinessException( EmBusinessError. PARAMETER_VALIDATION_ERROR, "秒杀令牌校验失败");
  27. }
  28. if (! StringUtils. equals(promoToken, inRedisPromoToken)) {
  29. throw new BusinessException( EmBusinessError. PARAMETER_VALIDATION_ERROR, "秒杀令牌校验失败");
  30. }
  31. }
  32. // 拥塞窗口为 20 的等待队列,用来队列化泄洪,在一个 Tomcat 上,同一时间只能有 20 个请求能下来做下单,其他请求都要排队
  33. Future< Object> future = executorService. submit( new Callable< Object>() {
  34. @Override
  35. public Object call() throws Exception {
  36. // 在 RocketMQ 的事务型消息中完成下单操作
  37. String stockLogId = itemService. initStockLog(itemId, amount);
  38. if (!mqProducer. transactionAsyncReduceStock(userModel. getId(), promoId, itemId, amount, stockLogId)) {
  39. throw new BusinessException( EmBusinessError. UNKNOWN_ERROR, "下单失败");
  40. }
  41. return null;
  42. }
  43. });
  44. try {
  45. // 倒不是要什么返回值,就是主线程等提交到线程池中的任务执行完,好给前端响应;
  46. future. get();
  47. } catch ( InterruptedException e) {
  48. throw new BusinessException( EmBusinessError. UNKNOWN_ERROR);
  49. } catch ( ExecutionException e) {
  50. throw new BusinessException( EmBusinessError. UNKNOWN_ERROR);
  51. }
  52. return CommonReturnType. create( null);
  53. }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值