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

详解【限流算法】:令牌桶、漏桶、计算器算法及Java实现

令牌桶算法

令牌桶(Token Bucket)算法以一个设定的速率产生令牌(Token)并放入令牌桶,每次用户请求都得申请令牌,如果令牌不足,则拒绝请求。

令牌的数量也是有上限的。令牌的数量与时间和发放速率强相关,时间流逝的时间越长,会不断往桶里加入越多的令牌,如果令牌发放的速度比申请速度快,令牌桶会放满令牌,直到令牌占满整个令牌桶。

  • 想象一个固定容量的 “令牌桶”,系统按固定速率向桶中添加令牌(比如每秒放 10 个)。
  • 当有请求到达时,需要从桶中获取一个令牌才能被处理;如果桶中没有令牌,请求则被丢弃或排队。
  • 令牌桶的容量决定了允许的最大突发流量(比如桶容量 100,意味着最多允许 100 个请求同时突发)。

关键概念:

  • 令牌生成速率(r):每秒生成的令牌数量(控制长期平均速率)。
  • 令牌桶容量(b):桶最多能存放的令牌数(控制最大突发流量)。
  • 令牌消耗:每个请求消耗 1 个令牌(可调整为按请求大小消耗)。

特点:

  • 平滑流量:通过固定速率生成令牌,限制长期平均请求速率。消费速度不固定。
  • 允许突发:桶中累积的令牌可应对短时间的突发流量(只要不超过桶容量)。
  • 灵活性:可调整令牌生成速率和桶容量,适配不同场景。

Java代码实现

/*** 令牌桶限流算法*/
public class TokenBucketLimiter {// 桶的容量private final int capacity;// 令牌生成速度 (个/秒)private final int rate;// 当前令牌数量private final AtomicInteger tokens;/*** 构造函数* @param capacity 桶的容量* @param rate     令牌生成速度 (个/秒)*/public TokenBucketLimiter(int capacity, int rate) {this.capacity = capacity;this.rate = rate;this.tokens = new AtomicInteger(capacity); // 初始化时令牌桶是满的// 启动令牌生成器线程startTokenProducer();}/*** 启动一个定时任务,以固定速率向桶中添加令牌*/private void startTokenProducer() {ScheduledExecutorService scheduledThreadPool = Executors.newSingleThreadScheduledExecutor();// 延迟0秒后开始执行,每隔1秒执行一次scheduledThreadPool.scheduleAtFixedRate(() -> {// 尝试增加令牌,确保不超过容量// getAndUpdate 是一个原子操作,它会获取当前值,根据函数计算新值,并更新它tokens.getAndUpdate(current -> Math.min(capacity, current + rate));// System.out.println("令牌已补充,当前令牌数: " + tokens.get()); // 可以注释掉,用于调试}, 0, 1, TimeUnit.SECONDS);}/*** 尝试获取一个令牌* @return 如果获取成功则返回 true,否则返回 false*/public boolean tryAcquire() {return tryAcquire(1);}/*** 尝试获取指定数量的令牌* @param numberOfTokens 需要获取的令牌数量* @return 如果获取成功则返回 true,否则返回 false*/public boolean tryAcquire(int numberOfTokens) {if (numberOfTokens <= 0) {throw new IllegalArgumentException("令牌数量必须大于0");}while (true) {int current = tokens.get();// 如果当前令牌数不足以获取所需数量,则直接返回 falseif (current < numberOfTokens) {return false;}// 尝试原子地减少令牌数if (tokens.compareAndSet(current, current - numberOfTokens)) {// 减少成功,返回 truereturn true;}// 如果 compareAndSet 失败,说明在获取和设置之间有其他线程修改了令牌数,// 循环会重试,直到成功或确定无法获取为止。}}/*** 获取当前桶中的令牌数 (主要用于调试和监控)* @return 当前令牌数*/public int getCurrentTokens() {return tokens.get();}}

漏桶算法

想象一个底部有漏洞的 “漏桶”,水(请求 / 数据包)从顶部流入桶中,桶底以固定速率漏水(处理请求)。无论流入速度多快(突发流量),漏水速度始终保持不变,从而限制了输出速率。如果流入速度超过漏水速度,桶会逐渐装满,当桶满后,多余的水会溢出(丢弃请求)。

关键概念:

