Guava学习计划-Semaphore—RateLimiter-漏桶算法-令牌桶算法

本文深入解析了限流算法中的漏桶算法与令牌桶算法,并通过示例代码展示了这两种算法的具体应用,包括如何使用RateLimiter进行限流。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

java中对于生产者消费者模型,或者小米手机营销  1分钟卖多少台手机等都存在限流的思想在里面。
关于限流 目前存在两大类,从线程个数(jdk1.5 Semaphore)和RateLimiter速率(guava)
Semaphore:从线程个数限流
RateLimiter:从速率限流  目前常见的算法是漏桶算法和令牌算法
令牌桶算法。相比漏桶算法而言区别在于,令牌桶是会去匀速的生成令牌,拿到令牌才能够进行处理,类似于匀速往桶里放令牌
漏桶算法是:生产者消费者模型,生产者往木桶里生产数据,消费者按照定义的速度去消费数据
应用场景:
漏桶算法:必须读写分流的情况下,限制读取的速度
令牌桶算法:必须读写分离的情况下,限制写的速率或者小米手机饥饿营销的场景  只卖1分种抢购1000
实现的方法都是一样。RateLimiter来实现
对于多线程问题查找时,很多时候可能使用的类都是原子性的,但是由于代码逻辑的问题,也可能发生线程安全问题
一、问题描述  
  某天A君突然发现自己的接口请求量突然涨到之前的10倍,没多久该接口几乎不可使用,并引发连锁反应导致整个系统崩溃。如何应对这种情况呢?生活给了我们答案:比如老式电闸都安装了保险丝,一旦有人使用超大功率的设备,保险丝就会烧断以保护各个电器不被强电流给烧坏。同理我们的接口也需要安装上“保险丝”,以防止非预期的请求对系统压力过大而引起的系统瘫痪,当流量过大时,可以采取拒绝或者引流等机制。 
二、常用的限流算法
      常用的限流算法有两种:漏桶算法和令牌桶算法,这篇博文介绍得比较清晰(过载保护算法浅析)。
      漏桶算法思路很简单,请求先进入到漏桶里,漏桶以一定的速度出水,当水请求过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。

图1 漏桶算法示意图
      对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。如图2所示,令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。

图2 令牌桶算法示意图
三、限流工具类RateLimiter
   google开源工具包guava提供了限流工具类RateLimiter,该类基于“令牌桶算法”,非常方便使用。该类的接口描述请参考:RateLimiter接口描述,具体的使用请参考:RateLimiter使用实践。


1.关于RateLimter和Semphore简单用法
package concurrent;

import com.google.common.util.concurrent.RateLimiter;

import java.util.concurrent.*;
import java.util.stream.IntStream;

import static java.lang.Thread.currentThread;

/**
 * ${DESCRIPTION}
 * 关于限流 目前存在两大类,从线程个数(jdk1.5 Semaphore)和RateLimiter速率(guava)
 * Semaphore:从线程个数限流
 * RateLimiter:从速率限流  目前常见的算法是漏桶算法和令牌算法,下面会具体介绍
 *
 * @author mengxp
 * @version 1.0
 * @create 2018-01-15 22:44
 **/
public class RateLimiterExample {

   //Guava  0.5的意思是 1秒中0.5次的操作,2秒1次的操作  从速度来限流,从每秒中能够执行的次数来
    private final static RateLimiter limiter=RateLimiter.create(0.5d);


    //同时只能有三个线程工作 Java1.5  从同时处理的线程个数来限流
    private final static Semaphore sem=new Semaphore(3);
    private static void testSemaphore(){
        try {
            sem.acquire();
            System.out.println(currentThread().getName()+" is doing work...");
            TimeUnit.MILLISECONDS.sleep(ThreadLocalRandom.current().nextInt(10));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            sem.release();
            System.out.println(currentThread().getName()+" release the semephore..other thread can get and do job");
        }
    }

