令牌桶算法简单实现及思考

令牌桶算法简介:

令牌桶算法是限流算法的常见实现 能够起到限流作用,允许一定程度的流量突发

令牌桶算法在限流组件中被广泛使用,如guava,sentinal等

令牌桶算法原理:

  • 令牌桶按固定速率往桶里放入令牌,令牌桶满则丢弃
  • 系统接收用户请求时,从桶里拿出令牌,如果能拿到就通过,否则拒绝用户请求

由于桶容量的设计,当有突发流量到达时,令牌可以马上被取走,读者学习漏桶算法后,可以对比理解为什么令牌桶可以处理一定程度的流量突发

令牌桶的初步设计分析及简单实现

本文以两个最小demo入门令牌桶,对于生产实践级别的令牌桶限流实践,除了令牌桶的算法实现,需要考虑限流粒度,降级方法等

常见的限流算法还有漏桶,滑动窗口等,本文不再赘述

令牌桶,顾名思义,需要有令牌,桶,同时还需要控制放入桶里的令牌速率,数量等

因此最简单的令牌桶应该有以下参数

  • 桶容量
  • 令牌放入速率
  • 当前令牌数量

对于每秒固定放入令牌,主流的限流组件不是依赖于每秒去填充令牌,而是基于时间差去动态计算应该生成的令牌数量

我的理解是与其每秒都去放入令牌(依赖于定时器,有cpu损耗,无效填充等问题)

不如请求到达时再去补充令牌,只要保证请求到达时,他看到的令牌数量是准确的即可

同时,需要考虑并发情况,对于令牌的填充和获取,需要处理其并发,每秒都填充令牌的策略,需要为其设置锁

流量较低时,锁会影响填充效率 流量较高时,锁竞争过大,影响并发性能,以及容易引起上下文切换

因此本文demo采用时间差动态计算应该生成的令牌数量

最小demo实现

java 复制代码
/**
 * @author omenkk7
 * @description 简单令牌桶实现
 * @create 2025/10/8
 */
public class TokenBucketLimiter {

    //桶容量
    private long bucketCapacity;

    //每秒放入多少个令牌
    private long refillRate;
    //当前令牌容量
    private long tokenValue;
    //上次取令牌时间
    private long lastGetTokenTime;

    public TokenBucketLimiter(long bucketCapacity,long refillRate){
this.bucketCapacity=bucketCapacity;
this.refillRate=refillRate;
this.tokenValue=bucketCapacity;
this.lastGetTokenTime=System.nanoTime();
    }


    //取出令牌
    boolean tryGetToken(){
        fillToken();
        //先尝试放入令牌
        if(tokenValue>0){
            tokenValue--;
            return true;
        }
        return false;
    }

    //填充令牌
    void fillToken(){
        long l = System.nanoTime();
        long time=(l-lastGetTokenTime)/1_000_000_000L;
        if(time>0){
            long min=Math.min(bucketCapacity-tokenValue,time*refillRate);
            tokenValue+=min;
            lastGetTokenTime=l;
        }

    }

}

并发问题分析及优化

在上述实现中,没有考虑并发问题.tokenValue的修改不是原子操作,可能会导致并发问题,我们要对tokenValue的修改做出并发控制

如果并发量非常高,我们可以采用加悲观锁的形式 直接在tryGetToken()方法上加锁即可

如果并发量不是很高,可以采用cas自旋来解决并发问题,从而避免悲观锁带来的线程上下文切换等问题

代码demo

java 复制代码
import java.util.concurrent.atomic.AtomicLong;

/**
 * @author omenkk7
 * @description 简单令牌桶实现
 * @create 2025/10/8
 */
public class TokenBucketLimiter {

    //桶容量
    private long bucketCapacity;

    //每秒放入多少个令牌
    private long refillRate;
    //当前令牌容量
    private AtomicLong tokenValue;
    //上次取令牌时间
    private AtomicLong lastGetTokenTime;

    public TokenBucketLimiter(long bucketCapacity, long refillRate) {
        this.bucketCapacity = bucketCapacity;
        this.refillRate = refillRate;
        this.tokenValue=new AtomicLong(bucketCapacity);
        this.lastGetTokenTime.set(System.nanoTime());
    }


    //取出令牌
    boolean tryGetToken() {
        fillToken();
        //先尝试放入令牌
            while (true){
                long curValue = tokenValue.get();
                if (curValue < 0) {
                    return false;
                } else if(tokenValue.compareAndSet(curValue, curValue - 1)){
                        return true;
                }
            }
        }


    //填充令牌
    void fillToken() {
        long l = System.nanoTime();
        long time = (l - lastGetTokenTime.get()) / 1_000_000_000L;
        if (time < 0)return;

            while (true) {
                long curValue = tokenValue.get();
                long adjustValue = Math.min(bucketCapacity - curValue, time * refillRate);
                if(adjustValue<=0)return ;
                //cas修改
                    if(tokenValue.compareAndSet(curValue,curValue+adjustValue))return;

                    //重试
                }


    }

}

通过cas自旋的无锁编程,相比悲观锁

并发情况下,自旋次数过多可能带来cpu损耗

减少了线程的上下文切换:悲观锁在竞争时会导致线程上下文切换

提高并发性:多线程可以同时对共享资源进行操作,提高了并发性

如上的AtomicLong就是采用了util.concurrent.atomic包下的原子类;

通过无锁编程提高了效率

总结

令牌桶算法作为限流算法的经典实现,能够起到限流作用,允许一定程度的流量突发

本文简单介绍了令牌桶算法的简单实现和基础原理

除此之外,还需要了解

  • 限流粒度
  • 降级策略
  • 并发下的线程安全及优化

相关推荐
jserTang7 小时前
Cursor Plan Mode:AI 终于知道先想后做了
前端·后端·cursor
SimonKing7 小时前
SpringBoot集成:5分钟实现HTML转PDF功能
java·后端·程序员
Asthenia04128 小时前
技术复盘:从 Interceptor 到 Filter —— 正确修改 HTTP Request 字段的探索之路
后端
JaguarJack8 小时前
别再用 PHP 动态方法调用了!三个坑让你代码难以维护
后端·php
mudtools8 小时前
打造.NET平台的Lombok:实现构造函数注入、日志注入、构造者模式代码生成等功能
后端·.net
江上月5138 小时前
django与vue3的对接流程详解(下)
后端·python·django
Cikiss8 小时前
图解 bulkProcessor(调度器 + bulkAsync() + Semaphore)
java·分布式·后端·elasticsearch·搜索引擎
Mintopia8 小时前
Next.js 与 Serverless 架构思维:无状态的优雅与冷启动的温柔
前端·后端·全栈
mudtools8 小时前
.NET驾驭Word之力:基于规则自动生成及排版Word文档
后端·.net