在高并发系统中,限流是必要的,不然一会就被人刷爆了。我介绍如何使用Spring AOP结合Redis实现分布式限流功能。
为什么需要限流?
在高并发场景下,系统可能会面临以下问题:
- 流量洪峰:突发流量导致系统过载
- 资源耗尽:数据库连接池、线程池等资源被耗尽
- 服务降级:保护核心服务(系统主动关掉非核心功能,优先保住核心功能)
- 用户体验:防止系统崩溃,提供错误提示,比如“当前排队人数过多,请稍后再试”
限流策略选择
常见的限流算法有:
- 令牌桶算法:平滑限流,允许突发流量
- 漏桶算法:严格限制请求速率
- 计数器算法:简单但不够平滑
我们用计数器算法结合Redis来简单实现分布式限流。
AOP限流实现原理
核心思路
- AOP切面:拦截标注@RateLimit注解的方法
- Redis存储:使用Redis记录请求计数
- Lua脚本:保证原子性操作
- 用户隔离:按用户ID或IP进行限流
架构设计
请求 → AOP拦截器 → Redis限流 → 业务方法
代码实现详解
自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
String key() default "rate_limit"; // 限流Key前缀
int time() default 60; // 时间窗口(秒)
int count() default 100; // 允许的请求数
}AOP切面实现
@Slf4j
@Aspect
@Component
public class RateLimitAspect {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private DefaultRedisScript<Long> redisScript;
@PostConstruct
public void init() {
redisScript = new DefaultRedisScript<>();
redisScript.setResultType(Long.class);
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/limit.lua")));
}
@Around("@annotation(com.campus.annotation.RateLimit)")
public Object interceptor(ProceedingJoinPoint pjp) throws Throwable {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
RateLimit rateLimit = method.getAnnotation(RateLimit.class);
if (rateLimit != null) {
String key = rateLimit.key();
int time = rateLimit.time();
int count = rateLimit.count();
// 组合 Key: rate_limit:方法名:用户ID
String combineKey = key + ":" + method.getName() + ":" + BaseContext.getCurrentId();
List<String> keys = Collections.singletonList(combineKey);
// 执行 Lua 脚本
Long number = stringRedisTemplate.execute(redisScript, keys, String.valueOf(count), String.valueOf(time));
if (number != null && number == 0) {
// 超过限流
throw new RuntimeException("访问过于频繁,请稍后再试");
}
log.info("限制请求'{}',当前请求'{}',缓存key'{}", count, number.intValue(), key);
}
return pjp.proceed();
}
}Lua脚本实现(计数器算法)
-- limit.lua
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local expire_time = tonumber(ARGV[2])
local is_exists = redis.call("EXISTS", key)
if is_exists == 1 then
local current = tonumber(redis.call("GET", key))
if current and current + 1 > limit then
return 0
end
redis.call("INCR", key)
else
redis.call("SET", key, "1")
redis.call("EXPIRE", key, expire_time)
end
return 1使用方法
添加注解
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@RateLimit(key = "user_login", time = 60, count = 5)
@Override
public User login(String username, String password) {
// 登录逻辑
}
}配置Redis
spring:
redis:
host: localhost
port: 6379后续优化
令牌桶算法优化
当前实现是简单的计数器算法,可以升级为令牌桶算法:
-- token_bucket.lua
local key = KEYS[1]
local rate = tonumber(ARGV[1]) -- 生成速率(令牌/秒)
local capacity = tonumber(ARGV[2]) -- 桶容量
local now = tonumber(ARGV[3]) -- 当前时间戳
local last_time = redis.call("HGET", key, "last_time") or now
local tokens = redis.call("HGET", key, "tokens") or capacity
local passed = now - last_time
local new_tokens = math.floor(passed * rate)
tokens = math.min(tokens + new_tokens, capacity)
if tokens > 0 then
tokens = tokens - 1
redis.call("HSET", key, "tokens", tokens)
redis.call("HSET", key, "last_time", now)
return 1
else
return 0
end动态限流
根据系统负载动态调整限流参数:
@RateLimit(key = "dynamic_limit", time = "${limit.time}", count = "${limit.count}")避坑
- Key 命名:一定要在切面里强制加上模块前缀,否则以后 Redis 里全是乱七八糟的 Key,排查起来能累死。
- 别写死异常:建议定义一个统一的业务异常,前端接收后能做更优雅的处理,而不是随便穿弹一个 RuntimeException。
- 进阶优化:目前是固定窗口的方法,有可能会遇到流量边界问题(比如刚好卡着时间窗口刷),后续可以无缝替换 Lua 脚本,用 ZSET 实现滑动窗口。
总结
我们详细了解基于Spring AOP和Redis的分布式限流实现,虽然简单,但是在小型项目中绝对够用,也能无缝切换更好的限流方法。






