接口限流

我正在参加「启航计划」

中间件层面的限流处理

Tomcat:能够设置最大衔接数,针对于单体的项目有用

Nginx:漏桶算法

Gateway:令牌桶算法

Nginx 限流

  1. 运用漏桶算法对恳求进行限流
http {
  limit_req_zone $binary_remote_addr zone=servicelRateLimit:10m rate=10r/s
  server {
    listen 80;
    server_name localhost;
    location / {
      limit_req_zone servicelRateLimit burst=20 nodelay;
			proxy_pass http://targetserver;
    }
  }
}

语法:limit_req_zone key zone rate

  • key:界说限流对像,binary_remote_addr 便是一种key,根据客户端 ip 限流
  • Zone:界说共享存储区来存储拜访信息,10m 能够存储 16wip 地址拜访信息
  • Rate:最大拜访速率,rate=10r/s 表明每秒最多恳求 10 个恳求
  • burst=20:相当于桶的巨细
  • Nodelay:快速处理
  1. 操控并发的衔接数
http {
  limit_conn_zone $binary_remote_addr zone=perip:10m;
  limit_conn_zone $server_name zone=perserver:10m;
  server {
    listen 80;
    server name localhost;
  	location / {
      limit conn perip 20;
      limit_conn perserver 100;
      proxy_pass http://targetserver;
  	}
  }
}
  • limit conn perip 20:对应的 key 是 $binary_remote_addr,表明约束单个lP同时最多能持有20个衔接
  • limit_conn perserver 100:对应的 key 是 $server_name表明虚拟主机 (server) 同时能处理并发衔接的总数

Gateway 限流

yml 配置文件中,微服务路由设置增加局部过滤器 RequestRateLimiter,根据的是令牌桶算法,默许运用 redis 存储令牌,需要配置 redis 的衔接

- id:gateway-consumer
  uri:1b://GATEWAY-CONSUMER
  predicates:
  - Path=/order/**
  filters:
  - name:RequestRateLimiter
    args:
    	#运用SpEL从容器中获取目标
      key-resolver:'#@pathKeyResolver}'
      #令牌桶每秒填充均匀速率
      redis-rate-limiter.replenishRate:1
      #令牌桶的上限
      redis-rate-limiter.burstCapacity:3
  • key-resolver:界说限流对像(ip路径参数),需代码实现,运用 spel 表达式获取
  • redis-rate-limiter.replenishRate:令牌桶每秒填充均匀速率
  • redis-rate-limiter.burstCapacity:令牌桶总容量。

Sentinel

Sentinel供给了丰厚的功用特性,如流量操控异常熔断集群限流速率操控

常见的限流手段和算法

虽然Sentinel供给了丰厚的功用特性,但我们当下需要重点关注的是流量操控部分。所谓流量操控,其原理是监控运用流量的 QPS 或并发线程数等目标,当达到指定的阈值时对流量进行操控,以防止被瞬时的流量高峰冲垮,然后保障运用的高可用性

@GetMapping("/{activityId}/list/{itemId}")
@SentinelResource((value = "GetSeckillGood")
public BaseResponse<SeckillGoodResponse> getSeckillGood(@RequestHeader(value = "TokenInfo") Long userId,
														@PathVariable Long activityId,
														@PathVariable Long itemId,
														@RequestParam(required = false) Long version) {
	return seckillGoodService.getSeckillGood(userId, activityId, itemId, version);
}

恳求接口增加 @SentinelResourse 接口

限流形式

单机限流

运用 Guava 中的单机限流东西即可

import com.google.common.util.concurrent.RateLimiter;
public class RateLimiterExample {
    public static void main(String[] args) {
        // 创立一个每秒答应2个恳求的限流器
        RateLimiter rateLimiter = RateLimiter.create(2);
        // 模仿10个恳求
        for (int i = 1; i <= 10; i++) {
            // 尝试获取令牌
            if (rateLimiter.tryAcquire()) {
                System.out.println("Request " + i + " is processed.");
            } else {
                System.out.println("Request " + i + " is rejected.");
            }
        }
    }
}

分布式限流

运用 Redis 记录用户的拜访频率或许运用 Gateway 来进行一致的限流处理,这儿展示运用 Redisson 自带的限流东西进行限流处理

/**
 * @author Ezreal
 * @Date 2023/6/22
 */
