Redis 限流最佳实践:令牌桶与滑动窗口全流程实现

B站影视 韩国电影 2025-09-03 00:13 2

摘要:在分布式系统中,API 限流是保护系统稳定性的重要手段。本文将介绍如何使用Spring Boot + Redis + 自定义注解 + AOP实现**可选限流算法(滑动窗口 / 令牌桶)**的高效方案。

在分布式系统中,API 限流是保护系统稳定性的重要手段。本文将介绍如何使用 Spring Boot + Redis + 自定义注解 + AOP 实现**可选限流算法(滑动窗口 / 令牌桶)**的高效方案。

org.springframework.bootspring-boot-starter-webspring-boot-starter-data-redisspring-boot-starter-aoporg.apache.commonscommons-lang3

支持选择 限流算法维度参数

@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNtime)@Documentedpublic @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}@Servicepublic class SlidingWindowRateLimiter {private final StringRedisTemplate redisTemplate;private final DefaultRedisScriptscript; 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 @Servicepublic class TokenBucketRateLimiter {private final StringRedisTemplate redisTemplate;private final DefaultRedisScriptscript; 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") Listret = (List) 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; } } @Aspect@Componentpublic 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);Mapresult = 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; } } @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";}}spring:redis:host: localhostport: 6379database: 0timeout: 3000mslettuce:pool:max-active: 8max-wait: -1msmax-idle: 8min-idle: 0# 滑动窗口for i in {1..20}; docurl -i http://localhost:8080/api/test1done# 令牌桶 (突发流量 + 稳态速率测试)wrk -t2 -c20 -d30s http://localhost:8080/api/order

本文实现了基于 Redis 的 API 限流组件,支持:

滑动窗口限流:严格控制请求速率,高精度。令牌桶限流:支持突发流量,速率可控。多维度限流:方法 / IP / 用户。分布式支持:Redis + Lua 保证原子性,适合微服务架构。低侵入性:注解 + AOP 接入,业务无感知。可扩展:可接入 Prometheus 监控,或增加降级策略。

这种方案在网关、微服务、接口保护场景都能直接落地,是企业级 API 防护的最佳实践之一。

来源:甜甜圈科技

相关推荐