分布式限流方案:基于 Redis 的令牌桶算法实现

分布式限流方案:基于 Redis 的令牌桶算法实现


前言

在分布式场景下,接口限流变得更加复杂。传统的单机限流方式难以满足跨节点的限流需求,因此需要一种分布式限流方案。

这里介绍一种基于 RedisRedisson 实现的令牌桶算法分布式限流方案。


一、原理介绍:令牌桶算法

令牌桶算法是一种用于控制流量的经典算法,其基本原理如下:

  • 生成令牌:按照固定的速率向令牌桶中放入令牌。

  • 消耗令牌:每个请求到来时需要消耗一个令牌才能执行。

  • 桶满时丢弃令牌:如果令牌桶已满,额外生成的令牌会被丢弃。

  • 拒绝无令牌请求:当令牌桶为空且有请求到达时,拒绝该请求。

示意图

lua 复制代码
        +--------------------------+
        |      请求到达            |
        +--------------------------+
                    |
                    V
       +----------------------------+
       |   令牌桶中是否有令牌?     |
       +----------------------------+
               /            \
            是                 否
           /                     \
+--------------------+    +----------------------+
|    消耗令牌,放行   |    |   拒绝请求,限流    |
+--------------------+    +----------------------+

二、分布式限流的设计思路

在分布式环境中,多个节点需要共享限流状态。为了解决这个问题,我们采用 Redis 作为分布式存储,并通过 Redisson 的 RRateLimiter 组件实现分布式的令牌桶限流:

  1. Redis 统一存储令牌桶状态:
  • 使用 Redis 的 RRateLimiter 对象存储令牌桶的容量和剩余令牌数。
  1. 多节点共享限流状态:
  • 各个服务节点通过 Redis 读取和更新令牌桶状态,实现跨节点的流量控制。
  1. 动态配置更新:
  • 支持从 Redis 中动态获取限流配置,实现限流规则的热更新。
  1. 基于 IP + 接口路径的粒度限流:
  • 使用 api_limit:ip:apiPath 作为 Redis 的 Key,针对不同接口和 IP 进行精细化限流。

三、代码实现

  1. 初始化令牌桶

    java 复制代码
    /**
     * 获取指定接口的令牌桶(每个接口独立一个)
     *
     * @param apiKey   接口唯一标识
     * @param rate     允许的请求数
     * @param interval 时间窗口(秒)
     * @return RRateLimiter 令牌桶实例
     */
    public RRateLimiter getRateLimiter(String apiKey, int rate, int interval) {
        return rateLimiterCache.compute(apiKey, (key, existingLimiter) -> {
            String redisKey = String.format("%s:%s", RedisKeyConstant.DOC_RATE_LIMIT_PRE, key);
            RRateLimiter rateLimiter = redissonClient.getRateLimiter(redisKey);
    
            // 获取 Redis 中的当前限流配置
            List<Integer> config = getRateLimiterConfig(redisKey);
            Integer currentRate = config.get(0);
            Integer currentInterval = config.get(1);
    
            // 检查是否需要重新初始化
            if (existingLimiter != null && existingLimiter.isExists()
                    && Objects.equals(currentRate, rate)
                    && Objects.equals(currentInterval, interval)) {
                return existingLimiter;
            }
    
            log.warn("检测到限流配置变化或 RateLimiter 失效,重新初始化令牌桶 [{}]", redisKey);
    
            // 重新初始化令牌桶
            rateLimiter.delete();
            if (!rateLimiter.trySetRate(RateType.OVERALL, rate, interval, RateIntervalUnit.SECONDS)) {
                log.error("令牌桶 [{}] 初始化失败", redisKey);
                return null;
            }
    
            log.info("创建令牌桶 [{}],QPS: {}, 时间窗口: {} 秒", apiKey, rate, interval);
            return rateLimiter;
        });
    }
  2. 申请令牌

    java 复制代码
    /**
     * 申请一个令牌
     *
     * @param ip   接口唯一标识
     * @param apiPath   接口唯一标识
     * @param rate     允许的请求数
     * @param interval 时间窗口(秒)
     * @return 是否成功获取令牌
     */
    public boolean tryAcquire(String ip, String apiPath, int rate, int interval) {
        String apiKey = "api_limit:" + ip + ":" + apiPath;
        RRateLimiter rateLimiter = getRateLimiter(apiKey, rate, interval);
        if (rateLimiter == null) {
            return false;
        }
    
        boolean acquired = rateLimiter.tryAcquire();
    
        // Redis Key 可能被删除,需要重新初始化
        if (!acquired && !rateLimiter.isExists()) {
            log.warn("检测到 RateLimiter [{}] 失效,重新初始化", apiKey);
            rateLimiterCache.remove(apiKey);
            rateLimiter = getRateLimiter(apiKey, rate, interval);
            if (rateLimiter != null) {
                acquired = rateLimiter.tryAcquire();
            }
        }
    
        if (!acquired) {
            log.warn("接口 [{}] 触发限流,QPS: {}, 时间窗口: {} 秒", apiKey, rate, interval);
        }
        return acquired;
    }
  3. 统一限流校验方法

    java 复制代码
    /**
     * 令牌限流检查(对外暴露的方法)
     * @param rate      允许的请求数
     * @param interval  时间窗口(秒)
     * @throws ServiceException 如果触发限流
     */
    public void checkRateLimit(int rate, int interval) {
        String clientIp = WebTool.getRealIpAddress(); // 获取真实 IP
        String apiPath = WebTool.getApiPath(); // 获取接口路径
    
        boolean allowed = tryAcquire(clientIp, apiPath, rate, interval);
        if (!allowed) {
            log.warn("ip: {}, 接口 [{}] 触发限流,QPS: {}, 时间窗口: {} 秒", clientIp, apiPath, rate, interval);
            String msg = String.format("访问过于频繁,请稍后再试。IP: %s", clientIp);
            throw new ServiceException(msg);
        }
    }

