一、背景与目标
在微服务架构中,网关作为流量入口,需要具备基础的防护能力。本文介绍一种基于 Redis + Lua 脚本的分布式 IP 限流方案,在网关层实现每个 IP 每秒最多 N 次请求的限制,防止恶意刷接口、暴力破解等行为。
核心目标
| 目标 | 说明 |
|---|
| 分布式限流 | 网关多节点共享计数,精确全局限流 |
| 高性能 | 单次 Redis 调用完成所有操作 |
| 原子性 | Lua 脚本保证无竞态条件 |
| 降级保护 | Redis 异常时自动放行,不影响业务 |
| 可观测 | 日志记录真实计数,反映攻击猛烈程度 |
二、整体架构
┌─────────────┐ │ 客户端 │ │ IP: 1.2.3.4│ └──────┬──────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Spring Cloud Gateway │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ IpRateLimitGlobalFilter │ │ │ │ ① 提取真实 IP(X-Forwarded-For 优先) │ │ │ │ ② 构造 Key: rate_limit:ip:{IP}:{秒时间戳} │ │ │ │ ③ 执行 Redis Lua 脚本(原子 INCR + EXPIRE) │ │ │ │ ④ 判断: count > limit ? 429 : 放行 │ │ │ └──────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Redis │ │ │ │ Key: rate_limit:ip:1.2.3.4:1734567890 │ │ Value: 3 (当前秒内该 IP 已请求 3 次) │ │ TTL: 1秒 │ └─────────────────────────────────────────────────────────────┘
三、核心实现
3.1 限流配置(application.yml)
rate-limit:ip:enabled:truelimit:30# 每秒最多允许 30 次请求
3.2 核心代码
配置类
@Component@ConfigurationProperties(prefix="rate-limit.ip")publicclassIpRateLimitProperties{privatebooleanenabled=true;privateintlimit=30;// getter / setter}
Lua 脚本(核心)
localkey=KEYS[1]localttl=tonumber(ARGV[2])-- 原子性自增localcurrent=redis.call('INCR',key)-- 首次访问设置过期时间ifcurrent==1thenredis.call('EXPIRE',key,ttl)end-- 返回真实计数returncurrent
限流过滤器
@ComponentpublicclassIpRateLimitGlobalFilterimplementsGlobalFilter,Ordered{privatestaticfinalintWINDOW_SECONDS=1;privatestaticfinalStringLUA_SCRIPT="local key = KEYS[1]\n"+"local ttl = tonumber(ARGV[2])\n"+"local current = redis.call('INCR', key)\n"+"if current == 1 then\n"+" redis.call('EXPIRE', key, ttl)\n"+"end\n"+"return current";privatefinalDefaultRedisScript<Long>limitScript=newDefaultRedisScript<>(LUA_SCRIPT,Long.class);@OverridepublicMono<Void>filter(ServerWebExchangeexchange,GatewayFilterChainchain){if(!rateLimitProperties.isEnabled()){returnchain.filter(exchange);}Stringip=getRealIp(exchange.getRequest());intlimit=rateLimitProperties.getLimit();Stringkey="rate_limit:ip:"+ip+":"+Instant.now().getEpochSecond();returnMono.from(redisTemplate.execute(limitScript,Collections.singletonList(key),String.valueOf(limit),String.valueOf(WINDOW_SECONDS))).flatMap(currentCount->{if(currentCount!=null&¤tCount>limit){log.warn("IP限流触发 - IP: {}, 计数: {}, 阈值: {}/s",ip,currentCount,limit);returnbuildTooManyRequestsResponse(exchange);}returnchain.filter(exchange);}).onErrorResume(e->{log.error("Redis异常,降级放行 - IP: {}",ip,e);returnchain.filter(exchange);});}@OverridepublicintgetOrder(){returnOrdered.HIGHEST_PRECEDENCE;// 最高优先级}}
四、为什么用 Lua 脚本?
| 方式 | 原子性 | Redis 调用次数 | 并发安全 |
|---|
| Java 两步操作(INCR + EXPIRE) | ❌ | 2 次 | 有竞态 |
| Redis 事务(MULTI + EXEC) | ⚠️ | 2 次 | 只保证顺序,不保证原子 |
| Lua 脚本 | ✅ | 1 次 | ✅ 单线程原子执行 |
关键点:Redis 执行 Lua 脚本时,整个脚本是一个原子操作,不会被其他命令打断。
五、关键设计决策
| 设计点 | 选择 | 原因 |
|---|
| 限流维度 | 按 IP | 最常见的防刷维度,区分不同客户端 |
| 窗口算法 | 固定窗口(1秒) | 实现最简单,性能最高,防刷够用 |
| 存储引擎 | Redis | 网关多节点共享计数,保证一致性 |
| Key 设计 | rate_limit:ip:{IP}:{秒时间戳} | 自然分片,自动过期,无需手动清理 |
| 返回值 | 真实计数 | 日志可观测攻击严重程度,方便调优 |
| 降级策略 | Redis 异常时放行 | 限流是辅助功能,不影响核心业务 |
六、压测验证
测试配置
rate-limit:ip:enabled:truelimit:30
JMeter 测试结果
| 线程数 | Ramp-Up | 预期放行 | 实际放行 | 结论 |
|---|
| 50 | 0s | 30 | 30 | ✅ 精准 |
| 30 | 0s | 30 | 30 | ✅ 精准 |
| 10 | 1s | ~8(跨窗口) | 8 | ✅ 合理 |
运行日志
DEBUG IP限流检查 - IP: 127.0.0.1, Key: rate_limit:ip:127.0.0.1:1734567890, Limit: 30/s WARN IP限流触发 - IP: 127.0.0.1, 当前计数: 31, 阈值: 30/s WARN IP限流触发 - IP: 127.0.0.1, 当前计数: 45, 阈值: 30/s
七、注意事项
7.1 Nginx 需透传真实 IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr;
7.2 Redis 连接池配置
spring:redis:host:${REDIS_HOST}port:6379timeout:2000mslettuce:pool:max-active:20max-idle:10min-idle:5
7.3 限流阈值调优
| 场景 | 建议阈值 | 说明 |
|---|
| 普通 API | 30-50/s | 覆盖正常用户浏览行为 |
| 登录/注册接口 | 3-5/s | 防暴力破解 |
| 批量查询接口 | 50-100/s | 列表加载等场景 |
八、方案优缺点
✅ 优点
- 极简高效:一个 Filter + 一个 Lua 脚本搞定
- 分布式支持:Redis 共享计数,网关集群一致
- 原子性保障:Lua 脚本保证无竞态
- 降级保护:Redis 异常不影响业务
- 可观测性强:日志记录真实计数
⚠️ 局限
- 固定窗口边界问题:1 秒末 + 2 秒初允许 2 倍流量(防刷场景可接受)
- 依赖 Redis:需要 Redis 高可用保障
- 单维度限流:仅按 IP 限流,如需多维度需扩展
九、总结
本文介绍了一种生产级分布式 IP 限流方案,核心思想是:
在网关层用 Redis + Lua 做原子计数,每个 IP 每秒独立计数,超过阈值返回 429。