Redis可以实现多种多样的限流,基于滑动窗口是比较简单的一种实现方式

使用Zset的数据结构,为有序数组,我们可以根据时间窗口来删除数据,这样就记录了在一个时间窗口内存在多少请求,以此为依据来进行限流

img

实现

  1. 在Pom中引入Redis的依赖(标准项目应该都会有)

  2. 创建一个注解来标记

    /**
    * 自定义限流注解
    * 用于基于Redis实现的分布式限流
    */
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface 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;

    }
  3. 对这个注解进行切面

    /**
    * 环绕通知处理限流逻辑
    */
    @Around("@annotation(com.baiwang.cloud.web.annotation.RateLimit)")
    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();
    }
  4. 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;
    }
    }
  5. 对应的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次,过多就会报错,限流级别是用户级别

image-20250917165823913

经过一次访问之后我们在Redis里就可以看到我们的访问记录,多次请求则会返回

{
"success": false,
"message": "请求过于频繁,请稍后再试",
"data": []
}