Java实现TokenBucket RateLimiter无法正常生成令牌问题修复
令牌桶refill逻辑不执行问题修复方案
根因
你现在用独立Thread跑refillBucket()的写法从设计上就容易出问题,常见触发不按间隔填充的原因有三个:
- 手动new的后台线程没有做保活、异常捕获,只要refill逻辑抛一次未捕获异常,线程直接终止,后续再也不会执行填充;如果是while循环+sleep的实现,sleep被打断也会直接终止线程。
- 定时逻辑写错:如果用
scheduleWithFixedDelay、或者循环里先执行业务再sleep,实际间隔是「业务执行时间+配置的间隔时间」,根本卡不准1秒的周期;如果桶数量多,单次refill遍历耗时超过1秒,还会出现任务堆积。 - 并发问题:请求线程取令牌和后台线程填令牌同时修改桶内令牌计数,没有加同步控制,导致新增的令牌被并发写覆盖,看起来就像没执行填充。
最优修复:干掉后台填充线程
令牌桶根本不需要独立线程做定时填充,用懒加载式填充就能彻底解决这类问题:每次请求到达要取令牌之前,先计算当前时间和上次填充的时间差,按实际流逝的时间计算应该补多少令牌,补完再判断是否允许请求通过,完全没有后台任务挂掉、调度不准的问题。
可直接参考实现代码:
import java.util.concurrent.ConcurrentHashMap; public class UserRateLimiter { // 单用户每秒最大请求数、桶最大容量 private static final long MAX_TOKENS_PER_USER = 10; // 每秒生成令牌数 private static final long REFILL_RATE_PER_SECOND = 10; // 存每个用户的令牌桶,key是用户ID private final ConcurrentHashMap<String, TokenBucket> userBuckets = new ConcurrentHashMap<>(); public boolean isAllowed(String userId) { TokenBucket bucket = userBuckets.computeIfAbsent(userId, k -> new TokenBucket()); return bucket.tryAcquire(); } private static class TokenBucket { private long currentTokens; private long lastRefillNanoTime; public TokenBucket() { // 初始预填充10个令牌 this.currentTokens = MAX_TOKENS_PER_USER; this.lastRefillNanoTime = System.nanoTime(); } private void refill() { long now = System.nanoTime(); long elapsedNanos = now - lastRefillNanoTime; // 间隔不足100毫秒直接跳过,减少无意义计算 if (elapsedNanos < 100_000_000L) { return; } // 计算流逝时间对应的新增令牌数 long addTokens = (elapsedNanos / 1_000_000_000L) * REFILL_RATE_PER_SECOND; if (addTokens > 0) { currentTokens = Math.min(MAX_TOKENS_PER_USER, currentTokens + addTokens); lastRefillNanoTime = now; } } // 加锁保证取令牌和填充的原子性 public synchronized boolean tryAcquire() { refill(); if (currentTokens > 0) { currentTokens--; return true; } return false; } } }
这个实现下,不管用户隔多久发请求,都会在请求到达时第一时间按实际间隔补够令牌,不会出现等了1秒还被限流的问题,也没有额外线程的运维开销。
如果你一定要保留后台线程实现
需要做三个强制修改:
- 抛弃手动new Thread的写法,用
ScheduledThreadPoolExecutor调度填充任务,调用scheduleAtFixedRate方法设置1秒间隔,不要用scheduleWithFixedDelay,避免任务执行时间拉长间隔。 - 给填充任务加全量异常捕获,只要有未捕获异常抛到调度层,定时任务会直接永久终止。
- 所有对令牌计数、填充时间的读写都要加同步控制,要么给方法加锁,要么用
AtomicLong存计数,避免并发写丢数据。
避坑提醒:不要写
while(true) { refill(); Thread.sleep(1000); }这种逻辑,refill本身的执行时间会被算到间隔里,实际填充周期永远大于1秒,sleep被打断时线程会直接退出。
内容的提问来源于stack exchange,提问作者OTUser
相关产品推荐
相关产品推荐

