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

实现限流器需根据场景(单机 / 分布式)选择合适的算法,业界主流方案基于令牌桶、漏桶、滑动窗口等核心思想,结合成熟工具或自研逻辑实现。以下是具体实现方案,涵盖单机和分布式场景,并参考业界主流实践(如 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(功能全面,支持熔断降级)。
  • 高并发场景:令牌桶算法(支持突发流量)优于漏桶算法(严格限制速率)。

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

相关推荐
海鸥-w2 小时前
什么是AOP
java
Cc.Y3 小时前
Java零基础入门:可变字符串与包装类:StringBuilder、StringBuffer 与 通讯录管理系统实战
java·开发语言·python
paopaokaka_luck3 小时前
基于springboot+vue的智慧家政管理系统(AI问答、 WebSocket实时沟通、ECharts运营分析、订单规则与服务追踪)
java·javascript·spring boot·数据分析·echarts·mybatis
sm_926787053 小时前
RFID 标签打印的技术实现要点与二次开发实践
java·大数据·前端·c++·编辑器
阿森_dev3 小时前
2027 届毕设选题怎么选:30 个 Java 方向选题的实现难度排序
java·开发语言·spring boot·毕业设计·课程设计
Sarvartha3 小时前
Lambda 表达式和函数式接口
java·开发语言
计算机毕业设计杰瑞3 小时前
【计算机毕设精品】基于SpringBoot的一体化教务与学业质量管理系统的设计与实现,附源码_增删改查_java项目_可完美运行部署_网站项目
java·spring boot·课程设计
colinu4 小时前
C++ 异步任务与线程生命周期:`std::async`、`future`、`thread`、`jthread` 与协作式退出
java·jvm·c++
蜗牛互联网4 小时前
Python + JSON Schema 实现工单结构化输出与本地复核
java·人工智能·后端
逐光者9334 小时前
STM32——SPI 屏 · Flash · 高速
java·前端·stm32