【Springboot starter 组件开发】限流组件 RateLimiter

一、摘要

  1. 基于guava的RateLimiter,实现限流
  2. 基于redis + lua脚本(推荐,准确性高),实现限流
  3. 掌握springboot starter的开发流程
  4. 源码地址:ratelimiter-spring-boot-starter

二、基于guava实现

2.1 核心依赖

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>23.5-jre</version>
</dependency>

2.2 核心逻辑

@Slf4j
public class GuavaLimiter implements LimiterManager {

    private final Map<String, RateLimiter> limiterMap = Maps.newConcurrentMap();

    @Override
    public boolean tryAccess(LimiterEntity entity) {
        if (StringUtils.isBlank(entity.getKey())) {
            throw new LimiterException("Guava limiter key cannot be empty");
        }
        RateLimiter rateLimiter = getRateLimiter(entity);
        if (rateLimiter == null) {
            return false;
        }
        boolean result = rateLimiter.tryAcquire(entity.getLimit(), 200, TimeUnit.MILLISECONDS);
        log.info("Guava limiter tryAccess, key={}, result={}", entity.getKey(), result);
        return result;
    }

    private RateLimiter getRateLimiter(LimiterEntity entity) {
        String key = entity.getKey();
        // 先看缓存中是否存在
        if (!limiterMap.containsKey(key)) {
            // 缓存中不存在,则创建令牌桶,预热时间设置为1s
            RateLimiter rateLimiter = RateLimiter.create(entity.getLimit(), 1, TimeUnit.SECONDS);
            limiterMap.put(key, rateLimiter);
            log.info("Guava limiter new bucket, key={}, permits={}", key, entity.getLimit());
            return rateLimiter;
        }
        return limiterMap.get(key);
    }
}

三、基于Redis + lua脚本实现

3.1 核心依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <scope>provided</scope>
</dependency>

3.2 核心逻辑

@Slf4j
public class RedisLimiter implements LimiterManager {

    private final StringRedisTemplate stringRedisTemplate;

