Redis 限流最佳实践:令牌桶与滑动窗口全流程实现
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,      // 客户端 IPUSER     // 用户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: localhostport: 6379database: 0timeout: 3000mslettuce:pool:max-active: 8max-wait: -1msmax-idle: 8min-idle: 0
7. 压测验证
# 滑动窗口
for i in {1..20}; docurl -i http://localhost:8080/api/test1
done# 令牌桶 (突发流量 + 稳态速率测试)
wrk -t2 -c20 -d30s http://localhost:8080/api/order
8. 总结
本文实现了基于 Redis 的 API 限流组件,支持:
- 滑动窗口限流:严格控制请求速率,高精度。
- 令牌桶限流:支持突发流量,速率可控。
- 多维度限流:方法 / IP / 用户。
- 分布式支持:Redis + Lua 保证原子性,适合微服务架构。
- 低侵入性:注解 + AOP 接入,业务无感知。
- 可扩展:可接入 Prometheus 监控,或增加降级策略。
这种方案在网关、微服务、接口保护场景都能直接落地,是企业级 API 防护的最佳实践之一。

