接口被刷到宕机后,我把分布式限流搬进了API网关:Redisson实战全记录

接口被刷到宕机后,我把分布式限流搬进了API网关:Redisson实战全记录

【免费下载链接】redisson Redisson: Valkey & Redis Java Client and Real-Time Data Platform. Sync/Async/RxJava/Reactive API. Over 50 Valkey and Redis based Java objects and services: Set, Multimap, SortedSet, Map, List, Queue, Deque, Semaphore, Lock, AtomicLong, Map Reduce, Bloom filter, Spring, Tomcat, Scheduler, JCache API, Hibernate, RPC, local cache.. 【免费下载链接】redisson 项目地址: https://gitcode.com/GitHub_Trending/re/redisson

周五晚上八点,促销秒杀开始,10万请求在十秒内涌进网关。订单服务CPU飙到100%,数据库连接池被打穿,工单群里一片哀嚎。事后复盘发现,真正的问题是分布式限流没有做——而它本该用一行代码挡住这场风暴。这篇文章就带你走一遍我用 Redisson 在网关层落地分布式限流的完整过程,看完你就能直接抄作业。

一场"促销事故"教会我的事

先还原一下当时的现场。

我们的网关是 Spring Cloud Gateway,后端挂了六个订单服务实例。一开始大家以为"集群嘛,扛得住",结果流量一进来,六个实例同时被击穿——因为没人做任何限流,请求全量打到数据库。

更扎心的是,事后检查日志,里面混着大量脚本刷单请求:同一个 IP、同一台设备,一秒钟发了上百次。接口防刷限流降级这两件事,平时觉得"上线再加也来得及",真出事才发现根本没机会补。

所以先说结论:网关必须有一道"闸门",挡住超出下游承载能力的流量,而这道闸门,我最终选了 Redisson。

四套限流方案,我为什么最后选了Redisson

在动手之前,我把市面上的方案都过了一遍,各有各的坑。

单机限流(如 Guava RateLimiter)

  • 优点:零依赖,写起来最快,本地缓存不占网络
  • 缺点:每台机器各限各的。六台实例、每台限100,总量就是600,根本没达到保护效果;重启还丢状态
  • 适合:单体应用、临时顶一下

Nginx / OpenResty 层限流

  • 优点:在流量入口拦截,性能极好,静态配置简单
  • 缺点:规则写死在配置里,想按用户维度、按接口维度动态调整很痛苦;集群多 Nginx 节点时同样要共享状态,得搭 Redis 配合
  • 适合:只做粗粒度 IP/连接数限制

Redis + Lua 自研

  • 优点:状态集中、原子性强,能精确控制
  • 缺点:令牌桶、滑动窗口、GCRA……每种算法都要自己写脚本,还要自己处理过期、防并发、容灾。我见过团队自研半年,最后代码比业务还复杂
  • 适合:公司有专门的中间件团队

Redisson 分布式限流

  • 优点:开箱即用的 RRateLimiter,基于 Redis 的 Hash + ZSet + Lua 实现,原子性由脚本保证;支持全局/按客户端维度,还能动态调速率
  • 缺点:依赖 Redis,Redis 挂了需要降级预案
  • 适合:绝大多数分布式 Java 应用

我选 Redisson 的理由很朴素:它把最难的部分(原子计数、状态管理、多实例同步)都封装好了,我只需要关心"限多少"和"超限怎么办"。官方文档里有一章专门讲各种分布式对象,限流器在 docs/data-and-services/objects.md 里有完整用法,后面源码级的疑问我都是去那翻的。

5分钟跑起来的第一个限流器

按"先跑起来再深究"的原则,我们先写最小示例。项目里加依赖,用现成的 redisson-spring-boot-starter/,连 Redis 的事它都帮你办了:

RedissonClient redisson = Redisson.create(config);
// 每个限流器对应一个 Redis 键,名字就是业务语义
RRateLimiter limiter = redisson.getRateLimiter("api:order:submit");

