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

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

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

相关推荐
短视频矩阵源码定制2 小时前
矩阵系统哪个好?2025年全方位选型指南与品牌深度解析
java·人工智能·矩阵·架构·aigc
kpli902 小时前
Java开发性能优化
java·jvm
三掌柜6662 小时前
C++ 零基础入门与冒泡排序深度实现
java·开发语言·c++
卿言卿语2 小时前
CC23-最长的连续元素序列长度
java·算法·哈希算法
light_forest3 小时前
tcp_connect_v4接口
java·网络·tcp/ip
JIngJaneIL3 小时前
助农惠农服务平台|助农服务系统|基于SprinBoot+vue的助农服务系统(源码+数据库+文档)
java·前端·数据库·vue.js·论文·毕设·助农惠农服务平台
Mos_x3 小时前
使用Docker构建Node.js应用的详细指南
java·后端
Spirit_NKlaus3 小时前
Springboot自定义配置解密处理器
java·spring boot·后端
龙猫蓝图3 小时前
IDEA新UI设置
java