当前位置: 首页 > news >正文

令牌桶限流算法

你提供的 Java 代码实现的是令牌桶限流算法(Token Bucket Algorithm),这是目前最常用、最灵活的限流算法之一。它允许一定程度的“突发流量”,同时又能控制平均速率。

下面我将:

  1. 逐行详细解释 TokenBucketLimiter 类的每一行代码逻辑和作用
  2. 分析 test4() 方法的运行流程和输出预期
  3. 指出当前实现的优点与潜在问题
  4. 提供优化建议和改进版本

✅ 一、TokenBucketLimiter 类逐行解析(带注释优化建议)

/*** 令牌桶算法限流实现*/
public class TokenBucketLimiter {// 上一次补充令牌的时间(毫秒)private long lastTime = System.currentTimeMillis();/*** 桶的容量* 作用:表示桶最多能存放多少个令牌* 当桶满时,新生成的令牌会被丢弃*/private long capacity;/*** 当前令牌数量* 作用:记录当前桶中可用的令牌数* 每次请求成功时,需从中减去 1 个令牌*/private long tokens;/*** 令牌放入速度,单位:个/秒* 作用:每秒向桶中添加多少个令牌* 例如 rate = 2 表示每秒生成 2 个令牌*/private long rate;/*** 构造函数:初始化令牌桶* @param capacity 桶的最大容量* @param rate 令牌生成速率(每秒生成多少个)*/public TokenBucketLimiter(long capacity, long rate) {this.capacity = capacity;this.rate = rate;this.tokens = capacity; // 初始状态:桶是满的(常见设计)// 注:原代码未初始化 tokens,会导致初始为 0,这是个 Bug!}/*** 尝试获取一个令牌* @return true: 获取成功(放行),false: 获取失败(限流)* 注意:方法加了 synchronized,保证线程安全*/public synchronized boolean tryAcquire() {// 获取当前时间戳(毫秒)long now = System.currentTimeMillis();// 计算自上次请求以来,应该新增的令牌数量// (now - lastTime) 是毫秒差,除以 1000.0 得到秒数,乘以 rate 得到应生成的令牌数// 使用 Math.round 是为了四舍五入,避免浮点误差long increasedTokens = Math.round((now - lastTime) / 1000.0 * rate);System.out.println("now=" + now + ", lastTime=" + lastTime + ", increasedTokens=" + increasedTokens);// 更新当前令牌数:新增令牌,但不能超过桶的容量tokens = Math.min(capacity, tokens + increasedTokens);// 更新“上次补充令牌时间”为当前时间lastTime = now;// 判断是否有足够令牌(至少 1 个)if (tokens < 1) {// 没有可用令牌,拒绝请求return false;} else {// 有令牌,领取一个(消耗一个)tokens--;return true; // 放行请求}}
}

✅ 二、test4() 方法运行流程分析

@Test
public void test4() throws Exception {// 创建令牌桶:容量 10,每秒生成 2 个令牌TokenBucketLimiter limiter = new TokenBucketLimiter(10, 2);for (int i = 0; i < 36; i++) {if (i != 0 && i % 10 == 0) {Thread.sleep(500); // 每处理 10 个请求后,休眠 500ms}boolean tryAcquire = limiter.tryAcquire();if (tryAcquire) {System.out.println("请求" + i + "正常执行");} else {System.out.println("请求" + i + "被限流");}}
}

🧪 运行逻辑模拟(假设启动时间为 T)

  • 桶容量:10
  • 生成速率:2 个/秒
  • 每 10 次请求后休眠 500ms
问题:tokens 未初始化!

原代码中:

private long tokens; // 默认值为 0

→ 构造函数没有初始化 tokens → 初始 tokens = 0

这意味着:第一个请求来时,桶是空的!

这不符合令牌桶“允许突发”的设计初衷。通常应初始化为满桶


✅ 修正后逻辑(假设 tokens = capacity = 10 初始化)

前 10 个请求(i=0~9):
  • 请求密集到来,间隔极短
  • now ≈ lastTime → increasedTokens ≈ 0
  • tokens 从 10 递减到 1 → 全部放行
i=10:
  • 执行 Thread.sleep(500),休眠 500ms
  • 下一次请求(i=11)时,距离上次已过去 ~500ms
  • increasedTokens = round(0.5 * 2) = 1
  • tokens = min(10, 0 + 1) = 1(假设之前已用完)
  • tokens >= 1 → 放行,tokens = 0
后续请求:
  • 若请求间隔短,increasedTokens ≈ 0tokens 仍为 0 → 被限流
  • 每过 500ms 可生成 1 个令牌,可放行 1 次

✅ 预期输出(修正后):

请求0正常执行
请求1正常执行
...
请求9正常执行
请求10被限流
请求11正常执行
请求12被限流
请求13被限流
...
请求35被限流

实际上,由于休眠 500ms,每 500ms 可生成 1 个令牌,所以大约每 500ms 能处理 1 个请求。


⚠️ 三、当前实现的问题与优化建议

❌ 问题1:tokens 未初始化(严重 Bug)

private long tokens; // 默认为 0

→ 第一个请求来时,桶是空的!

修复:在构造函数中初始化为满桶:

this.tokens = capacity;

❌ 问题2:使用 Math.round 可能导致“多补令牌”