// 每 1 秒最多放行 100 个请求,只看当前值,不覆盖已有配置
limiter.trySetRate(RateType.OVERALL, 100, Duration.ofSeconds(1));

// 试获取一个许可:拿得到就放行,拿不到立刻返回 false
if (limiter.tryAcquire()) {
    return handleOrder(order);
} else {
    return ResponseEntity.status(429).build(); // 超限,直接打回
}

这段代码跑在单机上和跑在六台实例上,效果是一样的:总量永远被压在 100/s。你不需要理解背后的实现也能用,但下面我劝你花三分钟看懂它,因为踩坑都藏在细节里。

令牌桶怎么在Redis里落地:RedissonRateLimiter原理拆解

用过之后你可能会好奇:tryAcquire() 到底在 Redis 里做了什么?我翻了实现类 RedissonRateLimiter.java,发现它的设计非常"令牌桶"。

它用了三类键。 限流器名字对应的 Hash 键存配置(rate、interval、type);一个 value 键存"当前桶里还剩多少令牌";一个 permits 键是个 ZSet,记录每个许可被拿走的时间戳。为什么用 ZSet?因为令牌过期要按时间清理——ZSet 的 score 就是毫秒时间戳,清理脚本直接 zremrangebyscore 把超期的记录删掉,同时把对应的令牌还回桶里。

原子性靠 Lua 脚本。 每次 tryAcquire 不是多条 Redis 命令,而是把"读配置→算过期令牌→判断够不够→扣减/记录"整段逻辑塞进一个 Lua 脚本一次性执行。Redis 保证脚本执行期间不会插入其他命令,所以多实例并发扣减不会超卖。脚本核心逻辑大概是:

local current = redis.call('get', valueName)
-- 把已经超过一个 interval 的许可记录清掉,还回令牌
local expired = redis.call('zrangebyscore', permitsName, 0, now - interval)
-- 桶里不够就返回需要等待的时长,够就 zadd + decrby 扣减

"桶"长什么样取决于 RateType。 OVERALL 是所有 Redisson 实例共用一个桶;PER_CLIENT 是每个实例一个桶,限流维度就变成"每个客户端节点各限各的"。很多团队在这个参数上踩过坑——想限总量却用了 PER_CLIENT,结果总量翻了实例数倍。

看完这个你就能理解一个常见疑问:tryAcquire 失败后那个返回值是什么?是下一个令牌可用前需要等待的毫秒数acquire() 内部就是拿这个值去做阻塞等待的。这让批量请求、排队请求都有了理论依据。

Spring Cloud Gateway接入:从依赖到降级闭环

理论落地,我们把限流器挂到网关过滤链上。完整流程分五步。

第一步,加依赖。 网关项目引入 Redisson starter,配置好单机或集群连接,网关和业务共用同一个 Redis 就行。

第二步,配置连接。 用官方 starter 的话,spring.redis 配置写好后 RedissonClient 会自动注入,网关过滤器里直接构造注入,不需要自己管理连接生命周期。

第三步,写网关限流过滤器。 核心是按请求特征生成限流器名字,再决定放不放行:

@Component
public class RateLimitFilter implements GlobalFilter, Ordered {
    private final RedissonClient redisson;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getPath().value();
        // 按接口路径隔离限流,一个接口一个桶,互不干扰
        RRateLimiter limiter = redisson.getRateLimiter("gw:rate:" + path);
        if (limiter.tryAcquire()) {
            return chain.filter(exchange);   // 拿到许可,放行
        }
        // 超限:直接回 429,不再往后端转发
        exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
        return exchange.getResponse().setComplete();
    }

    @Override
    public int getOrder() { return -100; }  // 越靠前越先执行
}

第四步,设计超限响应。 429 不能只给个状态码,客户端拿不到原因会一脸懵。建议在响应头里带 Retry-After,body 里说明"请求过于频繁",前端拿到后可以走降级提示、弹验证码或者直接重试。

