Java实现限流算法案例

wen java案例 2

本文目录导读:

Java实现限流算法案例

  1. 目录导读
  2. 限流:为什么它是高并发系统的“安全气囊”?
  3. 四大经典限流算法核心原理与Java手写实现
  4. 生产级限流利器:Guava RateLimiter与Redisson RRateLimiter
  5. 实战案例:基于Spring Boot + AOP + 自定义注解实现接口限流
  6. 热点问答(FAQ):面试官最爱问的限流陷阱
  7. 性能对比与选型建议:何时用本地限流,何时用分布式限流?

目录导读

  1. 限流:为什么它是高并发系统的“安全气囊”?
  2. 四大经典限流算法核心原理与Java手写实现
    • 1 固定窗口计数器(Fixed Window Counter)
    • 2 滑动窗口日志(Sliding Window Log)
    • 3 漏桶算法(Leaky Bucket)
    • 4 令牌桶算法(Token Bucket)
  3. 生产级限流利器:Guava RateLimiter与Redisson RRateLimiter
  4. 实战案例:基于Spring Boot + AOP + 自定义注解实现接口限流
  5. 热点问答(FAQ):面试官最爱问的限流陷阱
  6. 性能对比与选型建议:何时用本地限流,何时用分布式限流?

限流:为什么它是高并发系统的“安全气囊”?

在微服务架构与秒杀场景中,流量洪峰往往在毫秒级爆发,如果不加控制,数据库连接池会被瞬间打满,线程池阻塞,最终导致雪崩。限流(Rate Limiting) 的核心目标是:控制单位时间内的请求速率,保证系统的可用性与稳定性,它与熔断(Circuit Breaker)、降级(Degradation)并称高可用“三剑客”。

关键术语:

  • QPS(Queries Per Second):每秒查询率,衡量吞吐量。
  • 阈值(Threshold):允许的最大请求数/速率。
  • 超卖/超限:超过阈值后的策略(拒绝、排队、随机丢弃)。

四大经典限流算法核心原理与Java手写实现

1 固定窗口计数器(Fixed Window Counter)

原理:将时间划分为固定大小的窗口(如1秒),每个窗口维护一个计数器,请求进入则计数器+1,若计数器超过阈值则拒绝,直到下个窗口重置。

缺点:存在“临界突变”问题——假设阈值10,前0.9秒无请求,最后0.1秒涌入10个请求,紧接着下个窗口又涌入10个请求,实际20个请求在0.2秒内通过,可能压垮服务。

代码案例

public class FixedWindowCounter {
    private final int maxCount;       // 窗口允许的最大请求数
    private final long windowSizeMs;  // 窗口大小(毫秒)
    private long windowStart = System.currentTimeMillis();
    private int count;
    public FixedWindowCounter(int maxCount, long windowSizeMs) {
        this.maxCount = maxCount;
        this.windowSizeMs = windowSizeMs;
    }
    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        if (now - windowStart >= windowSizeMs) {
            windowStart = now;
            count = 0;
        }
        if (count < maxCount) {
            count++;
            return true;
        }
        return false;
    }
}

2 滑动窗口日志(Sliding Window Log)

原理:记录每个请求的时间戳,存入有序集合(如TreeSet),每次请求时,剔除所有超出当前窗口(如1秒)的旧时间戳,若剩余数量 < 阈值则放行。

优点:解决了固定窗口的临界问题,精度高(可精确到毫秒粒度)。 缺点:内存占用高(需要存储所有请求时间戳)。

代码案例

public class SlidingWindowLog {
    private final int maxCount;
    private final long windowSizeMs;
    private final TreeSet<Long> timestamps = new TreeSet<>();
    public SlidingWindowLog(int maxCount, long windowSizeMs) {
        this.maxCount = maxCount;
        this.windowSizeMs = windowSizeMs;
    }
    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        long earliest = now - windowSizeMs;
        // 移除过期时间戳
        timestamps.headSet(earliest + 1).clear();
        if (timestamps.size() < maxCount) {
            timestamps.add(now);
            return true;
        }
        return false;
    }
}

3 漏桶算法(Leaky Bucket)

原理:请求先进入一个固定容量的桶,桶底以固定速率“漏出”请求(即处理请求),如果桶满了,则拒绝新请求。控制的是“流出速率”

代码案例(基于线程池模拟)

public class LeakyBucket {
    private final int capacity;           // 桶容量
    private final double leakRatePerSec;  // 溢出速率(每秒)
    private long lastLeakTime = System.nanoTime();
    private double water = 0;
    public LeakyBucket(int capacity, double leakRatePerSec) {
        this.capacity = capacity;
        this.leakRatePerSec = leakRatePerSec;
    }
    public synchronized boolean tryAcquire() {
        long now = System.nanoTime();
        // 先漏水:根据时间差计算漏掉的水量
        double elapsed = (now - lastLeakTime) / 1e9;
        water = Math.max(0, water - elapsed * leakRatePerSec);
        lastLeakTime = now;
        if (water < capacity) {
            water += 1; // 加水
            return true;
        }
        return false;
    }
}

4 令牌桶算法(Token Bucket)—— 最推荐方案

原理:系统以恒定速率往桶里放入令牌(token),请求必须获取到令牌才能被处理,桶有最大容量(突发流量可预存令牌)。控制的是“流入速率”,且允许短暂突发

代码案例

