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

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

相关推荐
SummerGao.11 分钟前
【实操】Mybatis-plus2.x升级到3.x
java·spring boot·mybatisplus·系统升级
珹洺13 分钟前
C++从入门到实战(五)类和对象(第一部分)为什么有类,及怎么使用类,类域概念详解(附带图谱等更好对比理解)
java·c语言·开发语言·数据结构·c++·redis·缓存
就改了15 分钟前
SpringMVC 跨域问题两种常用解决方案
java·springmvc
凌乱的程序猿16 分钟前
安装和部署Tomcat并在idea创建web文件
java·前端·tomcat
辰尘_星启24 分钟前
【Gen6D】位姿估计部署日志
人工智能·pytorch·深度学习·算法·位姿估计·感知
居然有人65431 分钟前
45.图论3
算法·深度优先·图论
Cindy辛蒂36 分钟前
C语言:穷举法编程韩信点兵问题四种做法
c语言·开发语言·算法
为美好的生活献上中指42 分钟前
java每日精进 3.21 【SpringBoot规范2.0】
java·开发语言·spring boot·log4j·async·mail
kkk哥1 小时前
基于springboot的教师工作量管理系统(031)
java·spring boot·后端
可了~1 小时前
SpringBoot的配置文件了解
java·spring boot·后端