  • 漏桶容量(b):桶最多能容纳的请求数(或数据量),决定了允许的最大突发流量缓冲。
  • 漏水速率(r):每秒从桶中处理的请求数(固定速率,控制长期输出速率)。
  • 流入速率:请求到达的速率(可变,可能突发)。

令牌桶算法是 “漏桶算法” 的改进版:漏桶算法仅限制输出速率,不允许突发流量;令牌桶允许突发,但长期速率受控。

特点:

  • 平滑输出:无论输入流量如何突发,输出速率始终保持固定(由漏水速率决定)。
  • 限制突发:桶容量限制了最大缓冲请求数,超过容量的突发流量会被丢弃。
  • 无累积令牌:与令牌桶不同,漏桶不会累积 “处理能力”,仅被动缓冲和匀速处理。

/*** 漏桶限流算法*/
public class LeakyBucketLimiter {// 漏桶的容量private final int capacity;// 漏水速率 (个/秒)private final int rate;// 当前桶中的请求数量private final AtomicInteger water = new AtomicInteger(0);/*** 构造函数* @param capacity 漏桶的容量* @param rate     漏水速率 (个/秒)*/public LeakyBucketLimiter(int capacity, int rate) {this.capacity = capacity;this.rate = rate;// 启动漏水线程startLeaker();}/*** 启动一个定时任务,以固定速率从桶中"漏水"(处理请求)*/private void startLeaker() {ScheduledExecutorService scheduledThreadPool = Executors.newSingleThreadScheduledExecutor();// 延迟0秒后开始执行,每隔1秒执行一次scheduledThreadPool.scheduleAtFixedRate(() -> {// 每次漏水,尝试将当前水量减少rate个// getAndUpdate 是原子操作,确保线程安全water.getAndUpdate(current -> {int newWaterLevel = current - rate;// 不能让水量小于0return Math.max(0, newWaterLevel);});// System.out.println("漏水后,当前水量: " + water.get()); // 用于调试}, 0, 1, TimeUnit.SECONDS);}/*** 尝试添加一个请求到漏桶中* @return 如果添加成功(桶未满)则返回 true,否则返回 false(桶满,请求被丢弃)*/public boolean tryAddRequest() {return tryAddRequest(1);}/*** 尝试添加指定数量的请求到漏桶中* @param numberOfRequests 需要添加的请求数量* @return 如果添加成功(桶未满)则返回 true,否则返回 false(桶满,请求被丢弃)*/public boolean tryAddRequest(int numberOfRequests) {if (numberOfRequests <= 0) {throw new IllegalArgumentException("请求数量必须大于0");}while (true) {int currentWaterLevel = water.get();// 检查添加后是否会溢出if (currentWaterLevel + numberOfRequests > capacity) {return false; // 桶满,拒绝请求}// 尝试原子地增加水量if (water.compareAndSet(currentWaterLevel, currentWaterLevel + numberOfRequests)) {return true; // 添加成功}// 如果 compareAndSet 失败,则循环重试}}/*** 获取当前桶中的请求数量 (主要用于调试和监控)* @return 当前水量*/public int getCurrentWaterLevel() {return water.get();}
}

计数器算法

计数器算法的核心思想非常简单:在一个固定的时间窗口内,统计请求的数量,如果请求数超过了预设的阈值,就拒绝后续的请求。

可以想象成一个收费站,在一个小时内(时间窗口)最多允许 100 辆车通过(阈值)。当第 101 辆车到达时,收费站就会关闭栏杆,拒绝其通过。等到下一个小时开始,计数器清零,栏杆重新打开。

优点

  • 实现简单:逻辑非常清晰,代码容易编写和理解。
  • 高效:只涉及简单的时间比较和计数操作,性能开销极小。

缺点(临界问题)

        最严重的问题是 “临界区” 或 “突刺流量” 问题。假设时间窗口是 1 秒,阈值是 100。如果在第 0.9 秒时来了 100 个请求,全部被允许。然后在第 1.1 秒时(进入了新的窗口),又来了 100 个请求,也全部被允许。那么在 0.9 秒到 1.1 秒这短短 0.2 秒内,系统实际上处理了 200 个请求,这可能会瞬间压垮下游服务。

