0

0

基于Redis和Bucket4j的Java分布式限流器:实现滚动窗口与回退机制

DDD

DDD

发布时间:2025-07-11 21:34:19

|

548人浏览过

|

来源于php中文网

原创

基于redis和bucket4j的java分布式限流器:实现滚动窗口与回退机制

本文深入探讨如何在Java中利用Redis和Bucket4j库构建一个功能强大的分布式限流器。我们将详细介绍如何实现类似JavaScript redis-token-bucket-ratelimiter 的滚动窗口限流策略,并重点讲解如何获取并利用请求被拒绝时的回退(Retry-After)时间,以优化客户端行为。通过具体代码示例,帮助读者理解并应用这些高级限流特性。

1. 理解分布式限流与挑战

在构建高并发、高可用性的现代应用时,限流(Rate Limiting)是不可或缺的一环。它能够保护后端服务免受过载,防止恶意攻击,并确保系统资源的公平分配。对于分布式系统而言,限流的挑战在于如何跨多个实例同步和管理限流状态。Redis因其高性能和原子操作特性,成为实现分布式限流的理想选择。

常见的限流算法包括漏桶(Leaky Bucket)和令牌桶(Token Bucket)。令牌桶算法因其允许一定程度的突发流量而更受青睐。本文将围绕令牌桶算法,并结合“滚动窗口”的概念来构建限流器。

核心需求点:

  • 滚动窗口(Rolling Window):限制在过去某个时间窗口内的请求数量,而非固定时间段。虽然令牌桶本身是基于速率和容量的,但通过巧妙配置,可以模拟出滚动窗口的行为。
  • 回退机制(Back-off / Retry-After):当请求因限流而被拒绝时,能够返回一个建议的等待时间,指导客户端何时可以重试,从而避免客户端无意义的重试风暴。

2. 为什么选择Bucket4j?

在Java生态中,有多种限流库和实现方式。例如,一些基于Redis的Lua脚本实现,或像Redisson这样的综合性框架。然而,对于本文所关注的“滚动窗口”和“回退时间”这两个核心需求,Bucket4j库提供了非常强大和灵活的支持。

立即学习Java免费学习笔记(深入)”;

Bucket4j是一个Java的令牌桶限流库,它支持多种持久化方式,包括内存、JCache、Redis等。其强大的API能够提供详细的限流结果,包括剩余令牌数和下次可重试的时间。

初学者可能会误解Bucket4j不提供回退时间,但实际上,它通过 ConsumptionProbe 对象提供了非常详细的诊断信息,其中包括 getNanosToWaitForRefill() 方法,这正是我们所需的回退时间。

AIBox 一站式AI创作平台
AIBox 一站式AI创作平台

AIBox365一站式AI创作平台,支持ChatGPT、GPT4、Claue3、Gemini、Midjourney等国内外大模型

下载

3. Bucket4j实现滚动窗口限流

虽然Bucket4j是令牌桶算法的实现,但其“令牌填充速率”和“桶容量”的组合,可以有效地模拟出对“滚动窗口”的限制效果。例如,一个每秒填充10个令牌,容量为100个令牌的桶,意味着在过去一段时间内,平均每秒最多处理10个请求,且在短时间内最多允许100个请求的突发。

以下是一个基于Redis和Bucket4j实现分布式限流的示例。我们将使用Jedis作为Redis客户端的集成。

3.1 引入依赖

首先,在pom.xml中添加Bucket4j及其Jedis集成依赖:

<dependencies>
    <!-- Bucket4j Core -->
    <dependency>
        <groupId>com.github.vladimir-bukhtoyarov</groupId>
        <artifactId>bucket4j-core</artifactId>
        <version>8.1.1</version> <!-- 请使用最新稳定版本 -->
    </dependency>
    <!-- Bucket4j Jedis integration -->
    <dependency>
        <groupId>com.github.vladimir-bukhtoyarov</groupId>
        <artifactId>bucket4j-redis</artifactId>
        <version>8.1.1</version> <!-- 确保与bucket4j-core版本一致 -->
    </dependency>
    <!-- Jedis client -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>4.3.1</version> <!-- 请使用最新稳定版本 -->
    </dependency>
</dependencies>

3.2 配置和初始化限流器

接下来,我们将配置一个基于Redis的限流器。这里我们创建一个每秒允许10个请求,最大突发容量为20个请求的限流器。

import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Bucket4j;
import io.github.bucket4j.ConsumptionProbe;
import io.github.bucket4j.redis.jedis.JedisBasedProxyManager;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

public class DistributedRateLimiter {

    private static JedisPool jedisPool;
    private static JedisBasedProxyManager proxyManager;