@Component
public class CurrentLimitManager {
    @Resource
    private RedissonClient redissonClient;
    public void doRateLimit(String key) {
        RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
        // 每秒钟最多拜访两次
        rateLimiter.trySetRate(RateType.OVERALL, 2, 1, RateIntervalUnit.SECONDS);
        boolean acquire = rateLimiter.tryAcquire(1);
        if (!acquire) {
            throw new ToManyRequestException("to many request");
        }
    }
}

常见的限流算法

漏桶算法

规划一个漏桶,假如漏桶满了就能够拒绝服务,假如没有满,则能够经过固定的速率来处理漏桶中的恳求

假如漏桶中没有水:

  • 假如进水速率小于等于最大出水速率,漏桶内不会有积水
  • 假如进水速率大于最大出水速率,漏桶内会产生积水

假如漏桶中存在水

  • 假如进水速率小于等于最大出水速率,那么漏桶内的水会被排干
  • 假如进水速率大于最大出水速率,那么漏桶中的水就会满,多于的水会溢出
/**
 * @author Ezreal
 * @Date 2023/6/22
 */
public class LeakyBucketWater {
    long lastModifyTime = 0L;
    long currentWater = 0L;
    long capacity;
    long rate = 2L;
    public LeakyBucketWater(long capacity) {
        this.capacity = capacity;
    }
    public Boolean doProcess() {
        long currentTimeMillis = System.currentTimeMillis();
        // 每分钟出水的个数,如何体现固定限流(currentTimeMillis - lastModifyTime) / 1000  取余的操作
        long outWater = (currentTimeMillis - lastModifyTime) / 1000 * rate;
        // 当前水的容量巨细
        currentWater = Math.max(0, currentWater - outWater);
        if (currentWater < capacity) {
            lastModifyTime = currentTimeMillis;
            currentWater++;
            return true;
        } else {
            return false;
        }
    }
}

令牌桶算法

规划一个桶,以固定的速率向里面放入令牌,每次恳求到来时,都会先领取令牌,再去执行相关的业务

与漏桶算法相比,令牌桶算法能够支持很多突发的恳求,而漏桶算法处理的恳求相对平滑

/**
 * @author Ezreal
 * @Date 2023/6/22
 */
public class TokenBucket {
    long lastModifyTime = 0L;
    long bucketCounts = 10L;
    long capacity = 50L;
    long currentBucket = 0;
    public Boolean doProcess() {
        long currentTimeMillis = System.currentTimeMillis();
        long generateBucket = (currentTimeMillis - lastModifyTime) / 1000 * bucketCounts;
        currentBucket = Math.min(capacity, generateBucket + currentBucket);
        lastModifyTime = currentTimeMillis;
        if (currentBucket > 0) {
            currentBucket--;
            return true;
        } else {
            return false;
        }
    }
}

根据 Redis 的滑动窗口限流算法

思路:

  1. 界说一个时间段的长度(即窗口长度 len)
  2. 统计[now - len, now] 之间恳求的个数
  3. 若超过最大值,则直接返回错误信息即可;

运用 redis 中的 zset 来实现

  1. 运用用户的唯一标识(id、ip 等等)作为 key,当前时间 的作为 value,当前时间作为分数 score
  2. 当用户恳求到来时,将当前的 key - value - score 参加到 zset 中(key 要设置过期时间)
  3. 核算 start 和 end 的值
  • end:now time
  • start:end – len
  1. 移除[0, start] 之间的符号
  2. 统计 [start, end] 之间 key 的数量,判别是否超过最大值即可
@Component
public class SlidingWindowLimitServiceImpl implements SlidingWindowLimitService {
    private final Long maxCount = 100L;
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    @Override
    public boolean pass(String userKey, int period, int size) {
        int len = period * size;
        long now = System.currentTimeMillis();
        long start = now - len;
        // 将当前时间参加
        redisTemplate.opsForZSet().add(userKey, String.valueOf(now), now);
        redisTemplate.expire(userKey, len + period, TimeUnit.MILLISECONDS);
        // 移除 [0, start] 之间的记录
        redisTemplate.opsForZSet().reverseRangeByScore(userKey, 0, start);
        // 统计 (start, now] 的数量
        Long count = redisTemplate.opsForZSet().zCard(userKey);
        if (count == null) {
            return false;
        }
        return count <= maxCount;
    }
}