基于 Redis +Lua+ ZooKeeper 的轻量级内嵌式限流
一、一句话说清楚:我们要做什么?
用 Redis + Lua + ZooKeeper,在不改业务代码的前提下,给你的接口加上"智能红绿灯"------该放行时秒过,该拦截时果断拒绝。
本方案基于 Spring Boot Starter 封装,通过 AOP/Filter 拦截请求,结合 Redis + Lua实现高性能分布式限流,ZooKeeper 动态管理规则,真正做到:
- 零侵入:业务代码加个注解就行;
- 实时生效:规则改完立刻生效,不用重启;
- 高可用兜底:Redis 或 ZK 挂了也不影响主业务;
- 细粒度控制:支持全局、接口、客户端等多维度限流。
- 分布式限流: 基于 Redis 的
INCR+EXPIRE+原子操作(Lua)实现分布式限流(秒级并发、自定义时间窗口)。
二、限流流程图

✅ 所有核心逻辑都在应用进程内完成,无额外网关层,低延迟、低运维成本。
📊 流程图说明:请求限流处理流程
本方案中一次 HTTP 请求从进入系统到最终执行业务逻辑的完整限流处理流程,体现了 "拦截 → 判断 → 决策 → 执行" 的核心链路。
-
HTTP 请求到达
- 客户端发起 HTTP 请求,进入服务端。
- 请求首先进入统一入口(如 Servlet Filter 或 Spring AOP 切面)。
-
Filter / AOP 拦截器触发
- 系统通过
Filter(全局 HTTP 层)或@RateLimited注解驱动的AOP(方法级)进行拦截。 - 根据请求路径、客户端 IP、用户 ID 等信息,确定是否需要限流。
- 系统通过
-
读取内存中的限流规则
- 从本地 JVM 内存中获取当前生效的限流规则(如 QPS、时间窗口等)。
- 规则由 ZooKeeper 动态推送并实时更新,应用通过 Curator 监听节点变化,确保配置变更秒级生效。
-
调用 Redis 执行限流判断
- 使用 Redis 的
INCR + EXPIRE原子操作(通过 Lua 脚本保证一致性),对当前请求进行计数与判断。 - 若请求数未超阈值 → 放行;否则 → 拒绝。
- 使用 Redis 的
-
决策执行 + 日志记录
- 放行:继续执行后续业务逻辑;
- 拒绝:返回 429(Too Many Requests)状态码,并记录结构化日志(含时间、IP、接口、结果等);
- 日志异步写入(如 RocketMQ),避免阻塞主线程。
-
正常执行业务逻辑
- 若通过限流检查,请求进入业务方法,完成正常处理流程。
⚙️ 关键设计亮点
| 步骤 | 设计优势 |
|---|---|
| ZooKeeper 动态推送规则 | 配置无需重启,支持灰度发布和快速调整 |
| 内存缓存规则 | 即使 ZK 宕机,仍可用最后有效规则,保障高可用 |
| Redis 原子操作 | 分布式环境下精准计数,无并发问题 |
| 日志异步记录 | 不影响主流程性能,便于监控分析 |
✅ 总结一句话:
一个请求,三步走:拦截 → 查规则 → 判限流,全程零侵入、实时生效、高可用。
三、技术架构图

四、双层限流架构图

