基于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 许可协议。转载请注明来自 喵喵博客!