    public static void runTestSemaphore(){
        ExecutorService service = Executors.newFixedThreadPool(10);
        IntStream.range(0,10).forEach((i)->{
            //RateLimiterExample::testLimiter 这种写法是创建一个线程
            service.submit(RateLimiterExample::testSemaphore);
        });
    }

    /**
     * Guava的RateLimiter
     */
    private static void testLimiter(){
        System.out.println(currentThread().getName()+" waiting  " +limiter.acquire());
    }

    //Guava的RateLimiter
    public static void runTestLimiter(){
        ExecutorService service = Executors.newFixedThreadPool(10);
        IntStream.range(0,10).forEach((i)->{
            //RateLimiterExample::testLimiter 这种写法是创建一个线程
            service.submit(RateLimiterExample::testLimiter);
        });
    }



    public static void main(String[] args) {
        IntStream.range(0,10).forEach((a)-> System.out.println(a));//从0-9
        //runTestLimiter();
        runTestSemaphore();
    }
}

2.实现漏桶算法
package concurrent.BucketAl;

import com.google.common.util.concurrent.Monitor;
import com.google.common.util.concurrent.RateLimiter;

import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

import static java.lang.Thread.currentThread;

/**
 * ${DESCRIPTION}
 *
 * @author mengxp
 * @version 1.0
 * @create 2018-01-20 22:42
 * 实现漏桶算法 实现多线程生产者消费者模型 限流
 **/
public class Bucket {
    //定义桶的大小
    private final ConcurrentLinkedQueue<Integer> container=new ConcurrentLinkedQueue<>();

    private final static int  BUCKET_LIMIT=1000;

    //消费者 不论多少个线程,每秒最大的处理能力是1秒中执行10次
    private final RateLimiter consumerRate=RateLimiter.create(10d);

    //往桶里面放数据时,确认没有超过桶的最大的容量
    private Monitor offerMonitor=new Monitor();

    //从桶里消费数据时,桶里必须存在数据
    private Monitor consumerMonitor=new Monitor();


    /**
     * 往桶里面写数据
     * @param data
     */
    public void submit(Integer data){
        if (offerMonitor.enterIf(offerMonitor.newGuard(()->container.size()<BUCKET_LIMIT))){
            try {
                container.offer(data);
                System.out.println(currentThread()+" submit.."+data+" container size is :["+container.size()+"]");
            } finally {
                offerMonitor.leave();
            }
        }else {
            //这里时候采用降级策略了。消费速度跟不上产生速度时,而且桶满了,抛出异常
            //或者存入MQ DB等后续处理
            throw new IllegalStateException(currentThread().getName()+"The bucket is ful..Pls latter can try...");
        }
    }


