限流器设计思路(浅入门)

· 浏览次数 : 0

小编点评

Java限流器算法总结 限流器是控制系统资源利用率和质量的重要机制。在Java中,有多种实现限流器的方式,包括: 1. 令牌桶算法(Token Bucket) 2. 漏桶算法(Leaky Bucket) 3. 滑动窗口算法(Sliding Window) 本文对这些算法进行了简单介绍与对比。 1. 令牌桶算法 令牌桶算法维护一个固定大小的令牌桶和记录上次添加令牌的时间戳。以固定速率向桶中添加令牌。每当请求到来时,需要从桶中获取一个令牌,若桶中有足够的令牌,则请求被处理;否则,请求将被拒绝或阻塞。 优点:相对公平且能平滑控制速率。 缺点:令牌以恒定速率添加可能导致在高并发下出现拥塞。 2. 漏桶算法 漏桶算法与令牌桶类似,但它维护一个固定大小的请求队列。所有到达的请求会被放入队列中。队列以固定速率“漏水”,即在固定时间内能处理的请求数量。当队列满了,新请求将被拒绝或阻塞。 优点:能更好地处理突发流量。 缺点:缺乏公平性,可能导致某些低优先级的请求长时间得不到处理。 3. 滑动窗口算法 滑动窗口算法维护一个固定大小的窗口,检查窗口内的请求数是否达到限制。若未达到上限,则允许请求;否则,请求将被拒绝或阻塞。随着时间的推移,滑动窗口会移除较早的请求记录,从而保持窗口大小。 优点:能够更精确地控制单位时间内的请求数量。 缺点:需维护一个递增的队列,可能存在较高的内存消耗。 结论 以上三种算法各有千秋,适用于不同的场景。具体选择哪一种,需结合实际应用需求进行权衡。同时,也可考虑利用现有的限流库或框架简化开发工作,例如使用Guava RateLimiter或Netflix Hystrix。

正文


限流器(Rate Limiter)是一种用于控制系统资源利用率和质量的重要机制。它通过限制单位时间内可以执行的操作数量,从而防止系统过载和保护服务的可靠性。在Java中,可以使用多种方式来实现限流器,下面是几个常见思路:

  • 令牌桶算法
  • 漏桶算法
  • 划窗算法
  • 固定窗口算法
  • 基于计数器的流量控制算法
  • ...

令牌桶算法(Token Bucket)

令牌桶算法是一种常见的限流实现方式。它维护一个存放令牌的桶,以固定的速率向桶中添加令牌。每次请求到来时,需要从桶中获取一个令牌,只有当桶中有足够的令牌时,请求才能被处理。否则,请求将被拒绝或阻塞。

image

实现思路如下:

  • 维护一个固定大小的令牌桶和一个记录上一次令牌被添加到桶中的时间戳。
  • 以固定的速率(每秒生成的令牌数)向桶中添加令牌。
  • 当请求到来时,尝试从桶中获取一个令牌。如果桶中有令牌,则处理请求并消耗一个令牌;否则,拒绝或阻塞请求。
  • 使用锁或其他同步机制来保证线程安全。

code:

package RateLimiter;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

// 令牌桶算法 (TokenBucketRateLimiter)
public class TokenBucketRateLimiter {
    /**
     * REFILL_PERIOD 表示令牌桶的refill周期,即每隔多长时间(秒)向桶中添加令牌。
     * MAX_TOKENS 表示令牌桶的最大容量,即桶中最多可以存放多少个令牌。
     * REFILL_TOKENS 表示每个refill周期向桶中添加的令牌数量。
     * lastRefillTimestamp 记录上一次refill的时间戳。
     * tokenBucket 是一个Semaphore实例,用于模拟令牌桶的行为。
     */
    private static final long REFILL_PERIOD = 1; // 1秒
    private static final long MAX_TOKENS = 5; // 桶容量
    private static final long REFILL_TOKENS = 2; // 每次添加令牌数
    /**
     * AtomicLong是Java中用于表示一个原子性的长整型值的类。它提供了一些原子操作方法,用于在多线程环境下安全地更新和访问长整型值。
     *  - 在这些限流器实现中,AtomicLong主要用于记录上一次令牌/请求刷新的时间戳。
     *  - 由于多个线程可能同时尝试获取令牌或请求,因此需要确保对时间戳的读写操作是原子性的,以避免竞态条件。
     */
    private AtomicLong lastRefillTimestamp = new AtomicLong(System.nanoTime());
    /**
     * Semaphore(信号量)是Java中一个并发控制工具,用于控制对共享资源的访问。
     *      - 它基于计数器的原理,可以限制同时访问某个资源的线程数量。用于模拟令牌桶的行为。
     *      - Semaphore使用acquire()和release()方法来获取和释放信号量:
     */
    private Semaphore tokenBucket = new Semaphore((int) MAX_TOKENS);

