Java限流详解

发布时间:2023-05-20

一、限流概述

随着互联网化程度的不断提高,大量用户同时使用某些功能,可能会导致某些功能被耗尽,从而导致系统瘫痪。为了应对这种情况,限流成为了解决问题的重要手段。限流是指根据给定的规则和策略来控制、限制应用程序流量,以达到保护后端应用的目的。Java作为一门流行的编程语言,提供了多种限流机制,可以解决大流量并发任务下服务器资源被耗光的问题。

二、限流分类

限流根据限流目的和实现方式的不同,可以分为五种类型:

1. 固定窗口算法

固定窗口算法限制时间窗口内的请求数量,同一时间窗口内的请求量不能超过设定的阈值。

public class FixedWindowRateLimiter implements RateLimiter {
    private AtomicInteger counter = new AtomicInteger(0);
    private final int limit;
    private final int intervalMs;
    private final ThreadLocal<Long> last = new ThreadLocal<Long>() {
        @Override
        protected Long initialValue() {
            return System.currentTimeMillis();
        }
    };
    public FixedWindowRateLimiter(int limit, int intervalMs) {
        this.limit = limit;
        this.intervalMs = intervalMs;
    }
    @Override
    public boolean tryAcquire() {
        long now = System.currentTimeMillis();
        long lastTime = last.get();
        if (now - lastTime > intervalMs) {
            counter.set(0);
            last.set(now);
        }
        return counter.incrementAndGet() <= limit;
    }
}

2. 滑动窗口算法

滑动窗口算法仍然限制时间窗口内的请求数量,不同的是时间窗口可以滑动,即滑动窗口算法最近的一段时间窗口都被重复使用。

public class SlidingWindowRateLimiter implements RateLimiter {
    private AtomicInteger counter = new AtomicInteger(0);
    private final int limit;
    private final int intervalMs;
    private final int bucketSize;
    private final ConcurrentLinkedDeque<Long> windows = new ConcurrentLinkedDeque<>();
    public SlidingWindowRateLimiter(int limit, int intervalMs, int bucketSize) {
        this.limit = limit;
        this.intervalMs = intervalMs;
        this.bucketSize = bucketSize;
    }
    @Override
    public boolean tryAcquire() {
        long now = System.currentTimeMillis();
        if (!windows.isEmpty() && now - windows.peek() > intervalMs) {
            int size = windows.size();
            for (int i = 0; i < size && i < bucketSize; i++) {
                windows.poll();
            }
            counter.set(windows.size());
        }
        if (counter.incrementAndGet() <= limit) {
            windows.offer(System.currentTimeMillis());
            return true;
        }
        counter.decrementAndGet();
        return false;
    }
}

3. Leaky Bucket算法

Leaky Bucket算法是一种限制请求流量的算法,它通过给定最大速率控制进入网络的数据数量,有效地防止了请求峰的出现,从而保护了系统。

public class LeakyBucketRateLimiter implements RateLimiter {
    private AtomicInteger counter = new AtomicInteger(0);
    private final int limit;
    private final int intervalMs;
    private final float leakRate;
    private AtomicLong leftWater = new AtomicLong(0);
    private AtomicLong lastUpdateTime = new AtomicLong(System.nanoTime());
    public LeakyBucketRateLimiter(int limit, int intervalMs, float leakRate) {
        this.limit = limit;
        this.intervalMs = intervalMs;
        this.leakRate = leakRate;
    }
    @Override
    public boolean tryAcquire() {
        long now = System.nanoTime();
        long timeDelta = now - lastUpdateTime.get();
        long left = leftWater.addAndGet((long) (-timeDelta * leakRate));
        if (left < 0) {
            left = 0;
            leftWater.set(left);
        }
        if (left < limit) {
            if (counter.addAndGet(1) <= limit) {
                return true;
            } else {
                counter.decrementAndGet();
            }
        }
        lastUpdateTime.set(now);
        return false;
    }
}

4. Token Bucket算法

Token Bucket算法是一种计量允许突发请求流量的算法,它通过对请求进行统计并抑制突发请求流量,保护系统正常运行。

public class TokenBucketRateLimiter implements RateLimiter {
    private AtomicInteger tokens = new AtomicInteger(0);
    private final int limit;
    private final int intervalMs;
    private final int refillTimeMs;
    private final int refillCount;
    private final ThreadLocal<Long> last = new ThreadLocal<Long>() {
        @Override
        protected Long initialValue() {
            return System.currentTimeMillis();
        }
    };
    public TokenBucketRateLimiter(int limit, int intervalMs, int refillTimeSec, int refillCount) {
        this.limit = limit;
        this.intervalMs = intervalMs;
        this.refillTimeMs = refillTimeSec * 1000;
        this.refillCount = refillCount;
    }
    @Override
    public boolean tryAcquire() {
        long now = System.currentTimeMillis();
        int currentCount = tokens.get();
        if (now - last.get() > refillTimeMs) {
            int newTokens = (int) ((now - last.get()) / refillTimeMs * refillCount);
            if (newTokens > 0) {
                tokens.set(Math.min(currentCount + newTokens, limit));
                last.set(now);
            }
        }
        return tokens.compareAndSet(currentCount, currentCount - 1);
    }
}

5. 基于Google Guava的流量控制

Google Guava提供了一种基于令牌桶算法的流控方式,只需定制化配置令牌生成速率和令牌桶大小,即可轻松实现流控。

private static final RateLimiter RATE_LIMITER = RateLimiter.create(1.0);
public void rateLimiter() {
    if (RATE_LIMITER.tryAcquire()) {
        // Allow access
    } else {
        // Reject request
    }
}

三、应用场景

限流机制可以用于很多场景,例如:高并发请求、海量爬虫请求、大量数据库查询操作等。以下是限流机制的一些具体应用场景:

1. 秒杀系统抢购场景

秒杀场景下用户同时发起大量请求,服务器负担过重,为此可以通过限流措施对在一定时间内访问的请求进行限制,比如高并发时采用Leaky Bucket算法进行限流,控制请求流量。

2. 海量爬虫请求场景

爬虫请求注意要保证请求合理、抢占时间均匀,限流是抵抗爬虫攻击、保护服务器资源的重要手段,可以采用固定窗口、滑动窗口等算法进行限流。

3. 流媒体播放区分不同视频质量场景

流媒体播放场景下用户可以通过质量切换功能,切换不同的码率、分辨率、FPS等参数,可以通过Token Bucket算法或基于Guava的令牌桶算法实现限流。

四、总结

本文从限流基本概念入手,详细介绍了Java下的五种常见限流算法及其应用场景。针对不同的应用场景,可以选择不同的限流算法实现或者结合使用多个限流算法进行流量控制,以达到保护系统的目的。