分布式限流方案:基于 Redis 的令牌桶算法实现
前言
在分布式场景下,接口限流变得更加复杂。传统的单机限流方式难以满足跨节点的限流需求,因此需要一种分布式限流方案。
这里介绍一种基于 Redis 和 Redisson 实现的令牌桶算法分布式限流方案。
一、原理介绍:令牌桶算法
令牌桶算法是一种用于控制流量的经典算法,其基本原理如下:
生成令牌:按照固定的速率向令牌桶中放入令牌。
消耗令牌:每个请求到来时需要消耗一个令牌才能执行。
桶满时丢弃令牌:如果令牌桶已满,额外生成的令牌会被丢弃。
拒绝无令牌请求:当令牌桶为空且有请求到达时,拒绝该请求。
示意图
+--------------------------+
| 请求到达 |
+--------------------------+
|
V
+----------------------------+
| 令牌桶中是否有令牌? |
+----------------------------+
/ \
是 否
/ \
+--------------------+ +----------------------+
| 消耗令牌,放行 | | 拒绝请求,限流 |
+--------------------+ +----------------------+
二、分布式限流的设计思路
在分布式环境中,多个节点需要共享限流状态。为了解决这个问题,我们采用 Redis 作为分布式存储,并通过 Redisson 的 RRateLimiter
组件实现分布式的令牌桶限流:
- Redis 统一存储令牌桶状态:
- 使用 Redis 的
RRateLimiter
对象存储令牌桶的容量和剩余令牌数。
- 多节点共享限流状态:
- 各个服务节点通过 Redis 读取和更新令牌桶状态,实现跨节点的流量控制。
- 动态配置更新:
- 支持从 Redis 中动态获取限流配置,实现限流规则的热更新。
- 基于 IP + 接口路径的粒度限流:
- 使用
api_limit:ip:apiPath
作为 Redis 的 Key,针对不同接口和 IP 进行精细化限流。
三、代码实现
初始化令牌桶
/** * 获取指定接口的令牌桶(每个接口独立一个) * * @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; }); }
申请令牌
/** * 申请一个令牌 * * @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; }
统一限流校验方法
/** * 令牌限流检查(对外暴露的方法) * @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 命令读取当前限流配置(
rate
和interval
),确保分布式环境下的限流配置保持一致。如果发现 Redis 中的配置与本地配置不一致或令牌桶失效,则重新初始化令牌桶。
4. IP + 接口路径粒度限流
- 每个接口的限流是基于
api_limit:ip:apiPath
作为 Redis 的 Key,实现了IP + 接口级别的限流,可有效防止单个 IP 的恶意请求。
四、方案优缺点
优点 | 缺点 |
---|---|
支持分布式环境,多节点共享限流状态 | 依赖 Redis,如果 Redis 异常会影响限流功能 |
支持突发流量,平滑处理请求 | 需要额外维护 Redis 的资源占用 |
支持动态限流配置,实时生效 | 需要额外监控 Redis 的健康状态 |
提供接口级和 IP 级别的精细化限流 | 配置不当可能导致限流过于宽松或过于严格 |
五、 适用场景
API 网关限流:在 API 网关层通过该方案对外部流量进行限流,保护后端服务。
防止恶意攻击:防止某个 IP 针对特定接口的恶意请求。
限流突发流量:在秒杀、促销等场景中平滑处理流量峰值。
支付接口保护:确保支付接口在高并发情况下依旧可用。
总结
基于 Redis 的分布式令牌桶限流方案是一个可靠且高效的限流策略。它不仅能够有效应对突发流量,还能在分布式环境下保持限流配置一致性。
通过合理的配置和监控,可以保障系统的稳定性,提升用户体验。😊