分布式限流 = Redis 全局计数 + Lua 脚本保证原子性
- Redis:存全局计数器,所有服务共享
- Lua脚本:**保证原子性,**判断 + 计数 + 过期一步原子操作完成,避免并发计数错误
- 固定窗口算法(最简单、企业最常用)
注:Redis实现分布式限流是强一致性,因为:
-
**Redis 是单线程执行命令,**同一时间只有一个请求在执行计数、判断逻辑。
-
限流逻辑全部封装在 Lua 脚本里, 判断 + 计数 + 过期 是一个原子操作,不可分割。
-
**集群所有机器都访问同一个 Redis,**全局唯一计数,没有本地计数,不会出现误差
一、实现方案
1. Redis + Lua 固定窗口限流
- 原理
- 每个限流 key(如接口名、IP、用户 ID)在 Redis 里计数
- 1 秒 / 1 分钟内最多允许 N 个请求
- 超过就拒绝,没超过就计数 + 1
- 适用场景:简单、高效、通用的分布式限流
2. 滑动窗口限流(更精准)
把1 秒拆成多个小时间片 ,只统计最近 1 秒内的所有请求,而不是按整秒重置。
- 原理
-
用 ZSet 存储请求
key= 限流 keymember= 唯一请求 ID(UUID / 时间戳 + 随机数)score= 当前时间戳(毫秒)
-
每次请求做(原子):
- 移除窗口外的旧数据
- 统计窗口内总请求数
- 判断是否超过阈值
- 添加当前请求
-
全部用 Lua 脚本 原子执行。
-
适用场景:精准度高,适合高并发核心接口
-
优点
- 无临界突刺(比如最后 100ms 突增 100 请求),精准控制 QPS
- 分布式全局精准限流
- Redis + Lua 原子实现
二、Spring Boot 实现固定窗口
1. 依赖
spring-boot-starter-data-redis
2. 固定窗口
- 工具类
java
@Component
public class RedisRateLimit {
@Autowired
private StringRedisTemplate redisTemplate;
private final DefaultRedisScript<Long> SCRIPT;
public RedisRateLimit() {
SCRIPT = new DefaultRedisScript<>();
SCRIPT.setResultType(Long.class);
SCRIPT.setScriptText("local count = redis.call('incr',KEYS[1]);\n" +
"if count == 1 then redis.call('expire',KEYS[1],ARGV[1]); end\n" +
"if count > tonumber(ARGV[2]) then return 0 else return 1 end");
}
/**
* 分布式限流
* @param key 限流key
* @param seconds 窗口时间
* @param maxCount 最大请求数
* @return true=放行 false=限流
*/
public boolean tryAcquire(String key, int seconds, int maxCount) {
Long res = redisTemplate.execute(
SCRIPT,
Collections.singletonList(key),
String.valueOf(seconds),
String.valueOf(maxCount)
);
return res != null && res == 1;
}
}
- 使用
java
@RestController
public class TestController {
@Autowired
private RedisRateLimit redisRateLimit;
@GetMapping("/api/test")
public String test() {
// 1秒内最多 10 个请求
boolean allow = redisRateLimit.tryAcquire("limit:api:test", 1, 10);
if (!allow) {
return "系统繁忙,请稍后再试";
}
return "请求成功";
}
}
3. 滑动窗口
- 工具类
java
@Component
public class RedisSlideWindowLimiter {
private final StringRedisTemplate stringRedisTemplate;
private final RedisScript<Long> script;
public RedisSlideWindowLimiter(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
script = new DefaultRedisScript<>();
script.setResultType(Long.class);
script.setScriptText(
"redis.call('ZREMRANGEBYSCORE',KEYS[1],0,tonumber(ARGV[3])-tonumber(ARGV[1]));\n"
+ "local count=redis.call('ZCARD',KEYS[1]);\n"
+ "if tonumber(count)>=tonumber(ARGV[2]) then return 0 end;\n"
+ "redis.call('ZADD',KEYS[1],tonumber(ARGV[3]),tonumber(ARGV[3]));\n"
+ "redis.call('EXPIRE',KEYS[1],tonumber(ARGV[1])/1000+1);\n"
+ "return 1"
);
}
/**
* 滑动窗口限流
* @param key 限流key
* @param windowMs 窗口大小(毫秒)
* @param maxCount 最大请求数
* @return 是否放行
*/
public boolean tryAcquire(String key, long windowMs, int maxCount) {
long now = System.currentTimeMillis();
Long result = stringRedisTemplate.execute(
script,
Collections.singletonList(key),
String.valueOf(windowMs),
String.valueOf(maxCount),
String.valueOf(now)
);
return result != null && result == 1;
}
}
- 使用
java
@RestController
public class TestController {
@Autowired
private RedisSlideWindowLimiter slideWindowLimiter;
@GetMapping("/api/test")
public String test() {
// 滑动窗口:1秒内最多10个请求(精准无突刺)
boolean allow = slideWindowLimiter.tryAcquire("limit:api:test", 1000, 10);
if (!allow) {
return "限流了,请稍后再试";
}
return "请求成功";
}
}
三、关键说明
- 为什么用 Redis?
- 提供全局共享计数器
- 性能极高,适合高并发
- 为什么必须用 Lua?
- 不用 Lua:多台服务同时请求,会出现计数不准
- 用 Lua:Redis 单线程执行,原子性,绝对不会超发
- 可以限流哪些维度?
- 接口维度:
limit:api:order - IP 维度:
limit:ip:192.168.1.1 - 用户维度:
limit:uid:1001
- 限流 key 怎么设计?
- 接口维度:
rate_limit:api:getOrder - IP 维度:
rate_limit:ip:192.168.1.1 - 用户维度:
rate_limit:uid:1001
- 为什么不用 Java 直接 get/set?
- get + increment 不是原子操作,集群并发下会超限额。