导读:双十一零点,流量瞬间暴涨 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
-- 令牌桶限流 Lua 脚本
-- KEYS[1] : 桶的唯一 key
-- ARGV[1] : 最大令牌容量 capacity
-- ARGV[2] : 令牌生成速率 rate (每秒几个)
-- ARGV[3] : 当前请求需要的令牌数 (通常为1)
-- ARGV[4] : 当前时间戳(秒)
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local requested = tonumber(ARGV[3])
local now = tonumber(ARGV[4])
-- 获取桶的当前状态 {last_refresh_time, current_tokens}
local bucket = redis.call('hmget', key, 'last_time', 'tokens')
local last_time = tonumber(bucket[1]) or now
local tokens = tonumber(bucket[2]) or capacity
-- 计算应该补充的令牌数
local delta = math.max(0, now - last_time)
local filled_tokens = math.min(capacity, tokens + (delta * rate))
-- 判断是否足够
local allowed = 0
if filled_tokens >= requested then
allowed = 1
filled_tokens = filled_tokens - requested
end
-- 保存新状态
redis.call('hmset', key, 'last_time', now, 'tokens', filled_tokens)
-- 设置过期时间,避免闲置 key 浪费内存(2倍时间窗口)
redis.call('expire', key, 60)
return allowed
4.3 Spring Boot 中集成 Redis + Lua
① 引入依赖
xml
<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 脚本
java
@Component
public class RedisRateLimiter {
private final RedisScript<Long> rateLimitScript;
public RedisRateLimiter(RedisTemplate<String, Object> redisTemplate) {
// 读取 classpath 下的 lua 文件
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/token_bucket.lua")));
script.setResultType(Long.class);
this.rateLimitScript = script;
this.redisTemplate = redisTemplate;
}
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public boolean tryAcquire(String key, long capacity, double rate, int requested) {
List<String> keys = Collections.singletonList(key);
Long result = redisTemplate.execute(
rateLimitScript,
keys,
String.valueOf(capacity),
String.valueOf(rate),
String.valueOf(requested),
String.valueOf(System.currentTimeMillis() / 1000)
);
return result != null && result == 1L;
}
}
③ 自定义注解 + AOP 实现无侵入限流
java
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedRateLimit {
String key(); // 限流 key,支持 SpEL
long capacity() default 100;
double rate() default 10; // 每秒生成令牌数
int requested() default 1;
}
切面实现(省略部分校验代码):
java
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint joinPoint, DistributedRateLimit rateLimit) throws Throwable {
String key = parseKey(rateLimit.key(), joinPoint); // 支持 SpEL 解析,如 #userId
boolean allowed = redisRateLimiter.tryAcquire(key, rateLimit.capacity(), rateLimit.rate(), rateLimit.requested());
if (!allowed) {
throw new RateLimitException("请求过于频繁,请稍后再试");
}
return joinPoint.proceed();
}
④ 业务处使用
java
@GetMapping("/order")
@DistributedRateLimit(key = "order:create:#{#userId}", capacity = 50, rate = 10)
public String createOrder(@RequestParam Long userId) {
return "订单创建成功";
}
效果 :无论多少个实例,同一个 userId 的请求被全局限制在 10 QPS,且允许短时突发。
五、实战二:阿里 Sentinel 集群流控(更适合生产)
Sentinel 提供两种集群模式:
- 嵌入模式(Embedded) :选一个节点作为 Token Server,其他为 Client(适合小规模)
- 独立模式(Alone) :独立 Token Server 集群(适合大规模)
5.1 搭建 Sentinel 控制台
bash
docker run -d --name sentinel -p 8858:8858 -p 8719:8719 bladex/sentinel-dashboard:1.8.6
访问 http://localhost:8858,默认账号 sentinel/sentinel。
5.2 Spring Boot 集成 Sentinel
① 依赖
xml
<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:
yaml
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8858
cluster-client:
server-host: your-token-server-ip # Token Server 地址
server-port: 18730
flow:
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 生态的复杂治理需求。
记住:限流的本质是延迟拒绝,而不是系统崩溃。优雅地丢请求,比让用户等到超时更尊重用户体验。
最后,无论你选择哪条路,压测和监控永远是最好的老师。希望这篇文章能帮你构建起你的第一条"分布式护城河"。
📢 关注 《卷毛的技术笔记》 ,专注后端硬核技术分享,拒绝套路,只聊落地的技术。
如果觉得文章对你有帮助,欢迎点赞、收藏、关注!