摘要:面试被问到限流算法,很多面试官会让直接手写令牌桶和漏桶的实现。虽然平时用过Redis、Guava等现成的限流工具,但真要手写还是有点慌。今天就来聊聊这两种经典限流算法的区别,并用Java手写实现。
面试被问到限流算法,很多面试官会让直接手写令牌桶和漏桶的实现。虽然平时用过Redis、Guava等现成的限流工具,但真要手写还是有点慌。今天就来聊聊这两种经典限流算法的区别,并用Java手写实现。
很多的限流工具底层都应用了它们
令牌桶的核心思想:固定容量的桶,以固定速率往桶里放令牌,请求来了就从桶拿令牌,没令牌就拒绝。
有点像买票进站,想去坐火车就先去售票窗口买票,买到票了就凭票进入,买不到等待,因为窗口会定时的放票,再去抢。
下图是用Ai生成的,大致能体现出这么个意思
令牌桶特点:
1、可以处理突发流量(桶里有令牌就能用),因为并不是一直请求都很多,但会一直以固定速率向桶里添加令牌,请求少时桶内令牌满了,请求激增可以满桶拿令牌顶一阵
2、原理和实现上相对简单
3、内存占用小
令牌桶适用场景:
接口限流:保护业务系统或者敏感接口防止恶意攻击:抵御Dos或DDos攻击……它的优势在于能够限制平均速率,同时允许一定的突发流量
漏桶
漏桶的核心思想比令牌桶早更简单:请求像水一样流入桶中,桶以固定速率“漏水”处理请求,超出桶容量的请求被丢弃或排队。
漏桶的特点:
1、输出非常平滑稳定
2、能有效保护下游系统(流量平滑)
3、❌ 无法处理突发流量
4、❌ 可能造成请求延迟
漏桶适用场景:
令牌桶实现
import java.util.concurrent.atomic.AtomicLong;public class TokenBucket { // 桶容量(最大令牌数) private final long capacity; // 令牌填充速率(令牌/秒) private final long refillRate; // 当前令牌数量 private final AtomicLong tokens; // 上次填充时间戳(纳秒) private long lastRefillTime; public TokenBucket(long capacity, long refillRate) { this.capacity = capacity; this.refillRate = refillRate; this.tokens = new AtomicLong(capacity); this.lastRefillTime = System.nanoTime; } // 示例使用 public static void main(String args) throws InterruptedException { // 创建桶:容量10令牌,每秒填充5令牌 TokenBucket bucket = new TokenBucket(10, 2); // 模拟请求 for (int i = 1; i 0) { tokens.decrementAndGet; return true; } return false; } /** * 尝试获取多个令牌 * * @param numTokens 请求令牌数 */ public synchronized boolean tryAcquire(int numTokens) { refillTokens; if (tokens.get >= numTokens) { tokens.addAndGet(-numTokens); return true; } return false; } // 根据时间差补充令牌 private void refillTokens { long now = System.nanoTime; // 计算时间差(秒) double elapsedSec = (now - lastRefillTime) * 1e-9; // 计算应补充的令牌数 long tokensToAdd = (long) (elapsedSec * refillRate); if (tokensToAdd > 0) { tokens.set(Math.min(capacity, tokens.get + tokensToAdd)); lastRefillTime = now; } }}使用 AtomicLong 保证线程安全。通过时间差动态计算补充的令牌数。桶容量限制突发流量的最大值。漏桶实现
import java.util.concurrent.atomic.AtomicLong;public class LeakyBucket { // 桶容量(最大请求数) private final long capacity; // 漏水速率(请求/秒) private final long leakRate; // 当前水量(待处理请求数) private final AtomicLong water; // 上次漏水时间戳(毫秒) private long lastLeakTime; public LeakyBucket(long capacity, long leakRate) { this.capacity = capacity; this.leakRate = leakRate; this.water = new AtomicLong(0); this.lastLeakTime = System.currentTimeMillis; } // 示例使用 public static void main(String args) throws InterruptedException { // 创建桶:容量5请求,每秒处理2请求 LeakyBucket bucket = new LeakyBucket(5, 1); // 模拟请求 for (int i = 1; i 0) { // 计算漏水量 long leaked = (long) (elapsedMs * leakRate / 1000.0); if (leaked > 0) { water.updateAndGet(cur -> Math.max(0, cur - leaked)); lastLeakTime = now; } } }}漏出速率固定,确保请求处理平滑。水量超过容量时直接拒绝请求。面试官可能会问的问题:
Q: 两种算法的核心区别是什么?
A: 令牌桶允许突发,漏桶强制平滑输出
Q: 什么场景用令牌桶,什么场景用漏桶?
A: 需要处理突发用令牌桶,需要保护下游用漏桶
Q: 如何选择桶的容量和速率?
A: 根据业务峰值、系统承载能力、用户体验综合考虑
Q: 分布式环境下如何实现?
A: 可以用Redis实现,或者用一致性哈希分片
手写限流算法是一般在高级别的面试中不太会出现,但它们的基础概念要掌握,在考场景题时它们都是不错的方案。
简单记:令牌桶像ATM机,有钱就能取;漏桶像水龙头,固定流速出水。
完活!
来源:不秃头程序员