public class TokenBucket {
    private final int capacity;           // 桶容量(最大令牌数)
    private final double refillRatePerSec; // 每秒放入令牌数
    private double tokens = 0;
    private long lastRefillTime = System.nanoTime();
    public TokenBucket(int capacity, double refillRatePerSec) {
        this.capacity = capacity;
        this.refillRatePerSec = refillRatePerSec;
    }
    public synchronized boolean tryAcquire(int numTokens) {
        long now = System.nanoTime();
        double elapsed = (now - lastRefillTime) / 1e9;
        // 补充令牌,但不能超过容量
        tokens = Math.min(capacity, tokens + elapsed * refillRatePerSec);
        lastRefillTime = now;
        if (tokens >= numTokens) {
            tokens -= numTokens;
            return true;
        }
        return false;
    }
}

生产级限流利器:Guava RateLimiter与Redisson RRateLimiter

手写代码适合理解原理,但生产环境建议使用成熟类库。

  • Guava RateLimiter:基于令牌桶实现,但内部使用平滑突发限制(SmoothBursty)平滑预热限制(SmoothWarmingUp) 两种模式,支持 acquire()(阻塞)与 tryAcquire()(非阻塞)。
    RateLimiter limiter = RateLimiter.create(10); // 每秒10个令牌
    if (limiter.tryAcquire(1, 100, TimeUnit.MILLISECONDS)) {
        // 业务处理
    }
  • Redisson RRateLimiter:分布式限流,底层基于Redis Lua脚本,原子性保证多实例下的精确控制,适用于微服务集群。

实战案例:基于Spring Boot + AOP + 自定义注解实现接口限流

场景:对 @RateLimit 注解标注的接口进行每秒最多5次访问限制。

步骤

  1. 定义注解:

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RateLimit {
        double qps() default 5.0;
    }
  2. 切面类(使用Guava RateLimiter,ConcurrentHashMap存储不同方法的RateLimiter):

    @Aspect
    @Component
    public class RateLimitAspect {
        private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();
        @Around("@annotation(rateLimit)")
        public Object around(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
            String methodKey = pjp.getSignature().toLongString();
            RateLimiter limiter = limiters.computeIfAbsent(methodKey, k -> RateLimiter.create(rateLimit.qps()));
            if (!limiter.tryAcquire(1, 50, TimeUnit.MILLISECONDS)) {
                throw new RuntimeException("Too many requests, please try later.");
            }
            return pjp.proceed();
        }
    }
  3. 在Controller使用:

    @RestController
    public class DemoController {
        @GetMapping("/api/order")
        @RateLimit(qps = 5)
        public String createOrder() {
            return "order created";
        }
    }

扩展:如需分布式,可将 RateLimiter 替换为Redisson的 RRateLimiter,传入统一的Redis连接。


热点问答(FAQ):面试官最爱问的限流陷阱

Q1:固定窗口计数器和滑动窗口日志的区别?何时选哪个?

A:固定窗口实现简单,但有“临界突变”风险;滑动窗口日志通过存储时间戳解决精准问题,但内存开销大。如果内存紧张且QPS低于1000,选固定窗口+随机丢弃策略;如果需要严格平滑,选滑动窗口或令牌桶。

Q2:漏桶和令牌桶的根本差异?

A:漏桶强制平滑流出(不管流量多突发,处理速率恒定),适合“保护下游系统”的场景(如数据库写入),但不允许突发,令牌桶允许预存令牌,能应对突发流量(如电商大促闪购),适合“提升用户体验”的API网关。

Q3:Guava RateLimiter为什么不支持分布式?

A:Guava的令牌桶状态存储在本地JVM内存中,多实例部署时各自独立,无法共享全局速率,若需集群限流,必须用Redis或Sentinel(阿里开源)等中央存储方案。

Q4:限流应该放在网关还是业务层?

A最佳实践是两层都做,网关层(如Spring Cloud Gateway)做全局粗粒度限流(按IP或用户);业务层针对特殊接口(如秒杀)做精细粒度限流(按用户ID)。


性能对比与选型建议:何时用本地限流,何时用分布式限流?

算法/工具 精度 内存开销 允许突发 分布式支持 推荐场景
固定窗口计数器 需自实现 简单内部接口(QPS<500)
滑动窗口日志 需自实现 对平滑度要求极高的金融接口
漏桶 需自实现 保护下游慢资源(写数据库)
令牌桶(Guava) 单机应用默认首选
令牌桶(Redisson) 低(居中) 微服务集群、跨实例全局限流

核心选型建议

  • 单机应用:直接使用Guava RateLimiter,代码简洁,性能损耗小于0.1ms。
  • 微服务集群(无网关):使用Redisson RRateLimiter(基于Redis Lua脚本),注意Redis单点问题,可搭配集群模式。
  • 有网关(如Spring Cloud Gateway):优先使用网关内置的 RequestRateLimiter 过滤器(底层是Redis令牌桶),业务层无需再写限流逻辑。

最后总结:限流算法的本质是“用空间换时间”或“用时间换空间”的取舍,理解四种算法的数学原理,掌握Guava与Redisson的API,并能在Spring Boot中通过AOP优雅落地,是Java后端工程师进阶的必备技能,建议在实际项目中先压测确定阈值,再根据下游依赖的“脆弱程度”选择最合适的算法。

抱歉,评论功能暂时关闭!