    public static void initRedis(String redisHost, int redisPort) {
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(10); // 最大连接数
        poolConfig.setMaxIdle(5);   // 最大空闲连接数
        jedisPool = new JedisPool(poolConfig, redisHost, redisPort);
        proxyManager = new JedisBasedProxyManager(jedisPool);
    }

    /**
     * 获取或创建限流桶。
     * 每个唯一的key对应一个独立的限流桶。
     *
     * @param key 限流的唯一标识符(例如:用户ID, IP地址, API路径)
     * @param capacity 令牌桶容量
     * @param refillTokens 每次填充的令牌数
     * @param refillPeriod 令牌填充周期
     * @return 配置好的限流桶
     */
    public static Bucket getOrCreateLimiterBucket(String key, long capacity, long refillTokens, Duration refillPeriod) {
        // 定义限流带宽
        Bandwidth limit = Bandwidth.builder()
                .capacity(capacity) // 令牌桶容量
                .refillGreedy(refillTokens, refillPeriod) // 令牌填充策略:每 refillPeriod 填充 refillTokens 个令牌
                .build();

        // 使用proxyManager获取或创建分布式桶
        // key是限流器的唯一标识,Supplier是当key不存在时如何创建桶的定义
        return Bucket4j.builder()
                .addLimit(limit)
                .build(proxyManager, key);
    }

    public static void main(String[] args) throws InterruptedException {
        // 初始化Redis连接池
        initRedis("localhost", 6379); // 假设Redis运行在本地默认端口

        String userId = "user:123";
        // 为该用户设置限流:每秒10个请求,最大突发20个请求
        Bucket userBucket = getOrCreateLimiterBucket(userId, 20, 10, Duration.ofSeconds(1));

        System.out.println("--- 模拟用户请求 ---");
        for (int i = 0; i < 30; i++) {
            // 尝试消费一个令牌
            ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1);

            if (probe.isConsumed()) {
                // 请求成功
                System.out.printf("请求 %d 成功!剩余令牌:%d%n", i + 1, probe.getRemainingTokens());
            } else {
                // 请求被拒绝
                long nanosToWaitForRefill = probe.getNanosToWaitForRefill();
                long millisToWaitForRefill = TimeUnit.NANOSECONDS.toMillis(nanosToWaitForRefill);
                System.err.printf("请求 %d 被拒绝!需要等待 %d 毫秒后重试。当前剩余令牌:%d%n",
                        i + 1, millisToWaitForRefill, probe.getRemainingTokens());

                // 模拟客户端等待回退时间
                if (millisToWaitForRefill > 0) {
                    Thread.sleep(millisToWaitForRefill);
                }
            }
            // 每次请求间隔模拟
            Thread.sleep(50);
        }

        // 关闭Redis连接池
        jedisPool.close();
    }
}

3.3 代码解析与回退机制

  1. initRedis(String redisHost, int redisPort): 初始化Jedis连接池和JedisBasedProxyManager。ProxyManager是Bucket4j与底层存储(这里是Redis)交互的抽象层。
  2. getOrCreateLimiterBucket(String key, ...):
    • Bandwidth limit = Bandwidth.builder().capacity(capacity).refillGreedy(refillTokens, refillPeriod).build();:定义了限流的规则。
      • capacity(capacity):设置令牌桶的最大容量。
      • refillGreedy(refillTokens, refillPeriod):定义了令牌的填充策略。例如,refillGreedy(10, Duration.ofSeconds(1)) 表示每秒会向桶中添加10个令牌。
    • Bucket4j.builder().addLimit(limit).build(proxyManager, key);:这是关键一步。它使用proxyManager来为给定的key获取或创建一个分布式共享的限流桶。如果桶不存在,它会使用addLimit定义的规则来创建。
  3. userBucket.tryConsumeAndReturnRemaining(1): 尝试从桶中消费1个令牌。
    • 这个方法返回一个ConsumptionProbe对象,这是Bucket4j提供详细诊断信息的关键。
  4. probe.isConsumed(): 判断令牌是否成功消费。
    • 如果为true,表示请求被允许。probe.getRemainingTokens() 可以获取当前桶中剩余的令牌数。
    • 如果为false,表示请求被拒绝。
      • probe.getNanosToWaitForRefill():这就是我们所需的回退时间!它返回的是以纳秒为单位的,直到有足够令牌可供消费所需的等待时间。我们可以将其转换为毫秒或秒,并告知客户端。
      • probe.getRemainingTokens():在这种情况下,通常是负值,表示请求被拒绝。

通过模拟客户端在请求被拒绝后等待millisToWaitForRefill,我们实现了有效的回退机制,避免了无效的重试,保护了服务。

