如何用 Redisson 分布式限流给高并发 API 网关装上流量闸门
晚上 20:00,秒杀活动准时开始。第 1 秒,网关涌进 3 万个请求;第 5 秒,数据库连接池被打穿;第 9 秒,整个链路雪崩。这不是段子,而是许多团队真实经历过的"大促惨案"。
要守住 API 网关这条防线,Redisson 分布式限流是最趁手的工具之一。Redisson 是 Valkey 与 Redis 的 Java 客户端,内置了 50 多种分布式对象与服务,限流正是其中之一——它把判断"放不放行"这件事搬到了 Redis 端,用一段原子执行的 Lua 脚本,让所有网关实例共享同一把流量闸门。本篇不讲空理论,只讲"为什么崩、限什么、怎么用",帮你把第一道防线真正搭起来。
流量一来,服务为什么先崩?
后端资源永远是有限的:数据库连接数有限、线程池有限、下游第三方接口也有配额。当请求像洪水一样涌来时,最先被冲垮的往往不是网关本身,而是它身后的服务——连接池耗尽、队列积压、超时重试又带来更多请求,形成雪崩循环。
限流的本质,就是提前承认"我们接不住所有流量",然后主动把超出的部分挡在外面。它就像高速路收费站的发卡杆:车道就那么多,单位时间放行多少辆车是定死的,多出来的车在闸口排队或改道,而不是一起涌上主路把收费站挤爆。
限流不是"数数",背后是算法之争
很多人以为限流就是"每秒记一次数,超了就拦",真正动手才发现里面有讲究。常见的做法大致有四种:
| 方案 | 核心思路 | 优点 | 短板 |
|---|---|---|---|
| 固定窗口 | 按秒/分钟切块,每块内计数 | 实现简单 | 窗口边界会出现"双倍流量"突刺 |
| 滑动窗口 | 统计"最近一个周期"内的请求 | 边界更平滑 | 需要保存时间戳,成本略高 |
| 令牌桶 | 匀速补充令牌,请求消耗令牌 | 允许突发流量 | 桶容量需要调参 |
| 漏桶 | 请求按固定速率流出 | 输出绝对平滑 | 无法应对突发,可能丢弃能力内流量 |
这里没有"最好的算法",只有"最合适的场景"。但如果每一种都从零手写,你得处理存储、过期、并发,很快就会发现限流这件小事被写成了一个大项目。
单机限流很简单,分布式限流才是真麻烦
网关从来不是一台机器。你部署了 10 个网关实例,每个实例各自维护一份计数器——单看每一台都"没超限",合起来却把后端打爆了。这就是高并发场景限流最常见的坑:限流状态必须全局共享。
更要命的是并发竞态。传统写法是"先查计数,再判断,再扣减",三步之间只要有另一个请求插进来,计数就会出错;再加上多台机器时钟不一致,滑动窗口的"现在"到底以谁为准?在分布式环境下,靠 JVM 内存和本地时间做限流,几乎必然翻车。
所以正确姿势是找一个所有实例都信任的裁判——Redis 天然适合这个角色:它是共享存储,它单线程执行命令天然串行,而 Lua 脚本能保证多条操作一次性原子完成。
Redisson 把限流做成了"开箱即用"的组件
这就是 Redisson 的价值:它没有让你去写 Redis 命令,而是把整个限流器封装成了 RRateLimiter 接口,由 RedissonRateLimiter 类实现(源码在 redisson/src/main/java/org/redisson/RedissonRateLimiter.java,接口在 redisson/src/main/java/org/redisson/api/RRateLimiter.java)。你拿到的不是一段示例代码,而是一个生产可用的组件。
它的底层设计也相当精巧:
- 一个 Hash 保存限流配置:
rate(速率)、interval(时间间隔)、type(限流类型); - 一个 value 键 记录当前剩余许可数;
- 一个 有序集合(zset) 记录每次许可发放的时间戳与数量,时间窗口滑过后自动"回收"过期许可——相当于把滑动窗口算法原样落到了 Redis 里;
- 整个"检查-扣减-记录"过程用 Lua 脚本原子执行,多台网关并发获取许可也不会互相踩踏。
另外,RateType 提供了两种粒度:OVERALL 是全局总量限流,所有实例共享一个额度;PER_CLIENT 则是按客户端维度限流,每个调用方各自独立计数,适合做"每用户每秒最多 N 次"之类的接口配额。多数网关场景用 OVERALL,做账号体系限流时再用 PER_CLIENT,两者配合覆盖了绝大多数需求。
三步把 Redisson 限流接进 API 网关
接入过程比想象中短,核心就三步:拿到限流器、设置速率、尝试获取许可。下面这段代码几乎是全部骨架:
RRateLimiter limiter = redissonClient.getRateLimiter("rate_limit:" + path);
limiter.trySetRate(RateType.OVERALL, 100, Duration.ofSeconds(1));
boolean acquired = limiter.tryAcquire();
trySetRate 只在限流器首次创建时生效,重复调用不会覆盖已有配置,天然规避了多实例重复初始化的尴尬;tryAcquire() 就是"尝试获取限流许可",成功返回 true 放行,失败返回 false,网关直接回一个 429 Too Many Requests 即可,后端连接一个多余请求都接不到。
这里还有一个容易纠结的选择:tryAcquire 和 acquire 到底用哪个?记住一条准则就行:
| 方法 | 行为 | 适合场景 |
|---|---|---|
tryAcquire() | 拿不到许可立即返回 false,不等待 | 网关过滤、快速拒绝,本篇主推 |
tryAcquire(permits, timeout, unit) | 在超时时间内等待许可 | 业务内"排队等一会"的柔性场景 |
acquire() | 一直阻塞到拿到许可为止 | 慎用,容易把等待线程堆成新瓶颈 |
进阶:限流速率动态调整的正确姿势
限流不是配一次就一劳永逸。大促开始时你可能想把阈值调高,活动结束后再降下来。这时候用 updateRate 配合 RateLimiterArgs,可以动态修改速率而无需重建限流器:
limiter.updateRate(RateLimiterArgs.of(RateType.OVERALL, 500, Duration.ofSeconds(1)).keepState(true));
注意那个 .keepState(true)——它表示调整速率时保留当前已积累的许可状态,避免每次调参都把计数清零、给下游造成一次瞬时脉冲。如果限流器长期闲置,还可以用 trySetRate 的 keepAliveTime 参数让它自动过期清理,省得 Redis 里堆一堆没人用的键。
顺带一提,acquire(10) 这类批量获取许可的用法,适合"一次操作消耗多个配额"的场景,比如批量任务提交,按业务权重计费,而不是每个请求都固定消耗 1。
动手试试,把第一道防线搭起来
限流的价值不在于代码多华丽,而在于它让"最坏情况"变得可控:流量再大,后端永远只看到它能承受的那部分。把 getRateLimiter 加进你的网关过滤器、给不同接口配上不同的速率、再盯一盯被拒绝请求的曲线,第一道防线就算立住了。更完整的 API 说明可以翻阅项目文档 docs/overview.md,遇到具体接口的细节,直接去 RRateLimiter 的源码注释里找答案,往往比任何教程都准确。🚀
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



