【设计题】如何实现限流器

实现限流器需根据场景(单机 / 分布式)选择合适的算法,业界主流方案基于令牌桶、漏桶、滑动窗口等核心思想,结合成熟工具或自研逻辑实现。以下是具体实现方案,涵盖单机和分布式场景,并参考业界主流实践(如 Guava、Redis 等)。

一、核心限流算法(业界基础)

算法 核心思想 优势 劣势 适用场景
固定窗口 单位时间内(如 1 秒)限制请求数,超过则拒绝 实现简单,内存占用低 窗口边缘可能出现流量突增(如 59 秒和 0 秒各发 100 次) 对精度要求不高的场景
滑动窗口 将单位时间拆分为多个小窗口,滑动计算总请求数 解决固定窗口的边缘问题,精度较高 实现复杂,需存储各小窗口计数 中等精度需求
漏桶算法 请求先进入桶中,桶以固定速率流出,溢出则拒绝 平滑流量输出,控制突发流量 无法应对短时间内的合理流量峰值 需严格控制输出速率的场景
令牌桶算法 系统按固定速率生成令牌,请求需获取令牌才能通过,令牌可累积(应对突发流量) 支持合理突发流量,灵活性高 实现稍复杂 大多数限流场景(推荐优先选)

二、单机限流器实现(参考 Guava)

1. 基于 Guava RateLimiter(令牌桶算法)

Guava 是 Java 领域最常用的单机限流工具,底层基于令牌桶算法,支持平滑突发流量和预热流量。

使用示例

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 实现线程安全的固定窗口计数。

java 复制代码
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 脚本(保证原子性)

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 then
    redis.call('ZADD', key, now, now .. ':' .. math.random()) -- 用随机数避免score冲突
    redis.call('EXPIRE', key, windowMillis / 1000 + 1) -- 设置过期时间,避免内存泄漏
    return 1 -- 允许请求
end
return 0 -- 拒绝请求

Java 调用示例

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 存储令牌桶的「当前令牌数」和「最后填充时间」。
  • 每次请求时,先根据当前时间和填充速率计算新增令牌数,再尝试消耗令牌。

适用场景:需要应对突发流量的分布式场景(如秒杀入口)。

四、业界成熟工具与框架集成

  1. Spring Cloud Gateway 限流:基于 Redis 实现滑动窗口限流,配置示例:

    yaml

    XML 复制代码
    spring:
      cloud:
        gateway:
          routes:
            - id: service-route
              uri: lb://service
              predicates:
                - Path=/api/**filters:
                - name: RequestRateLimiter
                  args:
                    redis-rate-limiter.replenishRate: 10 # 令牌生成速率(QPS)
                    redis-rate-limiter.burstCapacity: 20 # 令牌桶容量(最大突发流量)
  2. Sentinel 限流:阿里开源的流量控制框架,支持单机 / 分布式限流,基于滑动窗口算法,可通过控制台动态配置规则:

    java 复制代码
    // 初始化限流规则:资源名"order",QPS=10
    initFlowRules();
    // 限流保护的资源
    try (Entry entry = SphU.entry("order")) {
        // 业务逻辑
    } catch (BlockException e) {
        // 被限流,处理逻辑
    }

五、选型建议

  • 单机限流:优先用 Guava RateLimiter(简单、成熟),简单场景可自研固定窗口。
  • 分布式限流:首选 Redis + Lua 滑动窗口(精度高),或集成 Sentinel(功能全面,支持熔断降级)。
  • 高并发场景:令牌桶算法(支持突发流量)优于漏桶算法(严格限制速率)。

核心原则:根据业务对「精度」「突发流量容忍度」「分布式需求」选择合适方案,优先使用成熟工

相关推荐
海兰16 小时前
使用 Spring AI 打造企业级 RAG 知识库第二部分:AI 实战
java·人工智能·spring
历程里程碑16 小时前
二叉树---二叉树的中序遍历
java·大数据·开发语言·elasticsearch·链表·搜索引擎·lua
小信丶17 小时前
Spring Cloud Stream EnableBinding注解详解:定义、应用场景与示例代码
java·spring boot·后端·spring
无限进步_17 小时前
【C++】验证回文字符串:高效算法详解与优化
java·开发语言·c++·git·算法·github·visual studio
亚历克斯神17 小时前
Spring Cloud 2026 架构演进
java·spring·微服务
七夜zippoe17 小时前
Spring Cloud与Dubbo架构哲学对决
java·spring cloud·架构·dubbo·配置中心
海派程序猿17 小时前
Spring Cloud Config拉取配置过慢导致服务启动延迟的优化技巧
java
阿维的博客日记17 小时前
为什么不逃逸代表不需要锁,JIT会直接删掉锁
java
William Dawson17 小时前
CAS的底层实现
java
九英里路17 小时前
cpp容器——string模拟实现
java·前端·数据结构·c++·算法·容器·字符串