4. 注意事项与最佳实践

  • 限流粒度:根据业务需求选择合适的key来定义限流粒度,可以是用户ID、IP地址、API端点、组合键等。
  • 异常处理:在实际应用中,需要对Redis连接异常、网络中断等情况进行适当的异常处理。
  • 性能考量:虽然Redis性能很高,但频繁的读写操作仍然会带来一定的网络开销。合理设置令牌桶容量和填充速率,避免过于频繁的Redis操作。
  • 多重限流规则:Bucket4j支持为同一个桶定义多个Bandwidth规则,例如,可以同时设置每秒限流和每分钟限流。
  • 监控与告警:将限流器的运行状态(如拒绝率、平均等待时间)纳入监控系统,并设置告警,以便及时发现和处理问题。
  • 客户端配合:限流器的回退机制需要客户端的积极配合。客户端收到Retry-After信息后,应遵循该指示进行等待,而非立即重试。对于HTTP API,通常会在响应头中添加X-Rate-Limit-Retry-After-Seconds或Retry-After。

5. 总结

本文详细介绍了如何在Java中使用Bucket4j和Redis构建一个强大的分布式限流器,重点解决了如何实现类似“滚动窗口”的行为以及获取“回退时间”的需求。Bucket4j的ConsumptionProbe机制提供了丰富的信息,使得实现智能的限流策略和友好的客户端交互成为可能。通过合理配置和应用,开发者可以有效保护系统,提升服务稳定性。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
什么是分布式
什么是分布式

分布式是一种计算和数据处理的方式,将计算任务或数据分散到多个计算机或节点中进行处理。本专题为大家提供分布式相关的文章、下载、课程内容,供大家免费下载体验。

411

2023.08.11

分布式和微服务的区别
分布式和微服务的区别

分布式和微服务的区别在定义和概念、设计思想、粒度和复杂性、服务边界和自治性、技术栈和部署方式等。本专题为大家提供分布式和微服务相关的文章、下载、课程内容,供大家免费下载体验。

251

2023.10.07

string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

1051

2023.08.02

pdf怎么转换成xml格式
pdf怎么转换成xml格式

将 pdf 转换为 xml 的方法:1. 使用在线转换器;2. 使用桌面软件(如 adobe acrobat、itext);3. 使用命令行工具(如 pdftoxml)。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

1949

2024.04.01

xml怎么变成word
xml怎么变成word

步骤:1. 导入 xml 文件;2. 选择 xml 结构;3. 映射 xml 元素到 word 元素;4. 生成 word 文档。提示:确保 xml 文件结构良好,并预览 word 文档以验证转换是否成功。想了解更多xml的相关内容,可以阅读本专题下面的文章。

2119

2024.08.01

xml是什么格式的文件
xml是什么格式的文件

xml是一种纯文本格式的文件。xml指的是可扩展标记语言,标准通用标记语言的子集,是一种用于标记电子文件使其具有结构性的标记语言。想了解更多相关的内容,可阅读本专题下面的相关文章。

1171

2024.11.28

登录token无效
登录token无效

登录token无效解决方法:1、检查token的有效期限,如果token已经过期,需要重新获取一个新的token;2、检查token的签名,如果签名不正确,需要重新获取一个新的token;3、检查密钥的正确性,如果密钥不正确,需要重新获取一个新的token;4、使用HTTPS协议传输token,建议使用HTTPS协议进行传输 ;5、使用双因素认证,双因素认证可以提高账户的安全性。

6631

2023.09.14

登录token无效怎么办
登录token无效怎么办

登录token无效的解决办法有检查Token是否过期、检查Token是否正确、检查Token是否被篡改、检查Token是否与用户匹配、清除缓存或Cookie、检查网络连接和服务器状态、重新登录或请求新的Token、联系技术支持或开发人员等。本专题为大家提供token相关的文章、下载、课程内容,供大家免费下载体验。

843

2023.09.14

TypeScript类型系统进阶与大型前端项目实践
TypeScript类型系统进阶与大型前端项目实践

本专题围绕 TypeScript 在大型前端项目中的应用展开,深入讲解类型系统设计与工程化开发方法。内容包括泛型与高级类型、类型推断机制、声明文件编写、模块化结构设计以及代码规范管理。通过真实项目案例分析,帮助开发者构建类型安全、结构清晰、易维护的前端工程体系,提高团队协作效率与代码质量。

26

2026.03.13

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
进程与SOCKET
进程与SOCKET

共6课时 | 0.4万人学习

Redis+MySQL数据库面试教程
Redis+MySQL数据库面试教程

共72课时 | 7.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号