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

令牌桶限流算法

你提供的 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.xdnf.cn/news/17272.html

相关文章:

  • 蚊子咬人问题何时休:深度学习引领智能灭蚊新时代
  • 【C/C++】形参、实参相关内容整理
  • SpringBoot 整合Langchain4j 对接主流大模型实战详解
  • AI Infra与LLM的联系与差异
  • 《Transformer黑魔法Mask与Softmax、Attention的关系:一个-∞符号如何让AI学会“选择性失明“》
  • 【React 插件】@uiw/react-md-editor 使用教程:从基础使用到自定义扩展
  • 【自动驾驶】《Sparse4Dv3 Advancing End-to-End 3D Detection and Tracking》论文阅读笔记
  • [科普] 从单核到千核:Linux SMP 的“演化史”与工程细节
  • MySQL数据库索引及底层数据结构
  • 16-DS18B20-±0.5℃精度-12bitADC--55°C ~ +125°C
  • 如何测试一个机器是大端还是小端
  • 如何解决网页视频课程进度条禁止拖动?
  • 查看ubuntu server 的基本信息
  • ubuntu 22.04 中安装python3.11 和 3.11 的 pip
  • 自然语言处理的相关概念与问题
  • 如何给小语种视频生成字幕?我的实测方法分享
  • 从《中国开源年度报告》看中国开源力量的十年变迁中,Apache SeaTunnel 的跃迁
  • Numpy科学计算与数据分析:Numpy入门之多平台安装与基础环境配置
  • 学习 Android(十四)NDK基础
  • RocketMQ和Kafka一样有重平衡的问题吗?
  • 人工智能-python-机器学习实战:特征降维、PCA与KNN的核心价值解析
  • LlaMA_Factory实战微调VL大模型
  • o2o 商城系统数据分析管理系统模块设计
  • SpringMVC基础
  • Linux部署tp5.1,nginx服务器不管访问那个方法,一直访问index/index问题解决方法
  • 【YOLOv8改进 - C2f融合】C2f融合EBlock(Encoder Block):低光增强编码器块,利用傅里叶信息增强图像的低光条件
  • 环保监测新范式:边缘计算网关如何为河长制赋能增效?
  • Java面试宝典:Java内存模型与对象可达性判定原理
  • NWinfo(硬件信息检测工具)v1.4.20绿色免费版,U盘随走随检,结果即刻导出
  • ⭐CVPR 文本到 3D 场景生成新突破:Prometheus 框架解析