        这个问题的根源在于时间窗口的切换是瞬间完成的,没有平滑过渡。


/*** 计数器限流算法*/
public class CounterLimiter {// 一个时间窗口内的最大请求数private final int maxRequests;// 时间窗口大小(毫秒)private final long windowSize;// 当前窗口的请求计数器private final AtomicInteger currentCount = new AtomicInteger(0);// 当前窗口的起始时间戳(毫秒)private final AtomicLong windowStartTime = new AtomicLong(System.currentTimeMillis());/*** 构造函数* @param maxRequests 一个时间窗口内的最大请求数* @param windowSize  时间窗口大小(秒)*/public CounterLimiter(int maxRequests, int windowSize) {this.maxRequests = maxRequests;this.windowSize = windowSize * 1000L; // 转换为毫秒// 启动一个定时任务,定期检查并重置窗口,避免长时间不请求导致的窗口不更新问题startWindowResetTask();}/*** 尝试获取一个请求许可* @return 如果获取成功则返回 true,否则返回 false*/public boolean tryAcquire() {long now = System.currentTimeMillis();// 1. 检查是否进入了新的时间窗口,如果是则重置计数器和窗口起始时间// 使用 compareAndSet 确保重置操作的原子性if (now - windowStartTime.get() > windowSize) {// 尝试原子地将窗口起始时间更新为当前时间// 只有当当前窗口起始时间仍然是 oldStartTime 时才会更新成功// 这可以防止多个线程同时重置窗口long oldStartTime = windowStartTime.get();if (windowStartTime.compareAndSet(oldStartTime, now)) {// 重置计数器currentCount.set(0);System.out.println("时间窗口已重置,新窗口起始时间: " + now);}}// 2. 检查当前窗口内的请求数是否已超限,并原子地增加计数int current = currentCount.getAndIncrement();return current < maxRequests;}/*** 启动一个定时任务,定期检查窗口是否过期。* 这是一个守护线程,主要用于在长时间没有请求的情况下,也能保证窗口能被重置。*/private void startWindowResetTask() {ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();// 每隔 windowSize/2 的时间检查一次,确保窗口能及时重置scheduler.scheduleAtFixedRate(() -> {long now = System.currentTimeMillis();if (now - windowStartTime.get() > windowSize) {windowStartTime.set(now);currentCount.set(0);// System.out.println("定时任务触发窗口重置"); // 用于调试}}, this.windowSize / 2, this.windowSize / 2, TimeUnit.MILLISECONDS);}
}

可以通过Redis + Lua来实现计数器算法的功能。典型的场景就是用户登录场景,可以用计数器+验证码的功能来实现削峰,降低并发量。可以设定时间窗口为1s,请求根据具体业务设置请求数阈值,每当发出一个用户登录请求就通过Lua脚本在Redis中更新计数器。如果达到了阈值就使当前请求进入验证码流程,然后重置计数器。

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

相关文章:

  • Spring Cloud Config
  • 河南卫生基层系统网站建设企业资质查询系统官网
  • 临沂网站改版购买商标去哪个网站
  • 模块化并行清洗工装:实现规模化清洗的增效方案
  • Vue项目实战《尚医通》,首页医院组件的搭建,笔记09
  • 《新概念英语青少年版》Unit1-4知识点
  • ParameterizedType
  • 订单流战争:AI、区块链与市场透明度的终极博弈
  • 阿里内推-11月新出HC
  • 使用讯飞星火 Spark X1-32K 打造本地知识助手
  • 学习笔记7
  • 广西水利工程建设管理网站网站建设项目费用报价
  • Rust 练习册 :Phone Number与电话号码处理
  • CUDA C++编程指南(3.2.5)——分布式共享内存
  • 华为路由器核心技术详解:数据包的智能导航系统
  • Go基础:字符串常用的系统函数及对应案例详解
  • redis查询速度快的原因?
  • 社区类网站开发网站怎么提升流量
  • 注册网站时手机号格式不正确容易做的html5的网站
  • 如何查询哪些服务器 IP 访问了 Google Cloud 的 Vertex AI API
  • DataWhale-HelloAgents(第一部分:智能体与语言模型基础)
  • Ollama:在本地运行大语言模型的利器
  • 构建智能知识库问答助手:LangChain与大语言模型的深度融合实践
  • 大语言模型如何获得符号逻辑演绎能力?从频率范式到贝叶斯范式的转移
  • 网站建设中的功能新浪微博图床wordpress
  • 【玩泰山派】9、ubuntu22.04安装中文输入法
  • Spring IOC/DI 与 MVC 从入门到实战
  • SCNet超算平台DCU异构环境的Ollama启动服务后无法转发公网的问题解决
  • macOS下如何全文检索epub格式文件?
  • 一键配置 macOS 终极终端:iTerm2 + Oh My Zsh 自动化安装脚本