第五步,和熔断降级配合。 限流是"入口闸门",熔断是"出口保险"。网关限流管住流量总量,下游仍可能偶发故障,所以还要配合 Sentinel 或 Resilience4j 做熔断:限流防的是"进太多",熔断防的是"出不去"。两个配合,才是一个完整的限流降级闭环。

网关踩坑清单(都是血泪)

  • 过滤器顺序不对:限流过滤器没放在最前面,请求先过了鉴权、日志才被拦,等于白限
  • 限流器名字没隔离:所有接口共用一个限流器,一个热点接口把别的接口也饿死了
  • trySetRatesetRate 混用:前者只在没配置时生效,后者会重置状态。想改配置却调了 trySetRate,会发现"改了没反应"
  • 网关超时配置太短acquire() 阻塞等待时如果网关的读超时小于等待时间,会抛异常而不是返回 429,要统一用 tryAcquire(timeout) 并捕获超时

调优与FAQ:动态调速、批量/异步,还有三个经典坑

动态调整限流速率

秒杀开始前把速率调高,结束后调低,不用重启网关:

// 动态调速,会重置当前桶状态
limiter.setRate(RateLimiterArgs.of(RateType.OVERALL, 500, Duration.ofSeconds(1)));

配合配置中心(Nacos/Apollo)下发,就能实现"活动前自动扩容限流阈值"。我们当时就靠这个,把秒杀接口的速率从 100 临时提到 500,活动结束再调回来。

批量与异步获取许可

  • 批量:订单提交接口一次消耗多个许可(比如一个订单算 2 个),用 tryAcquire(2),给重操作加权,防止批量调用钻空子
  • 异步:高并发下别用同步阻塞版本,tryAcquireAsync() 返回 RFuture<Boolean>,配合回调处理放行/拒绝,网关的线程池不会被占满

三个经典问题的解决办法

Q1:限流好像不生效? 先查两件事:一是确认所有网关实例连的是同一个 Redis(多环境 Redis 配串了很常见);二是确认用的 RateType.OVERALL 而不是 PER_CLIENT,后者是按实例限的。

Q2:误伤了正常用户? 常见原因是把"按 IP 限"和"按接口限"混在一个桶里。正确姿势是拆维度:全局总量一个桶、单用户一个桶(ratelimit:user:" + userId)、单 IP 一个桶。三者独立,各限各的,刷子被按 IP 挡住,正常用户不受影响。

Q3:集群 Redis 的时间问题? 脚本用的是 System.currentTimeMillis() 也就是客户端时间戳。如果各网关实例的时钟不同步,令牌过期判断就会错乱。NTP 对时是基本要求;实在没法对齐,就在 tryAcquire 前统一从 Redis 取一次时间,或者用 TIME 命令对齐,代价是多一次往返。

总结

回到开头那场事故:如果当时网关上有这道限流闸门,10 万请求最多只有 100 个能到达后端,剩下的全部 429 打回,数据库连一根头发都不会掉。

下一步建议你这样做:先抄第五节的最小过滤器,用压测工具打到超限,观察 429 和 Retry-After 是否正常;再把维度拆成"接口总量 + 单用户"两个桶;最后接上配置中心做动态调速。限流是保护系统的第一道防线,但它永远不能是唯一一道——熔断、降级、隔离,一个都不能少。🚀

【免费下载链接】redisson Redisson: Valkey & Redis Java Client and Real-Time Data Platform. Sync/Async/RxJava/Reactive API. Over 50 Valkey and Redis based Java objects and services: Set, Multimap, SortedSet, Map, List, Queue, Deque, Semaphore, Lock, AtomicLong, Map Reduce, Bloom filter, Spring, Tomcat, Scheduler, JCache API, Hibernate, RPC, local cache.. 【免费下载链接】redisson 项目地址: https://gitcode.com/GitHub_Trending/re/redisson

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值