本图展示了系统采用的双层流量控制机制:
- 全局级流控(Filter 层) :所有 HTTP 请求首先进入 Filter,基于全局规则(如总 QPS)进行初步限流判断,使用 Redis 原子操作实现分布式计数;
- 接口级流控(Service 层) :若未被全局拦截,则进入业务方法,通过
@RateLimited注解解析接口级规则,进行更细粒度的限流控制; - 限流规则由 ZooKeeper 动态推送并实时生效,Redis 保证高并发下的计数一致性;
- 任一层触发限流即返回拒绝响应,避免资源过载,保障服务稳定性。
三、为什么不用网关?我们选择"内嵌式限流"
市面上常见的限流实现主要有两类:独立网关型 和 内嵌式组件型。在深入评估后,决定自研轻量级内嵌方案,原因如下:
1. 网关太"重":功能过剩,性能多一跳
像 Spring Cloud Gateway 这类微服务网关,虽然内置限流能力,但它本质是一个全能型流量中枢,集成了路由、鉴权、协议转换、日志审计等大量功能。
❌ 对我们当前"只做限流"的需求来说,引入网关就像为了装个门铃而重建整栋楼------成本高、链路长、运维复杂。 ✅ 内嵌方案直接在业务进程内完成判断,仅需一次 Redis 调用,延迟更低、路径更短、风险更小。
2. 开源组件如 Sentinel:功能强大,但"杀鸡用牛刀"
阿里开源的 Sentinel 是业界优秀的内嵌式流量治理框架,支持限流、熔断、系统自适应保护等高级能力。
但我们发现:
- Sentinel 是产品级平台,包含控制台、指标采集、规则持久化、集群流控等模块;
- 要完整发挥其能力,需额外部署 Dashboard、对接数据源、维护心跳上报等,运维和集成成本较高;
- 而我们的核心诉求非常明确:只需基础的分布式限流 + 动态规则更新,并不需要复杂的熔断联动或实时监控大盘。
🛠️ 与其引入一个"瑞士军刀",不如打造一把趁手的"水果刀"------轻量、专注、易维护。
3. 自研内嵌方案的优势
| 维度 | 网关方案 | Sentinel 等开源方案 | 本方案(自研内嵌) |
|---|---|---|---|
| 部署复杂度 | 高(需独立集群) | 中(需控制台+Agent) | 低(仅依赖 Redis+ZK) |
| 侵入性 | 无(但链路变长) | 低(需埋点/注解) | 零侵入(Starter + 注解) |
| 控制粒度 | URL/Header 级 | 方法/资源级 | 方法级 + 上下文(IP/用户ID) |
| 规则生效速度 | 依赖网关刷新 | 秒级(需推送) | 秒级(ZK Watcher 实时监听) |
| 可维护性 | 公司级统一维护 | 社区+自维护 | 团队自主可控,代码透明 |
💡 结论 :当限流需求简单、稳定、且希望快速迭代时,轻量自研 > 重型开源 > 网关集成。
四、核心技术实现
4.1 Redis 限流:用 Lua 脚本守住"时间窗口"
我们使用 Redis 的 INCR + EXPIRE 原子操作(通过 Lua 脚本保证),实现固定时间窗口限流。若返回值 > 阈值,则返回false,拒绝请求。
java
import redis.clients.jedis.Jedis;
import java.util.Collections;
public class RateLimiter {
private final Jedis jedis;
private final String rateLimitKeyPrefix = "rate_limit:"; // Redis key 前缀
private final int windowSizeInSeconds; // 时间窗口大小(秒)
private final long limit; // 允许的最大请求数
public RateLimiter(String redisHost, int redisPort, int windowSizeInSeconds, long limit) {
this.jedis = new Jedis(redisHost, redisPort);
this.windowSizeInSeconds = windowSizeInSeconds;
this.limit = limit;
}
/**
* 尝试访问资源,如果超过限制则返回 false。
*/
public boolean tryAccess(String identifier) {
String key = rateLimitKeyPrefix + identifier;
long currentTime = System.currentTimeMillis() / 1000L; // 转换为秒级时间戳
// 使用 Lua 脚本来保证原子性操作
String script =
"local current\n" +
"current = redis.call('incr', KEYS[1])\n" +
"if tonumber(current) == 1 then\n" +
" redis.call('expire', KEYS[1], ARGV[1])\n" +
"end\n" +
"return current";
Object result = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(windowSizeInSeconds)));
if (result instanceof Long) {
return (Long) result <= limit;
} else {
throw new RuntimeException("Unexpected response type from Redis");
}
}
public void close() {
jedis.close();
}
public static void main(String[] args) {
RateLimiter limiter = new RateLimiter("localhost", 6379, 60, 5); // 每分钟最多5次请求
String userIdentifier = "user:123"; // 用户标识符或IP地址等
for (int i = 0; i < 7; i++) { // 尝试发起7次请求
boolean allowed = limiter.tryAccess(userIdentifier);
System.out.println("Attempt " + (i + 1) + ": " + (allowed ? "Allowed" : "Denied"));
}
limiter.close();
}
}
- 若返回值 ≤ 阈值 → 放行;
- 若 > 阈值 → 拒绝(返回 429)。
4.2 ZooKeeper:动态规则中心
所有限流规则存于 ZK 节点,结构如下:
bash
/rate-limit/config
├── /global → {"limit": 1000, "windowSeconds": 60}
├── /client/clientA → {"limit": 100, ...}
└── /api/user_get → {"limit": 20, "enabled": true}
应用启动时加载规则,并通过 Curator 监听节点变化 ,规则更新后秒级生效,无需重启。
🔒 即使 ZK 宕机,应用仍使用最后一次加载的规则,不影响服务运行。
4.3 零侵入集成:Starter + 注解 + AOP
(1)业务只需加注解:
less
@GetMapping("/user/info")
@RateLimited(value = "api:/user/info", fallbackCode = 429)
public User getUserInfo() {
return userService.get();
}
(2)框架自动拦截:
- AOP 切面监听
@RateLimited注解; - 自动拼接限流 Key(如
api:/user/info:ip:192.168.1.100); - 查询规则 → 调 Redis → 决策放行或拒绝。
🌟 同时支持 Filter 全局拦截 (如对所有
/api/**统一限流)。
4.4 容错兜底:系统挂了也不能拖垮业务!
| 故障组件 | 应对策略 |
|---|---|
| Redis 不可用 | 可配置降级策略: • Fail-open:放行(非关键接口) • Fail-close:拒绝(关键接口) → 同时记录告警日志 |
| ZooKeeper 不可用 | 使用内存中缓存的最后有效规则,继续工作 |
✅ 原则:限流是辅助功能,不能成为单点故障源。
4.5 日志与可观测性
每次限流决策都会记录结构化日志,示例:
json
{
"timestamp": "2025-11-14T18:30:00",
"ruleKey": "api:/user/info",
"clientIp": "192.168.1.100",
"userId": "u12345",
"result": "BLOCKED"
}
- 日志通过 SLF4J + Logback 输出;
- 可接入 ELK、SLS 等日志平台;
- 关键事件可异步投递 RocketMQ,避免阻塞主线程。
五、使用方式:三步搞定
-
引入 Starter 依赖
xml<dependency> <groupId>com.yourcompany</groupId> <artifactId>rate-limit-starter</artifactId> </dependency> -
配置 Redis & ZooKeeper 地址
yamlrate-limit: redis-host: localhost zk-connect: zk1:2181,zk2:2181 -
在方法上加注解
typescript@RateLimited("api:/order/create") public Order createOrder(...) { ... }
✅ 完成!无需改动一行业务逻辑。
六、总结:轻量、可靠、易用
| 优势 | 说明 |
|---|---|
| 轻量级 | 无独立部署,无额外网络跳转 |
| 高扩展 | 支持全局/接口/客户端多维度限流 |
| 强实时 | ZK 推送,规则秒级生效 |
| 高可用 | Redis/ZK 故障自动降级 |
| 零侵入 | 注解驱动,业务无感知 |
🚦 让流量有序通行,而不是一拥而上------这就是我们给每个接口装上的"智能红绿灯"。
当前方案比较简单轻量,如需进一步扩展(如滑动窗口、令牌桶、熔断联动等),可在当前架构上平滑演进。