【设计题】如何实现限流器
实现限流器需根据场景(单机 / 分布式)选择合适的算法,业界主流方案基于令牌桶、漏桶、滑动窗口等核心思想,结合成熟工具或自研逻辑实现。以下是具体实现方案,涵盖单机和分布式场景,并参考业界主流实践(如 Guava、Redis 等)。
一、核心限流算法(业界基础)
| 算法 | 核心思想 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|---|
| 固定窗口 | 单位时间内(如 1 秒)限制请求数,超过则拒绝 | 实现简单,内存占用低 | 窗口边缘可能出现流量突增(如 59 秒和 0 秒各发 100 次) | 对精度要求不高的场景 |
| 滑动窗口 | 将单位时间拆分为多个小窗口,滑动计算总请求数 | 解决固定窗口的边缘问题,精度较高 | 实现复杂,需存储各小窗口计数 | 中等精度需求 |
| 漏桶算法 | 请求先进入桶中,桶以固定速率流出,溢出则拒绝 | 平滑流量输出,控制突发流量 | 无法应对短时间内的合理流量峰值 | 需严格控制输出速率的场景 |
| 令牌桶算法 | 系统按固定速率生成令牌,请求需获取令牌才能通过,令牌可累积(应对突发流量) | 支持合理突发流量,灵活性高 | 实现稍复杂 | 大多数限流场景(推荐优先选) |
二、单机限流器实现(参考 Guava)
1. 基于 Guava RateLimiter(令牌桶算法)
Guava 是 Java 领域最常用的单机限流工具,底层基于令牌桶算法,支持平滑突发流量和预热流量。
使用示例:
import com.google.common.util.concurrent.RateLimiter;public class GuavaRateLimiterDemo {public static void main(String[] args) {// 1. 创建限流器:每秒生成10个令牌(QPS=10)RateLimiter limiter = RateLimiter.create(10.0);// 2. 尝试获取令牌(非阻塞)for (int i = 0; i < 15; i++) {boolean allowed = limiter.tryAcquire(); // 尝试获取1个令牌,无等待System.out.println("请求" + i + ":" + (allowed ? "通过" : "被限流"));}// 3. 阻塞获取令牌(适合必须处理的请求)limiter.acquire(2); // 获取2个令牌,阻塞等待System.out.println("获取2个令牌成功,处理请求");}
}
核心特性:
create(10.0):每秒生成 10 个令牌,支持浮点型(如2.5表示每 400 毫秒 1 个令牌)。tryAcquire(timeout, unit):带超时的非阻塞获取,超时未拿到令牌则拒绝。acquire(n):阻塞等待获取n个令牌,适合必须执行的任务。- 预热模式:
RateLimiter.create(10.0, 5, TimeUnit.SECONDS)表示 5 秒内令牌生成速率从 0 逐渐提升到 10QPS,避免冷启动冲击。
2. 自研固定窗口限流器(简单场景)
适合对依赖 Guava 有顾虑的场景,基于 AtomicInteger 实现线程安全的固定窗口计数。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;public class FixedWindowLimiter {private final int maxRequests; // 窗口内最大请求数private final long windowMillis; // 窗口大小(毫秒)private final AtomicInteger count = new AtomicInteger(0); // 当前窗口请求数private volatile long windowStart; // 窗口开始时间public FixedWindowLimiter(int maxRequests, long windowMillis) {this.maxRequests = maxRequests;this.windowMillis = windowMillis;this.windowStart = System.currentTimeMillis();}public boolean tryAcquire() {long now = System.currentTimeMillis();// 1. 检查是否进入新窗口if (now - windowStart > windowMillis) {synchronized (this) { // 加锁确保窗口切换原子性if (now - windowStart > windowMillis) {count.set(0);windowStart = now;}}}// 2. 检查当前窗口请求数是否超过限制return count.incrementAndGet() <= maxRequests;}
}
使用场景:简单的接口限流,优点是实现轻量,缺点是存在窗口边缘流量突增问题。
三、分布式限流器实现(参考 Redis + Lua)
分布式系统中,单机限流无法控制集群整体流量,需基于 Redis 实现全局限流(业界主流方案)。
1. 基于 Redis + 滑动窗口算法(精确限流)
通过 Redis 的 ZSet 存储请求时间戳,滑动窗口内的请求数不超过阈值。
Lua 脚本(保证原子性):
-- 滑动窗口限流:key=限流标识,maxCount=窗口内最大请求数,windowMillis=窗口大小(毫秒)
local key = KEYS[1]
local maxCount = tonumber(ARGV[1])
local windowMillis = tonumber(ARGV[2])
local now = tonumber(ARGV[3])-- 1. 移除窗口外的请求(时间戳 < now - windowMillis)
redis.call('ZREMRANGEBYSCORE', key, 0, now - windowMillis)
-- 2. 统计当前窗口内的请求数
local currentCount = redis.call('ZCARD', key)
-- 3. 若未超过限制,添加当前请求时间戳
if currentCount < maxCount thenredis.call('ZADD', key, now, now .. ':' .. math.random()) -- 用随机数避免score冲突redis.call('EXPIRE', key, windowMillis / 1000 + 1) -- 设置过期时间,避免内存泄漏return 1 -- 允许请求
end
return 0 -- 拒绝请求
Java 调用示例:
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;public class RedisSlidingWindowLimiter {private final StringRedisTemplate redisTemplate;private final DefaultRedisScript<Long> luaScript;public RedisSlidingWindowLimiter(StringRedisTemplate redisTemplate) {this.redisTemplate = redisTemplate;// 加载Lua脚本this.luaScript = new DefaultRedisScript<>();this.luaScript.setScriptText(/* 上述Lua脚本字符串 */);this.luaScript.setResultType(Long.class);}public boolean allowRequest(String key, int maxCount, long windowMillis) {long now = System.currentTimeMillis();// 执行Lua脚本Long result = redisTemplate.execute(luaScript,Collections.singletonList(key),String.valueOf(maxCount),String.valueOf(windowMillis),String.valueOf(now));return result != null && result == 1;}
}
优势:精度高,解决固定窗口的边缘问题;适合集群环境全局限流。
2. 基于 Redis + 令牌桶算法(参考 Redisson)
Redisson 提供了分布式令牌桶实现 RSemaphore,但更灵活的方式是结合 Lua 脚本模拟令牌桶。
核心逻辑:
- 用 Redis 存储令牌桶的「当前令牌数」和「最后填充时间」。
- 每次请求时,先根据当前时间和填充速率计算新增令牌数,再尝试消耗令牌。
适用场景:需要应对突发流量的分布式场景(如秒杀入口)。
四、业界成熟工具与框架集成
Spring Cloud Gateway 限流:基于 Redis 实现滑动窗口限流,配置示例:
yaml
spring:cloud:gateway:routes:- id: service-routeuri: lb://servicepredicates:- Path=/api/**filters:- name: RequestRateLimiterargs:redis-rate-limiter.replenishRate: 10 # 令牌生成速率(QPS)redis-rate-limiter.burstCapacity: 20 # 令牌桶容量(最大突发流量)Sentinel 限流:阿里开源的流量控制框架,支持单机 / 分布式限流,基于滑动窗口算法,可通过控制台动态配置规则:
// 初始化限流规则:资源名"order",QPS=10 initFlowRules(); // 限流保护的资源 try (Entry entry = SphU.entry("order")) {// 业务逻辑 } catch (BlockException e) {// 被限流,处理逻辑 }
五、选型建议
- 单机限流:优先用 Guava RateLimiter(简单、成熟),简单场景可自研固定窗口。
- 分布式限流:首选 Redis + Lua 滑动窗口(精度高),或集成 Sentinel(功能全面,支持熔断降级)。
- 高并发场景:令牌桶算法(支持突发流量)优于漏桶算法(严格限制速率)。
核心原则:根据业务对「精度」「突发流量容忍度」「分布式需求」选择合适方案,优先使用成熟工
