JAVA学习-限量算法

一、限流基础知识介绍

为啥要限流,相信就不用我多说了。

  • 比如,我周末去饭店吃饭,但是人太多了,我只能去前台拿个号,等号码到我的时候才能进饭店吃饭。如果饭店没有限流怎么办?一到饭点,人都往里冲,而饭店又处理不了这么多人流,很容易就出事故(饭店塞满了人,无路可走。饭店的工作人员崩溃了,处理不过来)

  • 回到代码世界上也是一样的,服务器能处理的请求数有限,如果请求量特别大,我们需要做限流(要么就让请求等待,要么就把请求给扔了)

图片

限流

在代码世界上,限流有两种比较常见的算法:

  • 令牌桶算法

  • 漏桶算法

1.1 什么是漏桶算法

比如,现在我有一个桶子,绿色那块是我能装水的容量,如果超过我能装下的容量,再往桶子里边倒水,就会溢出来(限流):

图片

桶子

我们目前可以知道的是:

  • 桶子的容量是固定的(是图上绿色那块)

  • 超出了桶子的容量就会溢出(要么等待,要么直接丢弃)

OK,现在我们在桶子里挖个洞,让水可以从洞子里边流出来:

图片

挖了个洞,水从洞口流出来

桶子的洞口的大小是固定的,所以水从洞口流出来的速率也是固定的

所以总结下来算法所需的参数就两个:

  • 桶子的容量

  • 漏水的速率

漏桶算法有两种实现:

  1. 不允许突发流量的情况:如果进水的速率大于出水的速率,直接舍弃掉多余的水。比如,我的桶子容量能装100L,但我的桶子出水速率是10L/s。此时,如果现在有100L/s的水进来,我只让10L的水进到桶子,其余的都限流。(限定了请求的速度

  2. 允许一定的突发流量情况:我的桶子能装100L,如果现在我的桶子是空的,那么这100L的水都能瞬间进我的桶子。我以10L/s的速率将这些水流出,如果还有100L的水进来,只能限流了。

经过上面的分析我们就知道:

漏桶算法可以平滑网络上的突发流量(因为漏水的速率是固定的)

1.2 什么是令牌桶算法

现在我有另外一个桶子,这个桶子不用来装水,用来装令牌:

图片

桶子装着令牌

令牌会一定的速率扔进桶子里边,比如我1秒扔10个令牌进桶子:

图片

以一定的速率扔令牌进桶子

桶子能装令牌的个数有上限的,比如我的桶子最多只能装1000个令牌。

每个请求进来,就会去桶子拿一个令牌

  • 比如这秒我有1001个请求,我就去桶子里边拿1001个令牌,此时可能会出现两种情况:

    • 桶子里边没有1001个令牌,只有1000个,那没拿到令牌的请求只能被阻塞了(等待)

    • 桶子里边有1001个令牌,所有请求都可以执行。

图片

令牌桶算法支持网络上的突发流量

漏桶和令牌桶的区别:从上面的例子估计大家也能看出来了,漏桶只能以固定的速率去处理请求,而令牌桶可以以桶子最大的令牌数去处理请求

二、RateLimiter使用

RateLimiter是Guava的一个限流组件,我这边的系统就有用到这个限流组件,使用起来十分方便。

引入pom依赖:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>20.0</version>
</dependency>

RateLimiter它是基于令牌桶算法的,API非常简单,看以下的Demo:

public static void main(String[] args) {
        //线程池
        ExecutorService exec = Executors.newCachedThreadPool();
        //速率是每秒只有3个许可
        final RateLimiter rateLimiter = RateLimiter.create(3.0);

        for (int i = 0; i < 100; i++) {
            final int no = i;
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    try {
                        //获取许可
                        rateLimiter.acquire();
                        System.out.println("Accessing: " + no + ",time:"
                                + new SimpleDateFormat("yy-MM-dd HH:mm:ss").format(new Date()));

                    } catch (Exception e) {
                        e.printStackTrace();
                    }

                }
            };
            //执行线程
            exec.execute(runnable);
        }
        //退出线程池
        exec.shutdown();
    }

我们可以从结果看出,每秒只能执行三个:

图片

每秒执行三个

三、分布式限流

RateLimiter是一个单机的限流组件,如果是分布式应用的话,该怎么做?

可以使用Redis+Lua的方式来实现,大致的lua脚本代码如下:

local key = "rate.limit:" .. KEYS[1] --限流KEY
local limit = tonumber(ARGV[1])        --限流大小
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then --如果超出限流大小
  return 0
else  --请求数+1,并设置1秒过期
  redis.call("INCRBY", key,"1")
   redis.call("expire", key,"1")
   return current + 1
end

Java代码如下:

public static boolean accquire() throws IOException, URISyntaxException {
    Jedis jedis = new Jedis("127.0.0.1");
    File luaFile = new File(RedisLimitRateWithLUA.class.getResource("/").toURI().getPath() + "limit.lua");
    String luaScript = FileUtils.readFileToString(luaFile);

    String key = "ip:" + System.currentTimeMillis()/1000; // 当前秒
    String limit = "5"; // 最大限制
    List<String> keys = new ArrayList<String>();
    keys.add(key);
    List<String> args = new ArrayList<String>();
    args.add(limit);
    Long result = (Long)(jedis.eval(luaScript, keys, args)); // 执行lua脚本,传入参数
    return result == 1;
}

解释:

  • Java代码传入key和最大的限制limit参数进lua脚本

  • 执行lua脚本(lua脚本判断当前key是否超过了最大限制limit)

    • 如果超过,则返回0(限流)

    • 如果没超过,返回1(程序继续执行)

<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` + 过期时间方案)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值