基于Redis滑动窗口实现的限流
Redis可以实现多种多样的限流,基于滑动窗口是比较简单的一种实现方式
使用Zset的数据结构,为有序数组,我们可以根据时间窗口来删除数据,这样就记录了在一个时间窗口内存在多少请求,以此为依据来进行限流
实现
-
在Pom中引入Redis的依赖(标准项目应该都会有)
-
创建一个注解来标记
/**
* 自定义限流注解
* 用于基于Redis实现的分布式限流
*/
public RateLimit {
String TENANT = "tenant";
String ORG = "org";
String USER = "user";
/**
* 限流key前缀
*/
String keyPrefix() default "";
/**
* 限流时间窗口(秒)
*/
int period() default 60;
/**
* 时间窗口内允许的请求数
*/
int count() default 50;
/**
* 限流时的提示信息
*/
String message() default "请求过于频繁,请稍后再试";
/**
* 限流类型
*/
String type() default USER;
} -
对这个注解进行切面
/**
* 环绕通知处理限流逻辑
*/
public Object around(ProceedingJoinPoint point) throws Throwable {
// 获取注解信息
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
RateLimit rateLimit = method.getAnnotation(RateLimit.class);
// 获取当前用户信息
User user = getCurrentUser();
if (user == null) {
log.warn("无法获取用户信息,跳过限流检查");
return point.proceed();
}
String keyPrefix = rateLimit.keyPrefix();
String redisPrefix = "RATE_LIMIT:";
String key = "";
switch (rateLimit.type()) {
case RateLimit.USER:
key = redisPrefix + keyPrefix + ":" + user.getUserId();
break;
case RateLimit.ORG:
key = redisPrefix + keyPrefix + ":" + user.getOrgId();
break;
case RateLimit.TENANT:
key = redisPrefix + keyPrefix + ":" + user.getTenantId();
break;
default:
log.warn("未定义的限流类型:{}", rateLimit.type());
return point.proceed();
}
// 执行限流检查
boolean allowed = redisRateLimiter.isAllowed(key, rateLimit.period(), rateLimit.count());
if (!allowed) {
log.warn("BY {} key {} 调用 {} 方法触发限流", rateLimit.type(), key, method.getName());
// 返回限流提示信息
return createRateLimitResult(rateLimit.message());
}
// 未触发限流,继续执行原方法
return point.proceed();
} -
redisRateLimiter是具体的实现
/**
* 执行限流检查(基于滑动窗口算法)
*
* @param key 限流key
* @param window 限流时间窗口(秒)
* @param limit 限流次数
* @return true表示允许访问,false表示拒绝访问
*/
public boolean isAllowed(String key, int window, int limit) {
try {
long currentTime = System.currentTimeMillis();
long minTime = currentTime - window * 1000L;
// 删除窗口外的旧记录
redisUtils.getZSetOps().removeRangeByScore(key, 0, minTime);
// 获取当前窗口内的请求数量
Long count = redisUtils.getZSetOps().zCard(key);
// 如果超过限制,返回false表示拒绝
if (count != null && count >= limit) {
return false;
}
// 添加当前请求
redisUtils.getZSetOps().add(key, String.valueOf(currentTime), currentTime);
// 设置过期时间
redisUtils.expire(key, (long) window);
return true;
} catch (Exception e) {
log.error("执行Redis限流检查异常", e);
// 发生异常时,为了保证服务可用性,允许通过
return true;
}
} -
对应的redisUtil则有以下几个方法
/**
* 获取ZSet操作对象
* @return ZSetOperations
*/
public ZSetOperations<String, Object> getZSetOps() {
return this.redisTemplate.opsForZSet();
}
/**
* 增加指定key的值,用于计数器限流
* @param key 键
* @param delta 增加的值
* @return 增加后的值
*/
public Long incr(String key, long delta) {
return this.redisTemplate.opsForValue().increment(key, delta);
}
/**
* 增加指定key的值,默认增加1,用于计数器限流
* @param key 键
* @return 增加后的值
*/
public Long incr(String key) {
return this.redisTemplate.opsForValue().increment(key, 1);
}
使用
我们可以在需要限流的方法上面直接添加
@RateLimit(keyPrefix = "selectIdentHistoryList", period = 60, count = 2, message = "请求过于频繁,请稍后再试", type = RateLimit.USER)
keyPrefix指的是当前方法级别的限流Key,时间窗口为60秒,窗口内可以访问2次,过多就会报错,限流级别是用户级别
经过一次访问之后我们在Redis里就可以看到我们的访问记录,多次请求则会返回
{ |
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 喵喵博客!