Redis 限流最佳实践:令牌桶与滑动窗口全流程实现
在分布式系统中,API 限流是保护系统稳定性的重要手段。本文将介绍如何使用 Spring Boot + Redis + 自定义注解 + AOP 实现**可选限流算法(滑动窗口 / 令牌桶)**的高效方案。
实现原理
- 滑动窗口算法:高精度,严格限制 QPS。
- 令牌桶算法:允许突发流量,平滑速率控制。
- Redis + Lua 脚本:保证分布式环境下的原子性和高性能。
- 自定义注解 + AOP:灵活接入,无侵入。
1. 添加依赖
<dependencies>
    <!-- Spring Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!-- AOP -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <!-- 工具包 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
</dependencies>2. 定义注解
支持选择 限流算法 、维度 、参数。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {
    /** 限流 key 前缀 */
    String key() default "rate_limit:";
    /** 算法模式:滑动窗口 / 令牌桶 */
    Mode mode() default Mode.SLIDING_WINDOW;
    /** ---------------- 滑动窗口参数 ---------------- */
    int time() default 60;        // 时间窗口(秒)
    int count() default 100;      // 窗口内允许的请求数
    /** ---------------- 令牌桶参数 ---------------- */
    long capacity() default 100;          // 桶容量
    long refillTokens() default 100;      // 每次补充令牌数
    long refillIntervalMs() default 1000; // 补充周期(毫秒)
    long requestedTokens() default 1;     // 单次请求消耗令牌数
    long idleTtlMs() default 300000;      // 空桶过期(毫秒)
    /** 限流维度:方法、IP、用户 */
    LimitType limitType() default LimitType.DEFAULT;
    enum Mode {
        SLIDING_WINDOW,
        TOKEN_BUCKET
    }
}
public enum LimitType {
    DEFAULT, // 方法级别
    IP,      // 客户端 IP
    USER     // 用户ID
}3. Redis 限流服务实现
(1) 滑动窗口 + Lua 脚本
@Service
public class SlidingWindowRateLimiter {
    private final StringRedisTemplate redisTemplate;
    private final DefaultRedisScript<Long> script;
    public SlidingWindowRateLimiter(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
        this.script = new DefaultRedisScript<>();
        this.script.setScriptText(buildLua());
        this.script.setResultType(Long.class);
    }
    private String buildLua() {
        return ""
            + "local key = KEYS[1]\n"
            + "local now = tonumber(ARGV[1])\n"
            + "local window = tonumber(ARGV[2])\n"
            + "local limit = tonumber(ARGV[3])\n"
            + "\n"
            + "redis.call('ZREMRANGEBYSCORE', key, 0, now - window)\n"
            + "local count = redis.call('ZCARD', key)\n"
            + "if count < limit then\n"
            + "  redis.call('ZADD', key, now, now)\n"
            + "  redis.call('PEXPIRE', key, window)\n"
            + "  return 1\n"
            + "else\n"
            + "  return 0\n"
            + "end";
    }
    public boolean allow(String key, int time, int count) {
        long now = System.currentTimeMillis();
        Long result = redisTemplate.execute(
                script,
                Collections.singletonList(key),
                String.valueOf(now),
                String.valueOf(time * 1000L),
                String.valueOf(count));
        return result != null && result == 1;
    }
}(2) 令牌桶 + Lua 脚本
@Service
public class TokenBucketRateLimiter {
    private final StringRedisTemplate redisTemplate;
    private final DefaultRedisScript<List> script;
    public TokenBucketRateLimiter(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
        this.script = new DefaultRedisScript<>();
        this.script.setScriptText(buildLua());
        this.script.setResultType(List.class);
    }
    private String buildLua() {
        return ""
            + "local key = KEYS[1]\n"
            + "local now = tonumber(ARGV[1])\n"
            + "local capacity = tonumber(ARGV[2])\n"
            + "local refillTokens = tonumber(ARGV[3])\n"
            + "local refillIntervalMs = tonumber(ARGV[4])\n"
            + "local requested = tonumber(ARGV[5])\n"
            + "local idleTtlMs = tonumber(ARGV[6])\n"
            + "\n"
            + "local tokens = tonumber(redis.call('HGET', key, 'tokens'))\n"
            + "local lastTs = tonumber(redis.call('HGET', key, 'ts'))\n"
            + "if tokens == nil then\n"
            + "  tokens = capacity\n"
            + "  lastTs = now\n"
            + "else\n"
            + "  if lastTs == nil then lastTs = now end\n"
            + "  local delta = now - lastTs\n"
            + "  if delta > 0 then\n"
            + "    local add = math.floor(delta * refillTokens / refillIntervalMs)\n"
            + "    if add > 0 then\n"
            + "      tokens = math.min(capacity, tokens + add)\n"
            + "      lastTs = lastTs + math.floor(add * refillIntervalMs / refillTokens)\n"
            + "    end\n"
            + "  end\n"
            + "end\n"
            + "local allowed = 0\n"
            + "if tokens >= requested then\n"
            + "  tokens = tokens - requested\n"
            + "  allowed = 1\n"
            + "end\n"
            + "redis.call('HSET', key, 'tokens', tokens, 'ts', now)\n"
            + "if idleTtlMs > 0 then redis.call('PEXPIRE', key, idleTtlMs) end\n"
            + "return {allowed, tokens, now}\n";
    }
    public boolean allow(String key,
                         long capacity,
                         long refillTokens,
                         long refillIntervalMs,
                         long requestedTokens,
                         long idleTtlMs) {
        long now = System.currentTimeMillis();
        @SuppressWarnings("unchecked")
        List<Long> ret = (List<Long>) redisTemplate.execute(
                script,
                Collections.singletonList(key),
                String.valueOf(now),
                String.valueOf(capacity),
                String.valueOf(refillTokens),
                String.valueOf(refillIntervalMs),
                String.valueOf(requestedTokens),
                String.valueOf(idleTtlMs)
        );
        return ret != null && !ret.isEmpty() && ret.get(0) == 1L;
    }
}4. AOP 切面
@Aspect
@Component
public class RateLimiterAspect {
    private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);
    private final SlidingWindowRateLimiter slidingWindowLimiter;
    private final TokenBucketRateLimiter tokenBucketLimiter;
    private final HttpServletRequest request;
    public RateLimiterAspect(SlidingWindowRateLimiter slidingWindowLimiter,
                             TokenBucketRateLimiter tokenBucketLimiter,
                             HttpServletRequest request) {
        this.slidingWindowLimiter = slidingWindowLimiter;
        this.tokenBucketLimiter = tokenBucketLimiter;
        this.request = request;
    }
    @Around("@annotation(limit)")
    public Object around(ProceedingJoinPoint pjp, RateLimiter limit) throws Throwable {
        String key = buildKey(limit.key(), limit.limitType(), pjp);
        boolean allowed;
        if (limit.mode() == RateLimiter.Mode.SLIDING_WINDOW) {
            allowed = slidingWindowLimiter.allow(key, limit.time(), limit.count());
        } else {
            allowed = tokenBucketLimiter.allow(
                    key,
                    limit.capacity(),
                    limit.refillTokens(),
                    limit.refillIntervalMs(),
                    limit.requestedTokens(),
                    limit.idleTtlMs()
            );
        }
        if (allowed) {
            return pjp.proceed();
        } else {
            log.warn("限流触发: key={}, mode={}", key, limit.mode());
            Map<String, Object> result = new HashMap<>();
            result.put("code", 429);
            result.put("message", "请求过于频繁,请稍后再试");
            return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(result);
        }
    }
    private String buildKey(String prefix, LimitType type, ProceedingJoinPoint pjp) {
        StringBuilder sb = new StringBuilder(prefix);
        if (type == LimitType.IP) {
            sb.append(clientIp());
        } else if (type == LimitType.USER) {
            String userId = request.getHeader("X-User-Id");
            sb.append(userId != null ? userId : "anonymous");
        } else {
            MethodSignature sig = (MethodSignature) pjp.getSignature();
            Method m = sig.getMethod();
            sb.append(m.getDeclaringClass().getName()).append(".").append(m.getName());
        }
        return sb.toString();
    }
    private String clientIp() {
        String ip = request.getHeader("X-Forwarded-For");
        if (StringUtils.isNotBlank(ip) && !"unknown".equalsIgnoreCase(ip)) {
            int idx = ip.indexOf(',');
            return idx > 0 ? ip.substring(0, idx).trim() : ip.trim();
        }
        ip = request.getHeader("X-Real-IP");
        return StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip) ? request.getRemoteAddr() : ip;
    }
}5. 示例 Controller
@RestController
@RequestMapping("/api")
public class TestController {
    /** 滑动窗口:10秒最多5次 */
    @RateLimiter(key = "test:", mode = RateLimiter.Mode.SLIDING_WINDOW, time = 10, count = 5)
    @GetMapping("/test1")
    public String test1() {
        return "ok";
    }
    /** 令牌桶:容量20,每秒补充10个,用户维度限流 */
    @RateLimiter(key = "order:", mode = RateLimiter.Mode.TOKEN_BUCKET,
                 capacity = 20, refillTokens = 10, refillIntervalMs = 1000,
                 requestedTokens = 1, limitType = LimitType.USER)
    @PostMapping("/order")
    public String order() {
        return "ok";
    }
}6. Redis 配置
spring:
  redis:
    host: localhost
    port: 6379
    database: 0
    timeout: 3000ms
    lettuce:
      pool:
        max-active: 8
        max-wait: -1ms
        max-idle: 8
        min-idle: 07. 压测验证
# 滑动窗口
for i in {1..20}; do
  curl -i http://localhost:8080/api/test1
done
# 令牌桶 (突发流量 + 稳态速率测试)
wrk -t2 -c20 -d30s http://localhost:8080/api/order8. 总结
本文实现了基于 Redis 的 API 限流组件,支持:
- 滑动窗口限流:严格控制请求速率,高精度。
- 令牌桶限流:支持突发流量,速率可控。
- 多维度限流:方法 / IP / 用户。
- 分布式支持:Redis + Lua 保证原子性,适合微服务架构。
- 低侵入性:注解 + AOP 接入,业务无感知。
- 可扩展:可接入 Prometheus 监控,或增加降级策略。
这种方案在网关、微服务、接口保护场景都能直接落地,是企业级 API 防护的最佳实践之一。