long increasedTokens = Math.round((now - lastTime) / 1000.0 * rate);
  • 四舍五入可能导致:本应补 0.6 个,结果补了 1 个 → 变相提高速率
  • 尤其在高并发、高频调用下,可能累积误差

建议:使用 Math.floor 或直接用 double 累积


✅ 优化建议1:使用 double 提高精度(推荐)

private double tokens = 0.0; // 支持小数令牌public synchronized boolean tryAcquire() {long now = System.currentTimeMillis();double seconds = (now - lastTime) / 1000.0;double newTokens = seconds * rate;tokens = Math.min(capacity, tokens + newTokens);lastTime = now;if (tokens >= 1.0) {tokens -= 1.0;return true;} else {return false;}
}

更精确,避免浮点舍入误差,适合高并发场景。


❌ 问题3:synchronized 可能成为性能瓶颈

  • 所有请求串行化判断,影响吞吐量
  • 在高并发下,建议使用 AtomicLong + CAS 实现无锁化

✅ 优化建议2:无锁版本(使用 AtomicLong

import java.util.concurrent.atomic.AtomicLong;public class TokenBucketLimiter {private final long capacity;private final double rate;private final AtomicLong tokens = new AtomicLong();private final AtomicLong lastTime = new AtomicLong(System.currentTimeMillis());public TokenBucketLimiter(long capacity, long rate) {this.capacity = capacity;this.rate = rate;this.tokens.set(capacity); // 初始满桶this.lastTime.set(System.currentTimeMillis());}public boolean tryAcquire() {while (true) {long now = System.currentTimeMillis();long prevTime = lastTime.get();double seconds = (now - prevTime) / 1000.0;double newTokens = seconds * rate;long prevTokens = tokens.get();long updatedTokens = (long) Math.min(capacity, prevTokens + newTokens);if (updatedTokens >= 1) {if (tokens.compareAndSet(prevTokens, updatedTokens - 1) &&lastTime.compareAndSet(prevTime, now)) {return true;}} else {if (lastTime.compareAndSet(prevTime, now)) {return false;}}// CAS 失败,重试}}
}

更高性能,适合高并发场景。


✅ 四、总结

项目说明
算法类型令牌桶算法(Token Bucket)
核心思想系统以固定速率生成令牌,请求需“领取令牌”才能执行
优点支持突发流量(桶满时可瞬间处理多个请求),输出速率灵活
缺点实现稍复杂,需注意浮点精度和线程安全
适用场景API 限流、网关、高并发服务(如秒杀)
对比漏桶令牌桶允许突发,漏桶输出更平滑

🔁 令牌桶 vs 漏桶 对比

特性令牌桶漏桶
是否允许突发✅ 允许(桶满时可突发)❌ 不允许(恒定输出)
输出速率可变(突发时快)恒定
实现难度中等中等
适用场景高并发、允许突发平滑限流、防止冲击

结论

你的代码基本正确,但存在一个关键 Bug:tokens 未初始化,导致第一个请求可能被错误限流。

✅ 推荐修复后的版本:

public class TokenBucketLimiter {private long lastTime = System.currentTimeMillis();private final long capacity;private long tokens;private final long rate;public TokenBucketLimiter(long capacity, long rate) {this.capacity = capacity;this.rate = rate;this.tokens = capacity; // 修复:初始化为满桶}public synchronized boolean tryAcquire() {long now = System.currentTimeMillis();long increasedTokens = (long) Math.floor((now - lastTime) / 1000.0 * rate); // 改用 floortokens = Math.min(capacity, tokens + increasedTokens);lastTime = now;if (tokens >= 1) {tokens--;return true;} else {return false;}}
}

如果你需要,我也可以提供基于 Redis 的分布式令牌桶实现,适用于集群环境。

http://www.dtcms.com/a/318440.html

相关文章:

  • 《动手学深度学习》读书笔记—9.3深度循环神经网络
  • 数字图像处理(冈萨雷斯)第三版:第四章——空间滤波与频域滤波(平滑与锐化)——主要内容和重点
  • SQL166 删除索引
  • 一篇认识synchronized锁
  • JAVA--流程控制语句
  • Android—服务+通知=>前台服务
  • shell基础之EOF的用法
  • 译 | 在 Python 中从头开始构建 Qwen-3 MoE
  • windos安装了python,但是cmd命令行找不到python
  • 012 网络—基础篇
  • 机器学习算法系列专栏:逻辑回归(初学者)
  • flex布局:容器的justify-content属性
  • Python训练Day35
  • Python在生物计算与医疗健康领域的应用(2025深度解析)
  • 局域网内某服务器访问其他服务器虚拟机内相关服务配置
  • 无人机遥控器舵量技术解析
  • 线上Linux服务器的优化设置、系统安全与网络安全策略
  • Android14的QS面板的加载解析
  • 云平台托管集群:EKS、GKE、AKS 深度解析与选型指南-第四章
  • k8s 网络插件 flannel calico
  • 第14届蓝桥杯Scratch选拔赛初级及中级(STEMA)真题2023年1月15日
  • 链式数据结构
  • LangChain4j实战
  • 深入解析系统调试利器:strace 从入门到精通
  • Linux——(16)深入理解程序运行的基石
  • 12. SELinux 加固 Linux 安全
  • react 流式布局(图片宽高都不固定)的方案及思路
  • npm run dev npm run build
  • Activiti7 调用子流程的配置和处理
  • 【Day 17】Linux-SSH远程连接