    /**
     * tryAcquire() 方法是获取令牌的入口:
     *
     * 1-获取当前时间戳 now。
     * 2-根据当前时间戳和上一次refill时间戳,计算出这段时间内应该添加多少个令牌 newTokens。
     * 3-更新上一次refill时间戳为当前时间戳。
     * 4-将新的令牌数量 newTokens 释放到 tokenBucket 中。
     * 5-尝试从 tokenBucket 中获取一个令牌,如果成功则返回 true,否则返回 false。
     */
    public boolean tryAcquire() {
        long now = System.nanoTime();
        long lastRefillTime = lastRefillTimestamp.get();
        long newTokens = calculateNewTokens(lastRefillTime, now);
        lastRefillTimestamp.set(now);

        tokenBucket.release((int) newTokens);
        return tokenBucket.tryAcquire();
    }

    /**
     * calculateNewTokens() 方法根据时间差计算出应该添加的令牌数量,但不会超过桶的最大容量。
     */
    private long calculateNewTokens(long lastRefillTime, long now) {
        long nanosElapsed = now - lastRefillTime;
        long refillPeriodCount = nanosElapsed / TimeUnit.SECONDS.toNanos(REFILL_PERIOD);
        return Math.min(refillPeriodCount * REFILL_TOKENS, MAX_TOKENS);
    }
}

漏桶算法(Leaky Bucket)

漏桶算法类似于令牌桶算法,不同之处在于它维护一个存放请求的队列,而不是令牌桶。当请求到来时,它们会被添加到队列中。队列以固定的速率漏水,即以固定的速率处理请求。

image

实现思路如下:

  • 维护一个固定大小的请求队列和一个上次处理请求的时间戳。
  • 当请求到来时,将其添加到队列中。如果队列已满,则拒绝或阻塞请求。
  • 以固定的速率(每秒处理的请求数)从队列中取出请求并处理。
  • 使用锁或其他同步机制来保证线程安全。

code:

package RateLimiter;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

// 漏桶算法 (LeakyBucketRateLimiter)
public class LeakyBucketRateLimiter {
    /**
     * REFILL_PERIOD 表示漏桶的refill周期,即每隔多长时间(秒)处理请求。
     * MAX_REQUESTS 表示漏桶的最大容量,即桶中最多可以存放多少个请求。
     * REFILL_REQUESTS 表示每个refill周期处理的请求数量。
     * requestQueue 是一个队列,用于存放待处理的请求。
     * lastRefillTimestamp 记录上一次refill的时间戳。
     */
    private static final long REFILL_PERIOD = 1; // 1秒
    private static final long MAX_REQUESTS = 5; // 桶容量
    private static final long REFILL_REQUESTS = 2; // 每次处理请求数

    private Queue<Long> requestQueue = new LinkedList<>();
    private AtomicLong lastRefillTimestamp = new AtomicLong(System.nanoTime());

    /**
     * tryAcquire() 方法是获取请求的入口:
     *  - 获取当前时间戳 now。
     *  - 根据当前时间戳和上一次refill时间戳,计算出这段时间内应该处理多少个请求 newRequests。
     *  - 更新上一次refill时间戳为当前时间戳。
     *  - 将新的请求数量 newRequests 添加到 requestQueue 中,如果队列已满则移除最早的请求。
     *  - 如果队列大小不超过最大容量 MAX_REQUESTS,则返回 true,否则返回 false。
     */
    public boolean tryAcquire() {
        long now = System.nanoTime();
        long lastRefillTime = lastRefillTimestamp.get();
        long newRequests = calculateNewRequests(lastRefillTime, now);
        lastRefillTimestamp.set(now);

        for (long i = 0; i < newRequests; i++) {
            if (requestQueue.size() >= MAX_REQUESTS) {
                requestQueue.poll();
            }
            requestQueue.offer(now);
        }

        return requestQueue.size() <= MAX_REQUESTS;
    }

    /**
     * calculateNewRequests() 方法根据时间差计算出应该处理的请求数量。
     */
    private long calculateNewRequests(long lastRefillTime, long now) {
        long nanosElapsed = now - lastRefillTime;
        long refillPeriodCount = nanosElapsed / TimeUnit.SECONDS.toNanos(REFILL_PERIOD);
        return refillPeriodCount * REFILL_REQUESTS;
    }
}

滑动窗口(Sliding Window)

滑动窗口算法通过维护一个固定大小的窗口来限制单位时间内的请求数。当请求到来时,它会检查窗口内的请求数是否已达到限制。如果没有,则允许请求;否则,拒绝或阻塞请求。窗口会随着时间推移而滑动,移除较早的请求记录。

