尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

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

接口被刷到宕机后,我把分布式限流搬进了API网关:Redisson实战全记录 接口被刷到宕机后我把分布式限流搬进了API网关Redisson实战全记录【免费下载链接】redissonRedisson: 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..项目地址: 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 实现原子性由脚本保证支持全局/按客户端维度还能动态调速率缺点依赖 RedisRedis 挂了需要降级预案适合绝大多数分布式 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 MonoVoid 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-Afterbody 里说明请求过于频繁前端拿到后可以走降级提示、弹验证码或者直接重试。第五步和熔断降级配合。限流是入口闸门熔断是出口保险。网关限流管住流量总量下游仍可能偶发故障所以还要配合 Sentinel 或 Resilience4j 做熔断限流防的是进太多熔断防的是出不去。两个配合才是一个完整的限流降级闭环。网关踩坑清单都是血泪过滤器顺序不对限流过滤器没放在最前面请求先过了鉴权、日志才被拦等于白限限流器名字没隔离所有接口共用一个限流器一个热点接口把别的接口也饿死了trySetRate和setRate混用前者只在没配置时生效后者会重置状态。想改配置却调了trySetRate会发现改了没反应网关超时配置太短acquire()阻塞等待时如果网关的读超时小于等待时间会抛异常而不是返回 429要统一用tryAcquire(timeout)并捕获超时调优与FAQ动态调速、批量/异步还有三个经典坑动态调整限流速率秒杀开始前把速率调高结束后调低不用重启网关// 动态调速会重置当前桶状态 limiter.setRate(RateLimiterArgs.of(RateType.OVERALL, 500, Duration.ofSeconds(1)));配合配置中心Nacos/Apollo下发就能实现活动前自动扩容限流阈值。我们当时就靠这个把秒杀接口的速率从 100 临时提到 500活动结束再调回来。批量与异步获取许可批量订单提交接口一次消耗多个许可比如一个订单算 2 个用tryAcquire(2)给重操作加权防止批量调用钻空子异步高并发下别用同步阻塞版本tryAcquireAsync()返回RFutureBoolean配合回调处理放行/拒绝网关的线程池不会被占满三个经典问题的解决办法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是否正常再把维度拆成接口总量 单用户两个桶最后接上配置中心做动态调速。限流是保护系统的第一道防线但它永远不能是唯一一道——熔断、降级、隔离一个都不能少。【免费下载链接】redissonRedisson: 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..项目地址: https://gitcode.com/GitHub_Trending/re/redisson创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表