双十一零点扛过10倍流量洪峰:Sentinel与Redis+Lua的分布式限流深度避坑指南
导读:双十一零点,流量瞬间暴涨 10 倍,数据库连接池打满,服务超时,雪崩一触即发……限流不是可选项,是微服务的生命线。今天我们来聊聊,如何在分布式环境下优雅地“丢请求”,保核心系统。
在单体时代,限流很简单:Guava RateLimiter、Semaphore 或者漏桶/令牌桶算法,内存里玩得风生水起。但到了微服务、多实例部署的场景,单个节点的限流变成了一本糊涂账——节点 A 限制 100 QPS,节点 B 也限制 100 QPS,但总流量可能冲到 200 QPS,后端依然被打爆。
分布式限流登场:将限流状态集中管理(Redis、Sentinel 集群),所有节点共享同一个“流量额度”,真正实现对全局入口的精准控制。
本文将从原理、方案对比、代码实战到避坑点,全面剖析企业级分布式限流的两种主流路子——Redis + Lua和阿里 Sentinel。
一、从“单机兵”到“集团军”:为什么需要分布式限流?
先看一个典型反例:
- 订单服务部署了 5 个 Pod
- 单机限流 200 QPS(令牌桶)
- 总容量 = 5 × 200 = 1000 QPS
- 外部恶意流量 1500 QPS,分摊到每台 300 QPS
- 每台都超限,但单机限流各自为政,总流量依然冲垮了下游数据库
分布式限流的核心目标:所有节点共享同一个限流计数器/令牌桶/漏桶状态,实现全局公平或按资源的精细化控制。
二、四大限流算法快速复习(半分钟看懂)
| 算法 | 原理 | 特点 | 适用场景 |
|---|---|---|---|
| 计数器 | 窗口内累加请求数,超阈值拒绝 | 简单,有“突刺”问题 | 粗粒度、非敏感场景 |
| 滑动窗口 | 将窗口分多个小格子,滑动计数 | 平滑,内存占用稍高 | 通用 API 限流 |
| 漏桶 | 请求入桶,恒定速率流出 | 强行平滑突发流量 | 保护下游弱处理能力 |
| 令牌桶 | 以固定速率放令牌,请求拿令牌通行 | 允许短时突发,灵活 | 高性能、允许突增场景 |
分布式限流多采用滑动窗口或令牌桶的高性能实现。
三、两大主流方案对比:Redis+Lua vs Sentinel
| 维度 | Redis + Lua | 阿里 Sentinel(集群流控) |
|---|---|---|
| 核心原理 | Lua 脚本原子性操作 Redis 计数器/令牌桶 | 基于滑动窗口 + 集群 Token Server |
| 依赖组件 | Redis(必须) | 可独立,集群模式需 Token Server 或 Redis |
| 性能 | 单次 Redis 调用 ~0.1ms,超高并发会拉高延迟 | 本地限流几乎无损耗,集群限流需 RPC 调用 |
| 准确性 | 强一致(Redis 单机/集群保证原子性) | 集群模式存在少量误差(Netty 通信延迟) |
| 功能丰富度 | 需手写算法逻辑 | 开箱即用:QPS/线程数/冷启动/关联限流/热点参数限流 |
| 运维成本 | 低(Redis 已是标配) | 中等(Sentinel 控制台需额外部署) |
| 语言无关性 | 任何语言都能用 | Java 最佳,非 Java 需自研客户端 |
| 流量分摊 | 全局精确 | 支持按调用来源、任意标识分组 |
一句话总结:
- Redis+Lua:轻量、通用、灵活,适合 Redis 已有、不想引入新组件的中小型团队。
- Sentinel:功能全、生态好(Spring Cloud Alibaba 亲儿子),适合 Java 栈、需要复杂流控策略(熔断、热点、系统自适应)的大中型项目。
四、实战一:Redis + Lua 实现分布式令牌桶限流
本实战将实现一个全局限流注解,任何方法加上@RedisRateLimiter即可保护。
4.1 为什么必须用 Lua 脚本?
因为 Redis 的INCR、GET、SET等命令不是原子组合。令牌桶需要“获取令牌 + 更新剩余令牌”两步,高并发下会出现超卖。Lua 脚本在 Redis 中整体原子执行,完美解决。
4.2 编写 Lua 脚本(token_bucket.lua)
-- 令牌桶限流 Lua 脚本-- KEYS[1] : 桶的唯一 key-- ARGV[1] : 最大令牌容量 capacity-- ARGV[2] : 令牌生成速率 rate (每秒几个)-- ARGV[3] : 当前请求需要的令牌数 (通常为1)-- ARGV[4] : 当前时间戳(秒)localkey=KEYS[1]localcapacity=tonumber(ARGV[1])localrate=tonumber(ARGV[2])localrequested=tonumber(ARGV[3])localnow=tonumber(ARGV[4])-- 获取桶的当前状态 {last_refresh_time, current_tokens}localbucket=redis.call('hmget',key,'last_time','tokens')locallast_time=tonumber(bucket[1])ornowlocaltokens=tonumber(bucket[2])orcapacity-- 计算应该补充的令牌数localdelta=math.max(0,now-last_time)localfilled_tokens=math.min(capacity,tokens+(delta*rate))-- 判断是否足够localallowed=0iffilled_tokens>=requestedthenallowed=1filled_tokens=filled_tokens-requestedend-- 保存新状态redis.call('hmset',key,'last_time',now,'tokens',filled_tokens)-- 设置过期时间,避免闲置 key 浪费内存(2倍时间窗口)redis.call('expire',key,60)returnallowed4.3 Spring Boot 中集成 Redis + Lua
① 引入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency>② 加载 Lua 脚本
@ComponentpublicclassRedisRateLimiter{privatefinalRedisScript<Long>rateLimitScript;publicRedisRateLimiter(RedisTemplate<String,Object>redisTemplate){// 读取 classpath 下的 lua 文件DefaultRedisScript<Long>script=newDefaultRedisScript<>();script.setScriptSource(newResourceScriptSource(newClassPathResource("lua/token_bucket.lua")));script.setResultType(Long.class);this.rateLimitScript=script;this.redisTemplate=redisTemplate;}@AutowiredprivateRedisTemplate<String,Object>redisTemplate;publicbooleantryAcquire(Stringkey,longcapacity,doublerate,intrequested){List<String>keys=Collections.singletonList(key);Longresult=redisTemplate.execute(rateLimitScript,keys,String.valueOf(capacity),String.valueOf(rate),String.valueOf(requested),String.valueOf(System.currentTimeMillis()/1000));returnresult!=null&&result==1L;}}③ 自定义注解 + AOP 实现无侵入限流
@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public@interfaceDistributedRateLimit{Stringkey();// 限流 key,支持 SpELlongcapacity()default100;doublerate()default10;// 每秒生成令牌数intrequested()default1;}切面实现(省略部分校验代码):
@Around("@annotation(rateLimit)")publicObjectaround(ProceedingJoinPointjoinPoint,DistributedRateLimitrateLimit)throwsThrowable{Stringkey=parseKey(rateLimit.key(),joinPoint);// 支持 SpEL 解析,如 #userIdbooleanallowed=redisRateLimiter.tryAcquire(key,rateLimit.capacity(),rateLimit.rate(),rateLimit.requested());if(!allowed){thrownewRateLimitException("请求过于频繁,请稍后再试");}returnjoinPoint.proceed();}④ 业务处使用
@GetMapping("/order")@DistributedRateLimit(key="order:create:#{#userId}",capacity=50,rate=10)publicStringcreateOrder(@RequestParamLonguserId){return"订单创建成功";}效果:无论多少个实例,同一个userId的请求被全局限制在 10 QPS,且允许短时突发。
五、实战二:阿里 Sentinel 集群流控(更适合生产)
Sentinel 提供两种集群模式:
- 嵌入模式(Embedded):选一个节点作为 Token Server,其他为 Client(适合小规模)
- 独立模式(Alone):独立 Token Server 集群(适合大规模)
5.1 搭建 Sentinel 控制台
dockerrun-d--namesentinel-p8858:8858-p8719:8719 bladex/sentinel-dashboard:1.8.6访问http://localhost:8858,默认账号 sentinel/sentinel。
5.2 Spring Boot 集成 Sentinel
① 依赖
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId></dependency><dependency><groupId>com.alibaba.csp</groupId><artifactId>sentinel-cluster-client-default</artifactId></dependency><dependency><groupId>com.alibaba.csp</groupId><artifactId>sentinel-cluster-server-default</artifactId></dependency>② 配置集群流控(独立模式示例)
在 application.yml 中配置应用为 Token Client:
spring:cloud:sentinel:transport:dashboard:localhost:8858cluster-client:server-host:your-token-server-ip# Token Server 地址server-port:18730flow:cold-factor:3同时在控制台配置集群流控规则(如针对/order/create资源的全局 QPS 阈值 = 500)。
③ 代码中无需显式调用,Sentinel 会通过拦截器自动保护 Web 接口。
5.3 Sentinel 比 Redis+Lua 强在哪里?
- 热点参数限流:对
userId维度单独限流(如每个用户 10 QPS),普通 Redis 脚本需要为每个用户维护桶,可能产生海量 key。 - 熔断降级:错误率超过阈值自动熔断,半开探测恢复。
- 系统自适应保护:根据 CPU 负载、平均 RT 等自动调整入口流量。
- 动态规则推送:通过 Nacos/Apollo 实现规则热更新。
六、生产避坑指南(重点!)
6.1 Redis 单点故障与网络开销
问题:每次限流都要请求 Redis,网络 RTT 增加 ~0.5ms,高并发下 Redis 成为瓶颈。
解法:
- 使用 Redis Cluster 做高可用。
- 本地缓存配合批量模式(如每次请求拿 5 个令牌,消耗完再请求 Redis)。
- 降级方案:Redis 不可用时切换到单机限流(Guava RateLimiter),并打印告警。
6.2 Sentinel 集群模式的偏差
由于 Token Server 和 Client 之间通信是异步 Netty,存在毫秒级延迟,在严格秒杀场景下可能累计误差。建议:对极端精确场景,使用 Redis+Lua 并压测验证精度。
6.3 限流 Key 的设计与热点问题
- 不合理:
limiter:user(所有用户共享一个桶)→ 一个恶意用户能刷爆全局限流。 - 合理:
limiter:user:{userId}(按用户隔离),但要警惕海量用户时 Redis 内存爆炸。 - 解决方案:对用户限流可采用滑动窗口 + 布隆过滤器,仅对活跃用户动态创建 key,并设置 TTL。
6.4 限流后用户体验优化
- 返回明确的限流错误码(如 429)和 Retry-After 头部。
- 实现分级限流:VIP 用户阈值更高,普通用户阈值更低。
- 异步排队:对于写请求,限流后可放入队列稍后处理,而不是粗暴拒绝。
七、方案选型速查表
| 你的情况 | 推荐方案 |
|---|---|
| Redis 已部署,团队小,需要快速实现全局限流 | Redis + Lua |
| 已有 Sentinel 生态(Spring Cloud Alibaba),需要熔断、热点、系统自适应 | Sentinel 集群流控 |
| 非 Java 栈(Go/Python),统一流量治理 | Redis + Lua或基于 Envoy 的全局限流(RLS) |
| 超高并发(10万+ QPS),要求极致性能 | Netflix Concurrency Limits(极限并发控制)+ 本地滑动窗口,结合 Redis 做周期性同步 |
八、总结
分布式限流不是单一的算法或组件,而是一套根据业务场景权衡的“流量手术刀”。本文我们从原理切入,对比了 Redis+Lua 和 Sentinel 两种主流方案,并分别给出了完整的 Spring Boot 实战代码,以及生产中那些容易踩的坑。
- Redis+Lua:造轮子成本低,适合通用、灵活动态控制的场景。
- Sentinel:功能航母,适合 Java 生态的复杂治理需求。
记住:限流的本质是延迟拒绝,而不是系统崩溃。优雅地丢请求,比让用户等到超时更尊重用户体验。
最后,无论你选择哪条路,压测和监控永远是最好的老师。希望这篇文章能帮你构建起你的第一条“分布式护城河”。
📢 关注《卷毛的技术笔记》,专注后端硬核技术分享,拒绝套路,只聊落地的技术。
如果觉得文章对你有帮助,欢迎点赞、收藏、关注!