    /**
     * 从桶里面消费数据
     * @param consumer
     */
    public void takeThenConsumer(Consumer<Integer> consumer){
        if (consumerMonitor.enterIf(consumerMonitor.newGuard(()->!container.isEmpty()))){
            try {
                //不打印时 写 consumerRate.acquire();
                System.out.println(currentThread()+"  waiting"+consumerRate.acquire());
                Integer data = container.poll();
                //container.peek() 只是去取出来不会删掉
                consumer.accept(data);
            }finally {
                consumerMonitor.leave();
            }
        }else {
            //当木桶的消费完后,可以消费那些降级存入MQ或者DB里面的数据
            System.out.println("will consumer Data from MQ...");
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}
2.1 漏桶算法测试类
package concurrent.BucketAl;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;

import static java.lang.Thread.currentThread;

/**
 * ${DESCRIPTION}
 *
 * @author mengxp
 * @version 1.0
 * @create 2018-01-20 23:11
 * 漏桶算法测试
 * 实现漏桶算法 实现多线程生产者消费者模型 限流
 **/
public class BuckerTest {

    public static void main(String[] args) {
        final Bucket bucket = new Bucket();
        final AtomicInteger DATA_CREATOR = new AtomicInteger(0);

        //生产线程 10个线程 每秒提交 50个数据  1/0.2s*10=50个
        IntStream.range(0, 10).forEach(i -> {
            new Thread(() -> {
                for (; ; ) {
                    int data = DATA_CREATOR.incrementAndGet();
                    try {
                        bucket.submit(data);
                        TimeUnit.MILLISECONDS.sleep(200);
                    } catch (Exception e) {
                        //对submit时,如果桶满了可能会抛出异常
                        if (e instanceof IllegalStateException) {
                            System.out.println(e.getMessage());
                            //当满了后,生产线程就休眠1分钟
                            try {
                                TimeUnit.SECONDS.sleep(60);
                            } catch (InterruptedException e1) {
                                e1.printStackTrace();
                            }
                        }
                    }
                }
            }).start();
        });


        //消费线程  采用RateLimiter每秒处理10个  综合的比率是5:1
        IntStream.range(0, 10).forEach(i -> {
            new Thread(
                    () -> {
                        for (; ; ) {
                            bucket.takeThenConsumer(x -> {
                                System.out.println(currentThread()+"C.." + x);
                            });
                        }
                    }
            ).start();
        });

    }
}
3.令牌桶算法
package concurrent.TokenBucket;

import com.google.common.util.concurrent.RateLimiter;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import static java.lang.Thread.currentThread;
import static java.lang.Thread.interrupted;

/**
 * ${DESCRIPTION}
 *
 * @author mengxp
 * @version 1.0
 * @create 2018-01-21 0:18
 * 令牌桶算法。相比漏桶算法而言区别在于,令牌桶是会去匀速的生成令牌,拿到令牌才能够进行处理,类似于匀速往桶里放令牌
 * 漏桶算法是:生产者消费者模型,生产者往木桶里生产数据,消费者按照定义的速度去消费数据
 *
 * 应用场景:
 * 漏桶算法:必须读写分流的情况下,限制读取的速度
 * 令牌桶算法:必须读写分离的情况下,限制写的速率或者小米手机饥饿营销的场景  只卖1分种抢购1000
 *
 * 实现的方法都是一样。RateLimiter来实现
 * 对于多线程问题查找时,很多时候可能使用的类都是原子性的,但是由于代码逻辑的问题,也可能发生线程安全问题
 **/
public class TokenBuck {

    //可以使用 AtomicInteger+容量  可以不用Queue实现
   private AtomicInteger phoneNumbers=new AtomicInteger(0);
   private RateLimiter rateLimiter=RateLimiter.create(20d);//一秒只能执行五次
   //默认销售500台
   private final static int DEFALUT_LIMIT=500;
   private final int saleLimit;

    public TokenBuck(int saleLimit) {
        this.saleLimit = saleLimit;
    }

    public TokenBuck() {
        this(DEFALUT_LIMIT);
    }

    public int buy(){
        //这个check 必须放在success里面做判断,不然会产生线程安全问题(业务引起)
        //原因当phoneNumbers=99 时 同时存在三个线程进来。虽然phoneNumbers原子性,但是也会发生。如果必须写在这里,在success
        //里面也需要加上double check
       /* if (phoneNumbers.get()>=saleLimit){
            throw new IllegalStateException("Phone has been sale "+saleLimit+" can not  buy more...")
        }*/

        //目前设置超时时间,10秒内没有抢到就抛出异常
        //这里的TimeOut*Ratelimiter=总数  这里的超时就是让别人抢几秒,所以设置总数也可以由这里的超时和RateLimiter来计算
         boolean success = rateLimiter.tryAcquire(10, TimeUnit.SECONDS);
         if (success){
             if (phoneNumbers.get()>=saleLimit){
                 throw new IllegalStateException("Phone has been sale "+saleLimit+" can not  buy more...");
             }
             int phoneNo = phoneNumbers.getAndIncrement();
             System.out.println(currentThread()+" user has get :["+phoneNo+"]");
             return phoneNo;
         }else {
             //超时后 同一时间,很大的流量来强时,超时快速失败。
             throw new RuntimeException(currentThread()+"has timeOut can try again...");
         }

    }
}
3.1 令牌桶算法的测试类
package concurrent.TokenBucket;

import java.util.stream.IntStream;

/**
 * ${DESCRIPTION}
 *
 * @author mengxp
 * @version 1.0
 * @create 2018-01-21 0:40
 **/
public class TokenBuckTest {
    public static void main(String[] args) {
        final TokenBuck tokenBuck=new TokenBuck(200);


        IntStream.range(0,300).forEach(i->{
            //目前测试时,让一个线程抢一次,不用循环抢
            //tokenBuck::buy 这种方式 产生一个Runnable
            new Thread(tokenBuck::buy).start();
        });
    }
}








<think>嗯,用户问令牌桶算法是不是基于信号量(semaphore)实现的。我需要先回顾一下这两个概念,确保自己理解正确。令牌桶算法是一种用于流量控制和速率限制的常用算法,它通过定期向桶中添加令牌,请求需要获取令牌才能被处理,以此来控制速率。而信号量是一种同步机制,用于控制对共享资源的访问,通常用来限制同时访问某个资源的线程数。 首先,我需要确认令牌桶算法的基本实现原理。令牌桶的核心是有一个固定容量的桶,系统以固定的速率向桶中添加令牌。当请求到达时,如果桶中有足够的令牌,请求被处理,令牌被消耗;否则,请求被限流。这听起来像是一个生产者-消费者模型,生产者定期生成令牌,消费者(请求)消耗令牌。 接下来,考虑信号量的作用。信号量维护一个计数器,当线程请求资源时,如果计数器大于零,则允许访问并减少计数器;否则线程可能被阻塞或返回错误。这和令牌桶中令牌的获取确实有相似之处,都是基于可用资源的计数来控制访问。 那么,令牌桶是否基于信号量实现呢?可能的实现方式之一是使用信号量来管理令牌的数量。例如,信号量的计数器可以代表当前可用的令牌数量。每次添加令牌时,释放信号量(增加计数器),而获取令牌时则尝试获取信号量(减少计数器)。这种情况下,信号量的确可以作为令牌桶的一个底层机制。 不过,令牌桶不仅仅是简单的计数器,它还需要处理令牌的定期添加。这可能需要一个独立的线程或定时器来周期性地向信号量中添加许可(即令牌)。同时,需要考虑桶的容量限制,当令牌已满时,即使定时添加,也不能超过容量。这时候信号量本身的最大值可以设置为桶的容量,从而自然限制令牌的数量。 但这里可能存在一些差异。例如,传统的信号量在许可被释放时如果超过最大值,可能会被忽略或导致错误,而令牌桶在桶满时应该停止添加令牌,而不是增加信号量的许可。因此,在实现时可能需要额外的逻辑来确保令牌添加不会超过桶的容量。这可能意味着在信号量的基础上包装一些逻辑,比如在释放信号量之前检查当前许可数量是否已达到最大值。 另外,信号量通常用于多线程同步,而令牌桶可能还需要考虑时间因素,即令牌的定期生成。这可能需要结合定时任务或延迟队列来触发令牌的添加。例如,使用一个定时器每隔一定时间调用信号量的release()方法来增加许可,但需要确保每次添加不超过桶的剩余容量。 不过,令牌桶算法的另一种实现方式可能不使用信号量,而是直接维护一个计数器和上一次生成令牌的时间戳。每次请求到来时,计算自上次生成令牌以来应该新增的令牌数量,然后更新计数器和时间戳,再判断是否有足够的令牌可用。这种实现方式可能更高效,因为它不需要依赖操作系统的信号量机制,尤其是在高并发环境下。 用户的问题可能源于对两者机制的理解,认为它们都是基于资源计数来控制访问,所以想知道是否有直接的实现关系。我需要明确回答是否令牌桶算法通常基于信号量实现,或者它们只是概念上相似但实现方式不同。 根据我的知识,虽然信号量可以用于实现令牌桶的一部分功能,但完整的令牌桶算法通常需要结合其他机制,如定时器或时间戳计算,来管理令牌的生成和消耗。因此,令牌桶算法并不完全基于信号量实现,但信号量可以作为其中的一个组件来管理可用令牌的数量。 还需要考虑不同的编程语言或库中的具体实现。例如,在Java中,可能有使用Semaphore类来实现令牌桶的案例,但同样需要处理定时添加令牌的逻辑。而在其他实现中,可能完全用自定义的计数器和时间管理来避免信号量的开销。 总结来说,令牌桶算法和信号量在概念上有相似之处,都涉及资源计数和访问控制,但令牌桶的实现通常需要额外的逻辑来处理令牌的定期生成和容量限制,因此并不完全基于信号量。不过,信号量可以作为实现令牌桶的一个有用工具,尤其是在多线程环境中管理并发的令牌获取。</think>令牌桶算法与信号量(Semaphore)在概念上有相似之处,但**令牌桶并非完全基于信号量实现**。以下是具体分析: --- ### **1. 核心机制对比** - **令牌桶算法** - **功能**:控制请求速率(如每秒处理 N 个请求)。 - **原理**: 1. 一个固定容量的“桶”定期生成令牌(如每秒添加固定数量)。 2. 请求需获取令牌才能执行,无令牌时触发限流。 - **实现关键**:需同时管理**令牌生成速率**和**桶容量限制**,通常结合时间计算(如记录上次生成令牌的时间戳)和原子计数器。 - **信号量(Semaphore)** - **功能**:控制并发资源访问(如最多允许 M 个线程同时运行)。 - **原理**:维护一个计数器,通过 `acquire()` 和 `release()` 操作控制资源占用。 - **局限性**:仅管理可用资源数量,**不直接支持时间维度的速率控制**(如每秒新增资源)。 --- ### **2. 令牌桶能否用信号量实现?** - **可以部分依赖信号量**,但需补充额外逻辑: 1. **令牌生成**: 需要独立线程或定时任务定期调用信号量的 `release()`(添加令牌),但需确保不超过桶容量。 2. **容量限制**: 信号量的初始值设为桶容量,但需拦截超额 `release()`(如桶满时停止添加令牌)。 3. **速率控制**: 通过固定间隔触发 `release()` 实现令牌生成速率(例如每秒一次)。 - **示例伪代码(Java)**: ```java Semaphore tokens = new Semaphore(MAX_BUCKET_SIZE); // 桶容量 // 定时任务:每秒添加 RATE 个令牌 scheduler.scheduleAtFixedRate(() -> { int available = tokens.availablePermits(); if (available < MAX_BUCKET_SIZE) { tokens.release(RATE); // 需确保不超过最大值 } }, 1, 1, TimeUnit.SECONDS); // 请求处理前获取令牌 if (tokens.tryAcquire()) { handleRequest(); } else { rejectRequest(); } ``` --- ### **3. 实际实现中的差异** - **性能优化**: 直接基于时间戳和计数器的实现(无信号量)更高效,避免了线程调度和锁竞争。例如: ```python class TokenBucket: def __init__(self, rate, capacity): self.rate = rate # 令牌生成速率(个/秒) self.capacity = capacity # 桶容量 self.tokens = capacity # 当前令牌数 self.last_time = time.time() def consume(self): now = time.time() elapsed = now - self.last_time self.tokens = min(self.capacity, self.tokens + elapsed * self.rate) self.last_time = now if self.tokens >= 1: self.tokens -= 1 return True return False ``` - **适用场景**: - **信号量实现**:适合简单限流,且对精度要求不高的场景。 - **时间戳+计数器实现**:适合高并发、需精确控制速率的场景(如 API 网关)。 --- ### **4. 总结** - **令牌桶与信号量的关系**: 令牌桶可以**借用信号量管理令牌数量**,但完整的速率控制需结合时间维度的令牌生成逻辑。 - **核心差异**: 信号量缺乏对“速率”的原生支持(如每秒新增资源),而令牌桶通过定期补充令牌实现了这一点。 - **实践建议**: 在需要严格速率限制时,优先选择专门的令牌桶实现(如 Guava 的 `RateLimiter` 或 Redis 的 `INCR` + 过期时间方案)。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值