首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >说说什么是令牌桶算法

说说什么是令牌桶算法

作者头像
SmileNicky
发布2026-02-26 08:27:10
发布2026-02-26 08:27:10
1320
举报
文章被收录于专栏:Nicky's blogNicky's blog

我来为你详细讲解令牌桶算法(Token Bucket),这是限流领域的核心算法,面试高频考点。


一、核心原理

直观理解

想象一个水桶(桶容量固定),以恒定速率往里面放令牌(token)。请求来了需要拿到令牌才能执行,拿不到就被限流。

代码语言:javascript
复制
        令牌以固定速率生成
              ↓
    ┌─────────────────┐
    │    令牌桶        │ ←── 桶容量固定(如容量=10)
    │  [○][○][○][○]   │     当前令牌数 ≤ 容量
    └────────┬────────┘
             ↓ 请求尝试获取令牌
    ┌─────────────────┐
    │   请求处理       │ ←── 拿到令牌:通过
    │   请求被拒绝     │ ←── 没令牌:限流/等待
    └─────────────────┘
关键参数

参数

含义

示例

容量(Capacity)

桶最多存多少令牌

100 tokens

产生速率(Rate)

每秒/每毫秒产生令牌数

10 tokens/sec

当前令牌数

实时可用令牌

动态变化


二、算法流程(伪代码)

代码语言:javascript
复制
class TokenBucket {
    long capacity;          // 桶容量
    long tokens;            // 当前令牌数
    long lastTime;          // 上次更新时间
    long rate;              // 产生速率(令牌/毫秒)
    
    synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        // 1. 计算这段时间产生的令牌数,并加到桶中(不超过容量)
        tokens = Math.min(capacity, tokens + (now - lastTime) * rate);
        lastTime = now;
        
        // 2. 尝试获取令牌
        if (tokens > 0) {
            tokens--;
            return true;    // 获取成功,请求通过
        }
        return false;       // 获取失败,被限流
    }
}

三、核心特性 ⭐面试重点

特性

说明

意义

允许突发流量

桶内有存量令牌时,可一次性取走

应对正常业务尖峰(非恶意攻击)

长期速率恒定

产生速率固定,限制平均流量

保护系统不被持续高流量压垮

平滑限流

流量曲线平滑,非断崖式拒绝

用户体验更好

突发流量示例
代码语言:javascript
复制
假设:容量=10,速率=1 token/sec

时间线:
t0: 桶满10个令牌,突然来了15个请求
    → 前10个请求瞬间通过(突发)
    → 后5个请求按1个/秒匀速通过
    → 长期平均速率 = 1 req/sec

四、vs 漏桶算法(Leaky Bucket)

对比维度

令牌桶 Token Bucket

漏桶 Leaky Bucket

核心思想

令牌匀速产生,请求取令牌

请求匀速流出,桶缓冲请求

流量形状

允许突发,长期匀速

强制匀速,无突发

桶内存储

存储令牌(许可)

存储请求(数据)

突发流量

✅ 支持

❌ 不支持

实现复杂度

简单(只需记录令牌数)

稍复杂(需要队列+定时消费)

典型应用

网络流量控制、API限流

流量整形(Traffic Shaping)

代码语言:javascript
复制
漏桶示意图(强制匀速):
    请求流入 → [ 漏桶/队列 ] → 匀速流出(固定速率处理)
                  ↑
              桶满则溢出(拒绝)

面试金句令牌桶限制的是平均速率,允许突发;漏桶限制的是瞬时流速,强制匀速


五、Java 实现示例

1. 简单版(理解原理)
代码语言:javascript
复制
public class TokenBucket {
    private final long capacity;      // 桶容量
    private final long rate;          // 每秒产生令牌数
    private double tokens;            // 当前令牌数(用double支持小数)
    private long lastTimestamp;       // 上次更新时间(秒)
    
    public TokenBucket(long capacity, long rate) {
        this.capacity = capacity;
        this.rate = rate;
        this.tokens = capacity;       // 初始满桶
        this.lastTimestamp = System.currentTimeMillis() / 1000;
    }
    
