Redis限流
在高并发系统的稳定性保障体系中,限流是与熔断、降级并列的核心防护手段。当流量突破服务承载上限时,限流能精准“削峰填谷”,避免数据库雪崩、应用服务器过载,成为保障业务连续性的“最后一道防线”。
Redis 凭借原子操作特性、分布式一致性支持、毫秒级响应速度,成为限流方案的首选技术栈——其性能可达单机 10 万+ QPS,远超数据库和应用本地限流的承载能力,广泛应用于秒杀抢购、API 网关、第三方接口调用等核心场景。
# 一、限流核心认知:不止是“计数”,更是流量治理的艺术
# 1.1 限流的本质目标
限流并非简单“拒绝请求”,而是通过科学的流量控制,实现三大核心目标:
- 保护系统稳定性:防止突发流量(如秒杀、爬虫攻击)压垮服务,确保核心业务可用;
- 保障用户体验:避免因系统过载导致的全量响应超时,仅牺牲部分非核心请求;
- 合理利用资源:使服务器 CPU、内存、网络等资源处于最优负载区间,避免浪费。
# 1.2 限流的核心衡量指标
设计限流方案前,需明确三个关键指标,避免无的放矢:
- 限流粒度:接口级(如
/api/pay)、用户级(如user:1001:/api/order)、IP 级(如ip:192.168.1.1)、商品级(如seckill:goods:10086); - 时间窗口:1 秒、10 秒、1 分钟、1 天(需匹配业务场景,如秒杀用 1 秒窗口,第三方 API 调用用 1 天窗口);
- 限流阈值:单位时间内允许的最大请求数(需通过压测确定,通常设为服务峰值承载能力的 80%,预留缓冲)。
# 1.3 Redis 限流的核心优势
相较于本地限流(如 Guava RateLimiter)、网关限流(如 Nginx),Redis 限流具备不可替代的优势:
- 分布式一致性:多节点、多服务部署时,能保证全局限流阈值统一(如多实例部署的应用,共同遵守“1 秒 100 次请求”规则);
- 高性能:内存操作 + 原子命令,响应时间 < 1ms,支持每秒 10 万+ 限流请求;
- 灵活性:支持多种限流算法,可通过 Lua 脚本扩展自定义逻辑;
- 可扩展性:结合 Redis Cluster 可横向扩展,突破单机性能瓶颈。
# 二、经典限流算法深度解析:原理+数学模型+适用场景
四种经典限流算法是 Redis 限流的基础,其核心差异在于“流量控制的灵活性”和“抗突发能力”。以下从数学模型、数据结构选择、底层逻辑三个维度,彻底讲透每种算法的设计思路。
# 2.1 固定窗口计数器:最简单的“计数限流”
# 核心原理
将时间划分为固定长度的窗口(如 1 秒),用 Redis 的 INCR 命令原子计数,窗口结束后计数器自动重置。数学模型可表示为:
count (t) ≤ threshold, 其中 t ∈ [k*window, (k+1)*window)
count(t):时间 t 所在窗口的请求数;threshold:窗口内最大请求数;window:窗口时长。
# 数据结构选择
- 采用 Redis String 类型存储计数器,key 为“限流粒度+窗口标识”(如
limiter:fixed:/api/pay:1690000000); - 利用
EXPIRE命令设置窗口过期时间,避免手动清理。
# 底层执行流程
- 新请求到来时,计算当前窗口标识(如时间戳整除窗口时长:
System.currentTimeMillis() / 1000); - 拼接 Redis key:
limiter:fixed:{key}:{windowId}; - 执行
INCR命令递增计数器,若为第一次递增则设置EXPIRE过期时间; - 若计数器值 > 阈值则限流,否则允许通过。
# 优缺点与适用场景
- 优点:实现最简单、Redis 操作最少(2 个命令)、性能最高(单机 QPS 可达 50 万+);
- 缺点:存在临界问题——窗口切换时可能出现“双倍流量”(如 0.9 秒到 1.1 秒跨两个窗口,共通过 2*threshold 次请求);
- 适用场景:对流量平滑性要求低的场景(如普通接口限流、非核心业务防护)。
# 2.2 滑动窗口计数器:解决临界问题的“精准限流”
# 核心原理
将固定窗口拆分为 N 个连续的小窗口(如 1 秒拆分为 10 个 100ms 小窗口),通过滑动窗口的方式计算单位时间内的请求数。数学模型可表示为:
count (t) = Σ count (t-i*subWindow) ,其中 i=0 到 N-1,subWindow = window/N
subWindow:小窗口时长;- N:小窗口数量(N 越大,流量控制越精准,但性能开销越高)。
# 数据结构选择
- 采用 Redis ZSET 类型,score 为请求时间戳,value 为唯一标识(如 UUID);
- ZSET 天然支持按 score 范围删除和计数,完美适配滑动窗口的“删除过期请求+统计当前请求数”需求。
# 底层执行流程
- 新请求到来时,获取当前时间戳
currentTime; - 计算窗口起始时间:
windowStartTime = currentTime - window*1000; - 执行
ZREMRANGEBYSCORE删除 ZSET 中 score <windowStartTime的过期请求; - 执行
ZCARD统计当前窗口内的请求数,若 >= 阈值则限流; - 若允许通过,执行
ZADD将当前请求的时间戳和 UUID 加入 ZSET; - 设置 ZSET 过期时间为
window+1秒,避免垃圾数据堆积。
# 优缺点与适用场景
- 优点:解决固定窗口的临界问题,流量控制精准,支持任意时间粒度的窗口;
- 缺点:Redis 操作较多(3 个命令),高并发下性能略低于固定窗口(单机 QPS 约 30 万+);
- 适用场景:对流量平滑性有要求的核心场景(如电商商品详情页、支付接口限流)。
# 2.3 漏桶算法:严格控制输出速率的“平稳限流”
# 核心原理
将请求比作“水流”,漏桶的容量为最大并发等待数,漏桶的“漏水速率”为单位时间允许通过的请求数。数学模型可表示为:
inRate ≤ outRate, 且 queueSize ≤ capacity
inRate:请求流入速率;outRate:请求流出速率(服务承载速率);queueSize:当前排队请求数;capacity:漏桶容量(最大排队数)。
# 数据结构选择
- 采用 Redis LIST 类型作为漏桶,存储待处理的请求(可存入请求 ID、时间戳等元数据);
- 结合 Redis 定时任务(或外部定时任务)实现“漏水”逻辑。
# 底层执行流程
- 新请求到来时,执行
LLEN统计 LIST 长度,若 < 容量则执行RPUSH入队; - 定时任务按
outRate速率执行LPOP出队,触发业务逻辑处理; - 若 LIST 长度 >= 容量则限流,返回“请求过于频繁”。
# 优缺点与适用场景
- 优点:严格控制请求输出速率,避免服务因突发流量过载,适用于下游服务性能有限的场景;
- 缺点:抗突发能力差(即使漏桶为空,突发流量也需排队),需额外维护定时任务;
- 适用场景:数据库写入限流、第三方 API 调用频率控制(如微信支付 API 每天限 1 万次)。
# 2.4 令牌桶算法:平衡灵活性与抗突发的“最优解”
# 核心原理
系统按固定速率向令牌桶中放入令牌,桶的最大容量为阈值;新请求需获取 1 个令牌才能通过,无令牌则限流。数学模型可表示为:
tokenCount(t) = min(capacity, tokenCount(t0) + (t - t0)*refillRate)
tokenCount(t):时间 t 时的令牌数;capacity:令牌桶容量;refillRate:令牌填充速率(单位:个/秒);t0:上次填充令牌的时间。
# 数据结构选择
- 采用 Redis HASH 类型存储令牌桶状态:
{ "lastRefillTime": 上次填充时间戳, "tokenCount": 当前令牌数 }; - HASH 支持原子更新多个字段,且存储紧凑,适合高频读写。
# 底层执行流程
- 新请求到来时,获取当前时间戳
currentTime; - 执行
HGETALL获取令牌桶状态,若不存在则初始化(tokenCount=capacity,lastRefillTime=currentTime); - 计算时间差
timeDiff = currentTime - lastRefillTime,补充令牌:addTokens = timeDiff * refillRate / 1000,更新tokenCount = min(tokenCount + addTokens, capacity); - 若
tokenCount > 0则令牌数减 1,允许通过;否则限流; - 执行
HMSET更新令牌桶状态,设置过期时间避免垃圾数据。
# 优缺点与适用场景
- 优点:兼顾灵活性(固定填充速率)和抗突发(令牌预存),适配大多数高并发场景;
- 缺点:实现略复杂,需处理令牌填充的时间差计算;
- 适用场景:秒杀、抢购、首页核心接口等对流量平滑性和抗突发能力均有要求的场景。
# 四种算法核心对比(生产选型参考)
| 算法 | 数学模型复杂度 | 单机 QPS 上限 | 抗突发能力 | 流量平滑性 | 存储开销 | 适用场景 |
|---|---|---|---|---|---|---|
| 固定窗口计数器 | 低 | 50 万+ | 一般 | 差 | 极低 | 普通接口、非核心业务 |
| 滑动窗口计数器 | 中 | 30 万+ | 一般 | 中 | 中 | 核心 API、商品详情页 |
| 漏桶算法 | 中 | 20 万+ | 差 | 极高 | 中高 | 数据库写入、第三方 API 调用 |
| 令牌桶算法 | 中高 | 25 万+ | 极高 | 中高 | 中 | 秒杀、抢购、高并发核心业务 |
# 三、Redis 限流生产级实现:架构设计+代码落地
# 3.1 整体架构设计(可扩展、易集成)
为了适配不同业务场景,采用“工厂模式+注解驱动”的设计思路,核心架构如下:
- 限流算法工厂:封装四种算法的实现,支持按类型动态选择;
- 自定义注解:通过
@RedisLimit注解快速集成到接口,无需侵入业务代码; - Lua 脚本原子化:所有算法均通过 Lua 脚本实现,避免竞态条件;
- 降级策略:Redis 异常时降级为本地限流(Guava RateLimiter),保证服务可用性。
# 3.2 前置准备:环境配置与依赖引入
# 1. Maven 依赖
<!-- Spring Boot 核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Redis 连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- Guava 本地限流(降级用) -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.2-jre</version>
</dependency>
<!-- AOP 依赖(注解驱动) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 2. Redis 配置(application.yml)
spring:
redis:
host: 127.0.0.1
port: 6379
password: # 生产环境必填
lettuce:
pool:
max-active: 64 # 最大连接数(按并发量调整)
max-idle: 32 # 最大空闲连接
min-idle: 8 # 最小空闲连接
max-wait: 3000ms # 连接等待时间
timeout: 2000ms # 连接超时时间
redis-limiter:
default-window-seconds: 1 # 默认窗口时长(秒)
default-threshold: 100 # 默认限流阈值
fallback-enabled: true # 启用降级策略
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 3. RedisTemplate 优化配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Key 序列化(String 序列化,避免乱码)
StringRedisSerializer stringSerializer = new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
// Value 序列化(JSON 序列化,支持对象存储)
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
// 开启事务支持(可选,根据业务需求)
template.setEnableTransactionSupport(true);
template.afterPropertiesSet();
return template;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 3.3 核心组件实现
# 1. 限流算法枚举(统一标识)
public enum LimitAlgorithm {
FIXED_WINDOW, // 固定窗口计数器
SLIDING_WINDOW, // 滑动窗口计数器
LEAKY_BUCKET, // 漏桶算法
TOKEN_BUCKET // 令牌桶算法
}
2
3
4
5
6
# 2. 自定义注解(@RedisLimit)
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLimit {
/**
* 限流键(支持 SpEL 表达式,如 "#userId + ':' + '/api/order'")
*/
String key() default "";
/**
* 限流算法类型
*/
LimitAlgorithm algorithm() default LimitAlgorithm.TOKEN_BUCKET;
/**
* 窗口时长(秒)
*/
int windowSeconds() default 1;
/**
* 限流阈值(窗口内最大请求数)
*/
int threshold() default 100;
/**
* 漏桶算法专用:桶容量
*/
int bucketCapacity() default 10;
/**
* 漏桶/令牌桶专用:速率(个/秒)
*/
int rate() default 5;
/**
* 限流提示信息
*/
String message() default "请求过于频繁,请稍后再试";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 3. 限流算法工厂与实现(Lua 脚本原子化)
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* Redis 限流算法工厂(封装四种核心算法)
*/
@Component
public class RedisLimitFactory {
@Resource
private RedisTemplate<String, Object> redisTemplate;
// 固定窗口计数器 Lua 脚本(原子执行 INCR + EXPIRE + 阈值判断)
private static final String FIXED_WINDOW_SCRIPT = """
local key = KEYS[1]
local windowSeconds = tonumber(ARGV[1])
local threshold = tonumber(ARGV[2])
-- 原子递增计数器
local count = redis.call('incr', key)
-- 第一次递增时设置过期时间
if count == 1 then
redis.call('expire', key, windowSeconds)
end
-- 返回是否允许通过(1 允许,0 限流)
return count <= threshold and 1 or 0
""";
// 滑动窗口计数器 Lua 脚本
private static final String SLIDING_WINDOW_SCRIPT = """
local key = KEYS[1]
local windowSeconds = tonumber(ARGV[1])
local threshold = tonumber(ARGV[2])
local currentTime = tonumber(ARGV[3])
local windowStartTime = currentTime - windowSeconds * 1000
-- 删除过期的请求(时间戳 < 窗口起始时间)
redis.call('zremrangebyscore', key, 0, windowStartTime)
-- 统计当前窗口内的请求数
local count = redis.call('zcard', key)
if count >= threshold then
return 0 -- 限流
end
-- 加入当前请求(value 用 UUID 避免重复)
redis.call('zadd', key, currentTime, ARGV[4])
-- 设置过期时间(窗口时长 + 1 秒,避免垃圾数据)
redis.call('expire', key, windowSeconds + 1)
return 1 -- 允许通过
""";
// 令牌桶算法 Lua 脚本
private static final String TOKEN_BUCKET_SCRIPT = """
local key = KEYS[1]
local capacity = tonumber(ARGV[1]) -- 桶容量
local refillRate = tonumber(ARGV[2]) -- 填充速率(个/秒)
local currentTime = tonumber(ARGV[3])
-- 获取令牌桶状态,不存在则初始化
local bucket = redis.call('hgetall', key)
local lastRefillTime = currentTime
local tokenCount = capacity
if #bucket > 0 then
for i = 1, #bucket, 2 do
if bucket[i] == 'lastRefillTime' then
lastRefillTime = tonumber(bucket[i+1])
elseif bucket[i] == 'tokenCount' then
tokenCount = tonumber(bucket[i+1])
end
end
end
-- 计算应补充的令牌数
local timeDiff = currentTime - lastRefillTime
local addTokens = timeDiff * refillRate / 1000
if addTokens > 0 then
tokenCount = math.min(tokenCount + addTokens, capacity)
lastRefillTime = currentTime
end
-- 尝试获取令牌
if tokenCount <= 0 then
return 0 -- 限流
end
tokenCount = tokenCount - 1
-- 更新令牌桶状态
redis.call('hmset', key, 'lastRefillTime', lastRefillTime, 'tokenCount', tokenCount)
-- 设置过期时间(10 分钟,避免长期无请求的垃圾数据)
redis.call('expire', key, 600)
return 1 -- 允许通过
""";
// 漏桶算法 Lua 脚本
private static final String LEAKY_BUCKET_SCRIPT = """
local key = KEYS[1]
local capacity = tonumber(ARGV[1]) -- 桶容量
local currentTime = tonumber(ARGV[2])
-- 统计当前桶中请求数
local size = redis.call('llen', key)
if size >= capacity then
return 0 -- 桶满,限流
end
-- 入桶(存储请求时间戳)
redis.call('rpush', key, currentTime)
-- 设置过期时间(5 分钟,避免请求处理完后残留)
redis.call('expire', key, 300)
return 1 -- 入桶成功
""";
/**
* 执行限流判断
*/
public boolean doLimit(LimitAlgorithm algorithm, String key, int windowSeconds, int threshold,
int bucketCapacity, int rate) {
try {
switch (algorithm) {
case FIXED_WINDOW:
return fixedWindowLimit(key, windowSeconds, threshold);
case SLIDING_WINDOW:
return slidingWindowLimit(key, windowSeconds, threshold);
case TOKEN_BUCKET:
return tokenBucketLimit(key, bucketCapacity, rate);
case LEAKY_BUCKET:
return leakyBucketLimit(key, bucketCapacity);
default:
return false;
}
} catch (Exception e) {
// Redis 异常时,降级为本地限流(Guava RateLimiter)
return localFallbackLimit(rate);
}
}
/**
* 固定窗口计数器限流
*/
private boolean fixedWindowLimit(String key, int windowSeconds, int threshold) {
String redisKey = buildRedisKey(key, LimitAlgorithm.FIXED_WINDOW);
DefaultRedisScript<Long> script = new DefaultRedisScript<>(FIXED_WINDOW_SCRIPT, Long.class);
Long result = redisTemplate.execute(script, Collections.singletonList(redisKey),
String.valueOf(windowSeconds), String.valueOf(threshold));
return result != null && result == 1;
}
/**
* 滑动窗口计数器限流
*/
private boolean slidingWindowLimit(String key, int windowSeconds, int threshold) {
String redisKey = buildRedisKey(key, LimitAlgorithm.SLIDING_WINDOW);
long currentTime = System.currentTimeMillis();
String uuid = UUID.randomUUID().toString();
DefaultRedisScript<Long> script = new DefaultRedisScript<>(SLIDING_WINDOW_SCRIPT, Long.class);
Long result = redisTemplate.execute(script, Collections.singletonList(redisKey),
String.valueOf(windowSeconds), String.valueOf(threshold),
String.valueOf(currentTime), uuid);
return result != null && result == 1;
}
/**
* 令牌桶算法限流
*/
private boolean tokenBucketLimit(String key, int capacity, int rate) {
String redisKey = buildRedisKey(key, LimitAlgorithm.TOKEN_BUCKET);
long currentTime = System.currentTimeMillis();
DefaultRedisScript<Long> script = new DefaultRedisScript<>(TOKEN_BUCKET_SCRIPT, Long.class);
Long result = redisTemplate.execute(script, Collections.singletonList(redisKey),
String.valueOf(capacity), String.valueOf(rate), String.valueOf(currentTime));
return result != null && result == 1;
}
/**
* 漏桶算法限流(入桶逻辑)
*/
private boolean leakyBucketLimit(String key, int capacity) {
String redisKey = buildRedisKey(key, LimitAlgorithm.LEAKY_BUCKET);
long currentTime = System.currentTimeMillis();
DefaultRedisScript<Long> script = new DefaultRedisScript<>(LEAKY_BUCKET_SCRIPT, Long.class);
Long result = redisTemplate.execute(script, Collections.singletonList(redisKey),
String.valueOf(capacity), String.valueOf(currentTime));
return result != null && result == 1;
}
/**
* 本地降级限流(Guava RateLimiter)
*/
private boolean localFallbackLimit(int rate) {
// 基于 Guava RateLimiter 实现本地限流,key 为速率(简化处理)
String localKey = "local:limiter:" + rate;
GuavaRateLimiterHolder holder = GuavaRateLimiterHolder.getInstance();
return holder.getRateLimiter(localKey, rate).tryAcquire();
}
/**
* 构建 Redis 键(避免冲突)
*/
private String buildRedisKey(String key, LimitAlgorithm algorithm) {
return String.format("limiter:%s:%s", algorithm.name().toLowerCase(), key);
}
/**
* Guava RateLimiter 单例持有者(避免重复创建)
*/
private static class GuavaRateLimiterHolder {
private static final GuavaRateLimiterHolder INSTANCE = new GuavaRateLimiterHolder();
private final Map<String, com.google.common.util.concurrent.RateLimiter> rateLimiterMap = new HashMap<>();
private GuavaRateLimiterHolder() {}
public static GuavaRateLimiterHolder getInstance() {
return INSTANCE;
}
public com.google.common.util.concurrent.RateLimiter getRateLimiter(String key, int rate) {
return rateLimiterMap.computeIfAbsent(key, k -> com.google.common.util.concurrent.RateLimiter.create(rate));
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# 4. AOP 切面(注解驱动执行)
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Method;
@Aspect
@Component
public class RedisLimitAspect {
@Resource
private RedisLimitFactory redisLimitFactory;
@Value("${spring.redis-limiter.fallback-enabled:true}")
private boolean fallbackEnabled;
// SpEL 解析器
private final SpelExpressionParser parser = new SpelExpressionParser();
private final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
/**
* 切入点:标注 @RedisLimit 注解的方法
*/
@Pointcut("@annotation(com.example.redis.limiter.RedisLimit)")
public void redisLimitPointcut() {}
/**
* 环绕通知:执行限流判断
*/
@Around("redisLimitPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RedisLimit annotation = method.getAnnotation(RedisLimit.class);
// 解析限流键(支持 SpEL 表达式)
String key = parseSpelKey(annotation.key(), method, joinPoint.getArgs());
// 若未指定 key,默认用 "类名:方法名"
if (key.isEmpty()) {
key = method.getDeclaringClass().getName() + ":" + method.getName();
}
// 执行限流判断
boolean allow = redisLimitFactory.doLimit(
annotation.algorithm(),
key,
annotation.windowSeconds(),
annotation.threshold(),
annotation.bucketCapacity(),
annotation.rate()
);
if (!allow) {
// 限流时抛出异常,由全局异常处理器捕获
throw new RuntimeException(annotation.message());
}
// 允许通过,执行原方法
return joinPoint.proceed();
}
/**
* 解析 SpEL 表达式,获取限流键
*/
private String parseSpelKey(String spelKey, Method method, Object[] args) {
if (spelKey.isEmpty()) {
return "";
}
// 构建 SpEL 上下文
EvaluationContext context = new StandardEvaluationContext();
String[] parameterNames = parameterNameDiscoverer.getParameterNames(method);
if (parameterNames != null) {
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}
}
// 解析 SpEL 表达式
Expression expression = parser.parseExpression(spelKey);
return expression.getValue(context, String.class);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# 5. 全局异常处理器(统一返回限流结果)
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
public Map<String, Object> handleLimitException(RuntimeException e) {
Map<String, Object> result = new HashMap<>();
result.put("code", 429);
result.put("message", e.getMessage());
result.put("success", false);
return result;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 3.4 接口集成示例(注解驱动,零侵入)
# 1. 普通接口限流(按接口粒度)
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
/**
* 普通接口限流:1 秒内最多允许 100 次请求(令牌桶算法)
*/
@GetMapping("/api/test")
@RedisLimit(
key = "/api/test",
algorithm = LimitAlgorithm.TOKEN_BUCKET,
windowSeconds = 1,
threshold = 100,
capacity = 100,
rate = 100,
message = "接口请求过于频繁,请稍后再试"
)
public String testLimit() {
return "请求成功";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 2. 用户粒度限流(按用户 ID + 接口)
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
/**
* 用户粒度限流:每个用户 10 秒内最多允许 3 次下单(滑动窗口算法)
* 限流键:user:1001:/api/order/create(通过 SpEL 表达式解析 userId)
*/
@GetMapping("/api/order/create")
@RedisLimit(
key = "'user:' + #userId + ':/api/order/create'",
algorithm = LimitAlgorithm.SLIDING_WINDOW,
windowSeconds = 10,
threshold = 3,
message = "您下单过于频繁,请 10 秒后再试"
)
public String createOrder(@RequestParam Long userId) {
// 下单业务逻辑
return "下单成功";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 3. 秒杀场景限流(商品粒度+用户粒度双重防护)
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SeckillController {
/**
* 秒杀限流:商品粒度(1 秒 100 次)+ 用户粒度(10 秒 1 次)
* 注:此处仅演示商品粒度,用户粒度可通过多注解或手动调用工厂实现
*/
@GetMapping("/api/seckill/{goodsId}")
@RedisLimit(
key = "'seckill:goods:' + #goodsId",
algorithm = LimitAlgorithm.TOKEN_BUCKET,
windowSeconds = 1,
threshold = 100,
capacity = 100,
rate = 100,
message = "秒杀过于火爆,请稍后再试"
)
public String seckill(@PathVariable Long goodsId, @RequestParam Long userId) {
// 手动添加用户粒度限流(也可通过自定义组合注解优化)
String userKey = "user:" + userId + ":seckill:" + goodsId;
boolean userAllow = redisLimitFactory.doLimit(
LimitAlgorithm.FIXED_WINDOW,
userKey,
10, // 10 秒窗口
1, // 1 次请求
0, // 无关参数
0 // 无关参数
);
if (!userAllow) {
throw new RuntimeException("您已参与过该商品秒杀,请勿重复提交");
}
// 秒杀业务逻辑(扣减库存、创建订单)
return "秒杀成功";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 四、生产级优化:高并发、高可用、可观测
# 4.1 高并发优化:突破单机 Redis 瓶颈
# 1. Redis Cluster 分片策略
- 问题:单 Redis 节点的 QPS 上限约 10 万 ~ 100 万,秒杀等场景可能压垮节点;
- 解决方案:按限流键
key哈希分片,将不同key的限流数据分散到 Redis Cluster 的多个节点; - 实现:利用 Redis Cluster 的哈希槽机制,无需手动分片(Redis 自动将
key映射到对应哈希槽); - 注意:同一
key的限流数据会落在同一节点,保证计数准确。
# 2. 本地缓存预热(减少 Redis 访问)
- 问题:高频请求(如每秒 10 万+)会频繁访问 Redis,增加网络 IO 开销;
- 解决方案:在应用本地缓存“限流状态”,如本地缓存 100ms,期间不访问 Redis;
- 实现:在
RedisLimitFactory中加入本地缓存(如 Caffeine),缓存限流键的“是否已限流”状态; - 注意:本地缓存时长需远小于 Redis 窗口时长(如 100ms vs 1 秒),避免数据不一致。
# 3. Lua 脚本优化(减少网络往返)
- 问题:分步执行 Redis 命令会增加网络往返次数,高并发下性能下降;
- 解决方案:所有算法均通过 Lua 脚本原子化执行,将多个命令合并为 1 次网络请求;
- 优化点:Lua 脚本避免复杂逻辑(如循环、条件判断过多),防止脚本超时(Redis 默认脚本超时时间 5 秒)。
# 4.2 高可用优化:避免 Redis 成为单点故障
# 1. 降级策略完善
- Redis 宕机/网络超时:降级为本地限流(Guava RateLimiter),保证服务基本可用;
- 降级开关:通过配置中心(如 Nacos)动态控制是否启用降级,便于故障恢复;
- 限流熔断:当 Redis 异常率超过阈值(如 50%),自动熔断 Redis 限流,切换为本地限流。
# 2. Redis 集群高可用部署
- 部署架构:Redis Cluster(3 主 3 从)+ 哨兵模式,确保节点故障时自动切换;
- 数据持久化:开启 RDB + AOF 混合持久化,避免 Redis 重启后限流数据丢失;
- 连接池优化:合理配置 Redis 连接池参数(最大连接数、空闲连接数),避免连接耗尽。
# 3. 限流键过期时间优化
- 问题:长期无请求的限流键会占用 Redis 存储;
- 解决方案:所有限流键均设置过期时间(如窗口时长 + 1 秒、10 分钟),避免垃圾数据堆积;
- 优化点:对大窗口限流(如 1 天),定期执行
SCAN命令清理过期键,避免过期键过多影响性能。
# 4.3 可观测性优化:监控告警体系
# 1. 限流指标监控(接入 Prometheus+Grafana)
- 核心指标:
- 限流总请求数、通过请求数、被拒绝请求数;
- 各限流键的 QPS、拒绝率;
- Redis 限流响应时间、异常率;
- 实现:在
RedisLimitFactory中埋点,通过 Prometheus 客户端暴露指标,Grafana 配置仪表盘。
# 2. 告警配置
- 告警阈值:拒绝率 > 10%、Redis 异常率 > 5%、限流响应时间 > 5ms;
- 告警渠道:短信、邮件、钉钉/企业微信,确保及时响应。
# 3. 日志打印
- 打印内容:限流键、算法类型、是否通过、当前计数器/令牌数;
- 日志级别:正常通过的请求用
INFO级,被限流的请求用WARN级,Redis 异常用ERROR级; - 采样打印:高并发场景下,对限流通过的请求进行采样打印(如 1% 采样率),避免日志刷屏。
# 4.4 可扩展性优化:动态调整与多场景适配
# 1. 动态阈值调整
- 问题:固定阈值无法适配流量波动(如促销期间需临时提高阈值);
- 解决方案:将限流阈值存储到 Redis/配置中心,实时读取,支持动态调整;
- 实现:修改
@RedisLimit注解,支持阈值从配置中心获取(如threshold = "${limiter.threshold.api.test:100}")。
# 2. 多租户限流
- 问题:多租户系统中,需为不同租户配置不同限流阈值;
- 解决方案:限流键中加入租户标识(如
tenant:1001:api:test),为每个租户配置独立阈值; - 实现:通过 SpEL 表达式解析租户 ID(如
key = "'tenant:' + #tenantId + ':' + '/api/test'")。
# 3. 流量染色与灰度限流
- 场景:新功能灰度发布时,需对灰度流量单独限流;
- 解决方案:通过流量染色(如请求头
X-Gray-Traffic: true),解析染色标识后使用独立限流键; - 实现:在
RedisLimitAspect中解析请求头,拼接灰度标识到限流键。
# 五、生产环境踩坑指南(10 个致命坑点)
# 5.1 坑 1:限流键冲突
- 现象:不同业务的限流键重复(如都用
test),导致计数混乱; - 原因:未给限流键加业务前缀;
- 解决方案:统一用
limiter:{algorithm}:{biz}:{key}格式,如limiter:token:seckill:goods:10086。
# 5.2 坑 2:临界问题导致限流失效
- 现象:固定窗口算法在窗口切换时出现“双倍流量”,突破阈值;
- 原因:固定窗口的天生缺陷;
- 解决方案:对核心场景改用滑动窗口/令牌桶算法,或在窗口切换时增加缓冲(如窗口重叠 100ms)。
# 5.3 坑 3:Lua 脚本超时
- 现象:高并发下 Lua 脚本执行超时,Redis 抛出
BUSY错误; - 原因:脚本逻辑复杂(如循环、大量
ZREMRANGEBYSCORE操作); - 解决方案:简化 Lua 脚本逻辑,避免循环;对滑动窗口算法,合理拆分小窗口数量(如 1 秒拆 10 个)。
# 5.4 坑 4:Redis 大 Key 问题
- 现象:滑动窗口算法的 ZSET 存储过多请求时间戳(如每秒 1 万次请求,1 秒窗口存 1 万个元素),成为大 Key;
- 原因:窗口时长过长、请求量过大;
- 解决方案:缩短窗口时长、增加小窗口数量;定期清理 ZSET 中的过期元素;对高频请求启用本地缓存预热。
# 5.5 坑 5:分布式场景下全局一致性问题
- 现象:Redis Cluster 分片后,同一限流键落在不同节点,总请求数突破阈值;
- 原因:限流键哈希分片错误,同一
key被分配到不同节点; - 解决方案:确保限流键的哈希计算一致(如使用 Redis Cluster 原生哈希槽机制);对全局限流(如全系统 1 秒 1 万次),使用 Redis 主从架构(单节点计数)。
# 5.6 坑 6:本地缓存与 Redis 数据不一致
- 现象:本地缓存了“已限流”状态,Redis 计数器已重置,但本地仍拒绝请求;
- 原因:本地缓存时长过长;
- 解决方案:本地缓存时长设为 Redis 窗口时长的 1/10(如 1 秒窗口缓存 100ms);缓存过期时间采用随机值,避免缓存雪崩。
# 5.7 坑 7:漏桶算法的“漏水”任务阻塞
- 现象:漏桶算法的定时任务阻塞,导致请求堆积在桶中,无法处理;
- 原因:定时任务执行时间过长(如处理请求的业务逻辑耗时久);
- 解决方案:将“漏水”逻辑与业务逻辑解耦,通过消息队列异步处理请求;定时任务仅负责弹出请求,不处理业务逻辑。
# 5.8 坑 8:令牌桶算法的时间差计算误差
- 现象:高并发下,令牌桶的令牌填充速率与预期不符;
- 原因:时间差计算采用毫秒级,高并发下多次请求的时间差过小,导致令牌填充不及时;
- 解决方案:改用微秒级时间戳;优化令牌填充逻辑,允许批量填充令牌(如一次填充多个)。
# 5.9 坑 9:Redis 连接池参数不合理
- 现象:高并发下,出现
Could not get a resource from the pool错误; - 原因:最大连接数设置过小,无法满足并发需求;
- 解决方案:通过压测确定合理的最大连接数(如每秒 10 万次请求,最大连接数设为 64);监控连接池状态,动态调整参数。
# 5.10 坑 10:未处理 Redis 主从切换导致的限流数据丢失
- 现象:Redis 主从切换后,限流计数器/令牌桶状态丢失,导致限流失效;
- 原因:主从复制是异步的,切换时从库可能未同步最新的限流数据;
- 解决方案:开启 Redis 持久化(RDB+AOF);主从切换后,对限流键进行初始化(如令牌桶重置为初始容量);对核心场景,采用 Redis Cluster 避免单主节点依赖。
# 六、行业最佳实践总结
# 6.1 算法选型最佳实践
- 普通接口、非核心业务:固定窗口计数器(简单、高性能);
- 核心 API、商品详情页:滑动窗口计数器(精准、无临界问题);
- 数据库写入、第三方 API 调用:漏桶算法(严格控制输出速率);
- 秒杀、抢购、高并发核心业务:令牌桶算法(抗突发、流量平滑)。
# 6.2 限流策略组合最佳实践
- 多层限流:网关层(Nginx)限流 + 应用层(Redis)限流 + 本地限流(降级),形成防护体系;
- 多粒度限流:商品粒度 + 用户粒度 + IP 粒度,多重防护(如秒杀场景);
- 流量削峰:结合消息队列,将突发流量缓存到队列,异步处理,与限流配合使用。
# 6.3 阈值调优最佳实践
- 压测确定基准:通过压测获取服务的最大承载 QPS,阈值设为基准的 80%;
- 动态调整:根据流量波动(如促销、节假日)动态调整阈值;
- 预留缓冲:为核心业务预留 20%~30% 的缓冲阈值,避免突发流量压垮服务。
# 七、总结与展望
Redis 限流的核心价值,在于用高性能、分布式的方式实现流量的精细化治理,而非简单“拒绝请求”。其设计哲学是“先保系统稳定,再谈用户体验”——通过科学的算法选择、架构设计和优化手段,在流量洪峰来临时,确保核心业务可用、非核心业务降级,最终实现系统的稳定性与用户体验的平衡。