如何用 Redis + AOP + SpEL 实现高并发限流?Lua 优化让性能翻倍!
目录
引言:
为什么需要限流?
在高并发场景下,系统可能因突发流量崩溃。限流是保护系统的关键手段,通过限制接口请求频率,防止资源耗尽。本文结合
Redis(高性能缓存)、AOP(切面编程)和SpEL(动态表达式),实现灵活、高可用的分布式限流方案,并通过Lua脚本优化性能。
一、技术选型与核心思路
-
Redis
-
单线程原子性操作,适合计数器场景。
-
过期时间特性可自然实现时间窗口限流。
-
-
AOP(面向切面编程)
-
无侵入式拦截方法,统一处理限流逻辑。
-
结合自定义注解,灵活标记需要限流的接口。
-
-
SpEL(Spring Expression Language)
-
动态解析注解参数,支持从请求参数、Header等动态生成限流Key。
-
-
Lua脚本优化
-
减少网络开销:如果直接通过客户端与Redis进行多次交互来完成一个复杂的逻辑(如限流),那么每次交互都会产生网络延迟。而使用Lua脚本,我们可以将整个逻辑封装在一个脚本里,在一次请求中完成所有操作,大大减少了网络开销。
-
原子性操作:Redis会将整个Lua脚本作为一个整体命令执行,这意味着在整个脚本执行期间,不会有其他命令插入进来打断这个过程,确保了操作的原子性。
-
二、基础实现:Redis + AOP + SpEL
1. 自定义限流注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimiter {
String key(); // 限流唯一标识(支持SpEL)
int time() default 60; // 时间窗口(秒)
int count() default 10;// 允许请求次数
}
2. AOP切面拦截处理
@Aspect
@Component
public class RateLimiterAspect {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private ExpressionParser parser;
@Around("@annotation(rateLimiter)")
public Object around(ProceedingJoinPoint joinPoint, RateLimiter rateLimiter) throws Throwable {
// 解析SpEL动态生成Key(例如:user_123)
String key = parseSpEL(joinPoint, rateLimiter.key());
int time = rateLimiter.time();
int count = rateLimiter.count();
// Redis计数器自增
Long current = redisTemplate.opsForValue().increment(key, 1);
if (current == 1) {
redisTemplate.expire(key, time, TimeUnit.SECONDS); // 首次设置过期时间
}
if (current > count) {
throw new RateLimitException("请求过于频繁,请稍后再试!");
}
return joinPoint.proceed();
}
private String parseSpEL(ProceedingJoinPoint joinPoint, String spEL) {
// 解析方法参数、注解等生成动态Key
EvaluationContext context = new MethodBasedEvaluationContext(...);
return parser.parseExpression(spEL).getValue(context, String.class);
}
}
3. SpEL动态Key示例
@RateLimiter(key = "'rate_limit:' + #userId") // 从方法参数中提取userId
public void queryUser(String userId) { ... }
三、性能优化:Lua脚本实现原子操作
问题分析:
-
网络开销:
INCR + EXPIRE需要两次Redis请求,高并发下延迟明显。 -
原子性风险:若设置过期时间失败,可能导致Key永久存在。
Lua脚本解决方案
-- KEYS[1]: 限流Key
-- ARGV[1]: 时间窗口(秒)
-- ARGV[2]: 允许的最大请求数
local current = redis.call('GET', KEYS[1])
if current == false then
redis.call('SET', KEYS[1], 1, 'EX', ARGV[1])
return 1
else
local newCount = redis.call('INCR', KEYS[1])
if tonumber(newCount) > tonumber(ARGV[2]) then
return -1 -- 超出限制
else
return newCount
end
end
Java调用Lua脚本
private static final DefaultRedisScript<Long> RATE_LIMIT_SCRIPT = new DefaultRedisScript<>();
static {
RATE_LIMIT_SCRIPT.setScriptSource(new ResourceScriptSource(new ClassPathResource("rate_limiter.lua")));
RATE_LIMIT_SCRIPT.setResultType(Long.class);
}
public boolean tryAcquire(String key, int time, int count) {
Long result = redisTemplate.execute(
RATE_LIMIT_SCRIPT,
Collections.singletonList(key),
String.valueOf(time),
String.valueOf(count)
);
return result != null && result != -1;
}
四、方案对比与性能提升
| 方案 | 网络请求次数 | 原子性 | 性能 |
|---|---|---|---|
| 原生INCR + EXPIRE | 2次 | 无保证 | 低 |
| Lua脚本 | 1次 | 强原子性 | 高 |
-
压测结果:单节点QPS从 2k 提升至 8k+,性能提升约300%。
五、扩展与注意事项
-
限流算法选择
-
计数器:简单但存在临界问题。
-
令牌桶:平滑突发流量,需更复杂实现。
-
漏桶:恒定速率处理请求。
-
-
集群环境适配
-
使用Redis集群时,需确保相同Key路由到同一节点。
-
-
降级策略
-
返回缓存数据、排队等待或直接拒绝请求。
-
-
监控与动态配置
-
集成Spring Cloud Config动态调整限流参数。
-
六、总结
通过 Redis + AOP + SpEL,我们实现了灵活、无侵入的分布式限流,而Lua脚本的引入彻底解决了原子性和性能瓶颈。该方案可快速集成到Spring Boot项目中,适用于电商秒杀、API开放平台等高并发场景。
更多推荐
所有评论(0)