    public synchronized boolean allowRequest() {
        long now = System.currentTimeMillis() / 1000;
        // 计算新产生的令牌
        tokens = Math.min(capacity, tokens + (now - lastTimestamp) * rate);
        lastTimestamp = now;
        
        if (tokens >= 1) {
            tokens--;
            return true;
        }
        return false;
    }
}
2. 生产级(Guava RateLimiter)
代码语言:javascript
复制
import com.google.common.util.concurrent.RateLimiter;

// 创建:每秒产生2个令牌(平滑突发)
RateLimiter limiter = RateLimiter.create(2.0);

// 阻塞获取(拿不到就等待)
limiter.acquire();  

// 非阻塞尝试(立即返回是否成功)
if (limiter.tryAcquire()) {
    // 处理请求
} else {
    // 被限流,返回503或降级
}
3. 分布式场景(Redis + Lua)
代码语言:javascript
复制
-- 令牌桶限流 Lua 脚本(原子操作)
local key = KEYS[1]           -- 桶的key
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2]) 
local now = tonumber(ARGV[3]) -- 当前时间戳(毫秒)
local requested = tonumber(ARGV[4]) -- 需要令牌数

local fill_time = capacity / rate
local ttl = math.floor(fill_time * 2)

-- 获取当前状态
local last_tokens = redis.call('get', key .. ':tokens')
local last_updated = redis.call('get', key .. ':last_updated')

if last_tokens == false then
    last_tokens = capacity
    last_updated = 0
else
    last_tokens = tonumber(last_tokens)
    last_updated = tonumber(last_updated)
end

-- 计算新令牌数
local delta = math.max(0, now - last_updated)
local filled_tokens = math.min(capacity, last_tokens + (delta * rate / 1000))
local allowed = filled_tokens >= requested

local new_tokens = filled_tokens
if allowed then
    new_tokens = filled_tokens - requested
end

-- 更新状态
redis.call('setex', key .. ':tokens', ttl, new_tokens)
redis.call('setex', key .. ':last_updated', ttl, now)

return allowed and 1 or 0

六、应用场景

场景

配置建议

原因

API网关限流

容量=100,速率=1000/s

应对正常流量尖峰

用户级限流

容量=20,速率=10/s

防单个用户刷接口

下载限速

漏桶更合适

需要严格匀速

秒杀系统

令牌桶+队列

允许瞬间高并发,保护后端


七、面试高频问题

Q1:令牌桶的"桶"具体指什么?是队列吗?

不是队列!只是一个计数器(记录当前可用令牌数)。不需要存储具体请求,这是和漏桶的本质区别。

Q2:令牌桶会有"精度"问题吗?

有。如果更新间隔太长,计算的令牌数可能不准确。生产环境建议:

  • 单机:每次请求时计算(如上述代码)
  • 分布式:用Redis+Lua保证原子性,或定时同步

Q3:Guava的RateLimiter是令牌桶吗?

是,但做了优化。它使用平滑预热(SmoothWarmingUp)和平滑突发(SmoothBursty)两种模式,基于令牌桶思想但实现更高效(使用resync算法避免定时任务)。

Q4:令牌桶容量设置多大合适?

  • 太小:无法应对正常突发,误杀
  • 太大:失去限流意义
  • 经验值:正常峰值的2-3倍,或根据业务容忍的最大延迟计算(容量=速率×最大等待时间)

八、一句话总结

令牌桶 = 匀速生产 + 桶内缓存 + 突发消耗 + 长期限速。既保护系统,又兼顾用户体验,是互联网限流的首选算法。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-02-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、核心原理
    • 直观理解
    • 关键参数
  • 二、算法流程(伪代码)
  • 三、核心特性 ⭐面试重点
    • 突发流量示例
  • 四、vs 漏桶算法(Leaky Bucket)
  • 五、Java 实现示例
    • 1. 简单版(理解原理)
    • 2. 生产级(Guava RateLimiter)
    • 3. 分布式场景(Redis + Lua)
  • 六、应用场景
  • 七、面试高频问题
  • 八、一句话总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档