冷知识:
TCP协议中数据包的传输,同样也是采用滑动窗口来进行流量控制。

实现思路如下:

  • 维护一个队列或其他数据结构来存储请求的时间戳。
  • 当请求到来时,将其时间戳添加到队列中。
  • 检查队列中最近的一段时间内(窗口大小)的请求数是否超过限制。如果没有,则允许请求;否则,拒绝或阻塞请求。
  • 定期(或在每次请求到来时)移除队列中较早的请求记录,以维护窗口大小。
  • 使用锁或其他同步机制来保证线程安全。

Reference:
https://blog.csdn.net/legend050709/article/details/114917637

原理:
需要先看看固定窗口算法的原理和缺点,
image

动图:
image

image

image

code:

package RateLimiter;
import java.util.LinkedList;
import java.util.Queue;

/**
 * 滑动窗口算法 (SlidingWindowRateLimiter)
 */
public class SlidingWindowRateLimiter {
    /**
     * WINDOW_SIZE 表示滑动窗口的大小(秒)。
     * MAX_REQUESTS 表示窗口内允许的最大请求数量。
     * requestTimestamps 是一个队列,用于存放请求的时间戳。
     */
    private static final long WINDOW_SIZE = 5; // 窗口大小(秒)
    private static final long MAX_REQUESTS = 10; // 最大请求数
    private Queue<Long> requestTimestamps = new LinkedList<>();

    /**
     * tryAcquire() 方法是获取请求的入口:
     *  - 获取当前时间戳 now。
     *  - 将当前时间戳添加到 requestTimestamps 队列中。
     *  - 计算窗口的开始时间 windowStartTime。
     *  - 移除队列中早于 windowStartTime 的时间戳,即移除窗口之外的请求记录。
     *  - 如果队列大小不超过 MAX_REQUESTS,则返回 true,否则返回 false。
     */
    public boolean tryAcquire() {
        long now = System.currentTimeMillis();
        requestTimestamps.add(now);

        long windowStartTime = now - WINDOW_SIZE * 1000;
        while (!requestTimestamps.isEmpty() && requestTimestamps.peek() < windowStartTime) {
            requestTimestamps.poll();
        }

        return requestTimestamps.size() <= MAX_REQUESTS;
    }
}

演示code:

package RateLimiter;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class RateLimiterDemo {
    public static void main(String[] args) {
        // 创建限流器实例
        TokenBucketRateLimiter tokenBucketRateLimiter = new TokenBucketRateLimiter();
        LeakyBucketRateLimiter leakyBucketRateLimiter = new LeakyBucketRateLimiter();
        SlidingWindowRateLimiter slidingWindowRateLimiter = new SlidingWindowRateLimiter();

        // 创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        // 提交任务
        for (int i = 0; i < 20; i++) {
            executorService.submit(() -> {
                boolean tokenBucketAllowed = tokenBucketRateLimiter.tryAcquire();
                boolean leakyBucketAllowed = leakyBucketRateLimiter.tryAcquire();
                boolean slidingWindowAllowed = slidingWindowRateLimiter.tryAcquire();

                System.out.println("Token Bucket: " + tokenBucketAllowed +
                        ", Leaky Bucket: " + leakyBucketAllowed +
                        ", Sliding Window: " + slidingWindowAllowed);

                try {
                    TimeUnit.MILLISECONDS.sleep(500); // 模拟处理请求
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 关闭线程池
        executorService.shutdown();
    }
}

out:

Token Bucket: true, Leaky Bucket: true, Sliding Window: true
Token Bucket: false, Leaky Bucket: true, Sliding Window: true
Token Bucket: false, Leaky Bucket: true, Sliding Window: true
Token Bucket: false, Leaky Bucket: true, Sliding Window: true
Token Bucket: true, Leaky Bucket: true, Sliding Window: true
Token Bucket: false, Leaky Bucket: true, Sliding Window: true
Token Bucket: false, Leaky Bucket: true, Sliding Window: true
Token Bucket: true, Leaky Bucket: true, Sliding Window: true
Token Bucket: true, Leaky Bucket: true, Sliding Window: true
Token Bucket: true, Leaky Bucket: true, Sliding Window: true
Token Bucket: false, Leaky Bucket: true, Sliding Window: false
Token Bucket: false, Leaky Bucket: true, Sliding Window: false
Token Bucket: false, Leaky Bucket: true, Sliding Window: false
Token Bucket: false, Leaky Bucket: true, Sliding Window: false
Token Bucket: false, Leaky Bucket: true, Sliding Window: false
Token Bucket: false, Leaky Bucket: true, Sliding Window: false
Token Bucket: false, Leaky Bucket: true, Sliding Window: false
Token Bucket: false, Leaky Bucket: true, Sliding Window: false
Token Bucket: false, Leaky Bucket: true, Sliding Window: false
Token Bucket: false, Leaky Bucket: true, Sliding Window: false

总结

这三种算法都是通过控制请求的速率或数量来实现限流,但具体的实现方式有所不同。令牌桶算法和漏桶算法都依赖于时间来控制速率,而滑动窗口算法则是基于请求数量来控制。它们各有优缺点,适合不同的场景。具体选择哪种需要自己根据应用场景进行选择和调整。

同时,也可以考虑使用现有的限流器库或框架,如Guava RateLimiter、Netflix Hystrix等,以简化开发过程。

与限流器设计思路(浅入门)相似的内容:

限流器设计思路(浅入门)

目录令牌桶算法(Token Bucket)漏桶算法(Leaky Bucket)滑动窗口(Sliding Window)总结 限流器(Rate Limiter)是一种用于控制系统资源利用率和质量的重要机制。它通过限制单位时间内可以执行的操作数量,从而防止系统过载和保护服务的可靠性。在Java中,可以使

网关限流功能性能优化

本文主要从设计与原理方面分享优化过程中的思考,不涉及具体的代码实现。在分析过程中我会写一些当时思考的问题,在看后续答案时可以自己也先思考一下 老的限流方案 首先讲解一下原本网关限流功能的实现方案,省略其中的白名单,黑名单,令牌桶算法实现等一些细节 限流策略中包含多种策略,比如根据用户维度限流,ip维

接口防刷!利用redisson快速实现自定义限流注解

问题: 在日常开发中,一些重要的对外接口,需要加上访问频率限制,以免造成资��损失。 如登录接口,当用户使用手机号+验证码登录时,一般我们会生成6位数的随机验证码,并将验证码有效期设置为1-3分钟,如果对登录接口不加以限制,理论上,通过技术手段,快速重试100000次,即可将验证码穷举出来。 解决思

可插拔组件设计机制—SPI

SPI 的全称是Service Provider Interface,即提供服务接口;是一种服务发现机制,SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。本篇文章聚焦SPI的使用场景及使用介绍。

深入理解java和dubbo的SPI机制

1 SPI简介 1.1 SPI(Service Provider Interface) 本质:将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。 java SPI:用来设计给服务提供商做插件使用的。基于策略模式来实现动态加载的机制。我

从0到1构造自定义限流组件

在系统高可用设计中,接口限流是一个非常重要环节,一方面是出于对自身服务器资源的保护,另一方面也是对依赖资源的一种保护措施。比如对于 Web 应用,我限制单机只能处理每秒 1000 次的请求,超过的部分直接返回错误给客户端。虽然这种做法损害了用户的使用体验,但是它是在极端并发下的无奈之举,是短暂的行为,因此是可以接受的。

算法~利用zset实现滑动窗口限流

滑动窗口限流 滑动窗口限流是一种常用的限流算法,通过维护一个固定大小的窗口,在单位时间内允许通过的请求次数不超过设定的阈值。具体来说,滑动窗口限流算法通常包括以下几个步骤: 初始化:设置窗口大小、请求次数阈值和时间间隔。 维护窗口:将请求按照时间顺序放入窗口中,并保持窗口内请求数量不超过阈值。 检查

理论+实践,教你如何使用Nginx实现限流

摘要:Nginx作为一款高性能的Web代理和负载均衡服务器,往往会部署在一些互联网应用比较前置的位置。此时,我们就可以在Nginx上进行设置,对访问的IP地址和并发数进行相应的限制。 本文分享自华为云社区《【高并发】使用Nginx实现限流》,作者:冰 河。 Nginx作为一款高性能的Web代理和负载

记一次由于操作失误致使数据库瘫痪的故障分析与解决方案

在这篇文章中,我将分享一次由于操作不当导致数据库瘫痪的经验。通过回顾故障发生的时间、系统简介、时间线、问题分析和经验总结等方面的内容。讨论操作时间不当、操作流程不当、缺乏执行计划和限流机制等问题,并提出一些建议,如确认数据库更新时间、优化更新操作、使用限流工具、设置超时时间和重试机制、调整数据库参数以及定期维护和优化数据库。通过分享这次经验,我希望能帮助他人避免类似的错误,并提高数据库操作的准确性和稳定性。

Redisson 限流器源码分析

Redisson 限流器源码分析 对上篇文章网友评论给出问题进行解答:redis 的key 是否会过期,过期指的限流器 可以先阅读上篇文章: redis + AOP + 自定义注解实现接口限流 - 古渡蓝按 - 博客园 (cnblogs.com) 注解AOP 代码部分提取 // 调用Reids工具类