✅ 方案解析

1. 分布式环境下的限流

  • 使用 Redis 作为中心存储,每个接口的令牌桶都存储在 Redis 中,便于多个节点共享限流状态。

  • 使用 Redisson 提供的 RRateLimiter 对象,它基于 Redis 提供了令牌桶算法的封装,自动管理令牌生成和消费的过程。

2. 令牌桶的管理

  • 通过 getRateLimiter 方法动态创建和管理令牌桶。

  • 使用 rateLimiter.trySetRate() 设置令牌桶的容量和生成速率。

  • 每次请求前调用 rateLimiter.tryAcquire() 尝试获取一个令牌,如果成功则执行请求,否则拒绝。

3. 动态配置管理

  • 使用 Redis hget 命令读取当前限流配置(rateinterval),确保分布式环境下的限流配置保持一致。

  • 如果发现 Redis 中的配置与本地配置不一致或令牌桶失效,则重新初始化令牌桶。

4. IP + 接口路径粒度限流

  • 每个接口的限流是基于 api_limit:ip:apiPath 作为 Redis 的 Key,实现了IP + 接口级别的限流,可有效防止单个 IP 的恶意请求。

四、方案优缺点

优点 缺点
支持分布式环境,多节点共享限流状态 依赖 Redis,如果 Redis 异常会影响限流功能
支持突发流量,平滑处理请求 需要额外维护 Redis 的资源占用
支持动态限流配置,实时生效 需要额外监控 Redis 的健康状态
提供接口级和 IP 级别的精细化限流 配置不当可能导致限流过于宽松或过于严格

五、 适用场景

  • API 网关限流:在 API 网关层通过该方案对外部流量进行限流,保护后端服务。

  • 防止恶意攻击:防止某个 IP 针对特定接口的恶意请求。

  • 限流突发流量:在秒杀、促销等场景中平滑处理流量峰值。

  • 支付接口保护:确保支付接口在高并发情况下依旧可用。


总结

基于 Redis 的分布式令牌桶限流方案是一个可靠且高效的限流策略。它不仅能够有效应对突发流量,还能在分布式环境下保持限流配置一致性。

通过合理的配置和监控,可以保障系统的稳定性,提升用户体验。😊

相关推荐
zzb15803 小时前
RAG from Scratch-优化-query
java·数据库·人工智能·后端·spring·mybatis
wuqingshun3141593 小时前
如何停止一个正在退出的线程
java·开发语言·jvm
卷福同学3 小时前
QClaw内测体验,能用微信指挥AI干活了
人工智能·算法·ai编程
sali-tec3 小时前
C# 基于OpenCv的视觉工作流-章34-投影向量
图像处理·人工智能·opencv·算法·计算机视觉
xiaoye-duck3 小时前
《算法题讲解指南:递归,搜索与回溯算法--递归》--3.反转链表,4.两两交换链表中的节点,5.快速幂
数据结构·c++·算法·递归
Eward-an3 小时前
【算法竞赛/大厂面试】盛最多水容器的最大面积解析
python·算法·leetcode·面试·职场和发展
山栀shanzhi3 小时前
归并排序(Merge Sort)原理与实现
数据结构·c++·算法·排序算法
阿豪学编程3 小时前
LeetCode438: 字符串中所有字母异位词
算法·leetcode
Trouvaille ~3 小时前
【递归、搜索与回溯】专题(七):FloodFill 算法——勇往直前的洪水灌溉
c++·算法·leetcode·青少年编程·面试·蓝桥杯·递归搜索回溯
地平线开发者4 小时前
征程 6P codec decoder sample
算法·自动驾驶