    public RedisLimiter(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private final static DefaultRedisScript<Long> redisScript;

    static {
        log.info("Redis Limiter lua is already loaded");
        redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(Long.class);
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua\\limit.lua")));
    }

    @Override
    public boolean tryAccess(LimiterEntity entity) {
        if (StringUtils.isBlank(entity.getKey())) {
            throw new LimiterException("Redis limiter key cannot be empty");
        }
        List<String> keys = Collections.singletonList(entity.getKey());

        Long count = stringRedisTemplate.execute(redisScript, keys,
                "" + entity.getLimit(), "" + entity.getExpire());

        log.info("Redis limiter tryAccess, key={}, count={} ", entity.getKey(), count);

        return count != null && count != 0;
    }
}

lua脚本:

-- 简单的令牌桶算法的变体
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local expire = tonumber(ARGV[2])

local current = tonumber(redis.call('get', key) or "0")

if current == 0 then
    -- 当前计数为0,说明这是第一次请求或计数已重置
    redis.call('SET', key, '1') -- 设置计数器为1
    redis.call('EXPIRE', key, expire) -- 设置过期时间
    return 1
elseif current + 1 > limit then
    -- 达到限流阈值
    return 0
else
    -- 计数器递增
    redis.call('INCRBY', key, 1)
    return current + 1
end
### 使用Spring Boot和Redis实现固定时间窗口限流 #### 项目初始化 为了创建一个新的Spring Boot应用程序,可以使用Spring Initializr来快速搭建项目框架。确保引入必要的依赖项,如`spring-boot-starter-data-redis`用于连接Redis数据库以及`spring-boot-starter-aop`支持面向切面编程[^2]。 #### 创建限流注解 定义一个自定义注解`@RateLimit`,该注解可用于标记需要进行速率限制的方法或类。此注解应包含参数以指定允许的最大请求次数及单位时间长度等信息[^1]。 ```java import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimit { int maxRequests() default 100; // 默认最大请求数 String timeWindow(); // 时间窗口大小(秒) } ``` #### Lua脚本编写 准备一段Lua脚本来处理计数逻辑并返回是否超出限额的结果。这段代码将在每次调用受保护的服务端点之前执行,并由Redis服务器解释运行[^4]。 ```lua local key = KEYS[1] local limit = tonumber(ARGV[1]) local current_time = tonumber(redis.call('time')[1]) -- 获取当前存储的时间戳和计数值 local stored_data = redis.call("GET", key) if not stored_data then -- 如果不存在,则设置初始值 redis.call("SET", key, string.format("%d:%d", current_time, 1)) redis.call("EXPIRE", key, ARGV[2]) -- 设置过期时间为窗口周期 else local parts = {} for part in string.gmatch(stored_data, "[^:]+") do table.insert(parts, part) end local last_timestamp = tonumber(parts[1]) local count = tonumber(parts[2]) if (current_time - last_timestamp) >= tonumber(ARGV[2]) then -- 当前时间超过设定间隔,重置计数器 redis.call("SET", key, string.format("%d:%d", current_time, 1)) elseif count < limit then -- 更新现有条目中的计数 redis.call("INCRBY", key, 1) else return "LIMIT_EXCEEDED" end end return "ALLOWED" ``` #### Redis处理器构建 开发服务层组件负责与Redis交互,加载上述Lua脚本并通过命令传递给它相应的参数来进行实际的限流判断工作。 ```java @Service public class RedisRateLimiter { private final ReactiveRedisTemplate<String, Object> template; public RedisRateLimiter(final ReactiveRedisTemplate<String, Object> template){ this.template = template; } /** * 执行Lua脚本实施限流策略. */ public Mono<Boolean> isAllowed(String apiKey, Integer maxRequestsPerSecond, Long windowInSeconds) { List<String> keys = Collections.singletonList(apiKey); List<String> args = Arrays.asList( String.valueOf(maxRequestsPerSecond), String.valueOf(windowInSeconds)); return template.execute(new DefaultRedisScript<>(new Resource() { @Override public InputStream getInputStream() throws IOException { return new ByteArrayInputStream(""" -- 上述Lua脚本内容... """.getBytes()); } }, Boolean.class), keys, args).map(result -> !"LIMIT_EXCEEDED".equals(result)); } } ``` #### 编写AOP切片 利用Spring AOP特性拦截带有特定注解的方法调用,在方法真正被执行之前先检查其是否满足预设条件——即未违反规定的访问频率限制。 ```java @Aspect @Component @Slf4j public class RateLimitingAspect { private static final Logger logger = LoggerFactory.getLogger(RateLimitingAspect.class); private final RedisRateLimiter rateLimiter; public RateLimitingAspect(RedisRateLimiter rateLimiter) { this.rateLimiter = rateLimiter; } @Around("@annotation(rateLimit)") public Object handleApiCall(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); String methodName = method.getName(); Class<?> targetClass = method.getDeclaringClass(); boolean allowed = rateLimiter.isAllowed(targetClass.getSimpleName().concat(".").concat(methodName), rateLimit.maxRequests(), Long.parseLong(rateLimit.timeWindow())).blockOptional().orElse(false); if (!allowed) { throw new TooManyRequestsException("Too many requests"); } else { return joinPoint.proceed(); } } } ``` #### Controller接口测试 最后一步是在控制器中应用新创建好的注解来验证整个流程能否正常运作。当尝试发送过多请求时,应该会收到HTTP状态码429表示太频繁了;而在合理范围内则能成功响应数据。 ```java @RestController @RequestMapping("/api/v1") public class ExampleController { @GetMapping("/test-rate-limit") @RateLimit(timeWindow="60") // 每分钟最多接收100次请求 public ResponseEntity<String> testRateLimit(){ return ResponseEntity.ok("Request processed successfully."); } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qiye丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值