当前位置: 首页 > news >正文

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 限流组件,支持:

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

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

http://www.dtcms.com/a/549736.html

相关文章:

  • *清理磁盘空间
  • 用什么软件做网站原型外贸退税流程及方法
  • 微软网站制作软件常见营销策略都有哪些
  • 全栈开源:一套源码快速构建电竞/体育直播平台(PC+H5+双端APP)
  • 淘宝网站维护用DW做的网站怎么弄成链接
  • 【C++】【常见面试题】最简版带大小和超时限制的LRU缓存实现
  • CSAPP实验2:Bomb
  • [人工智能-大模型-118]:模型层 - RNN状态记忆是如何实现的?是通过带权重的神经元,还是通过张量?
  • 手机做网站需要多少天国外代理ip地址和端口
  • Unity-WebGL开发——用IIS(Internet Information Services)部署webGL工程
  • 怎么仿制别人的网站一个网站的建设流程有哪些
  • 学生信息管理系统oracle
  • Angular【router路由】
  • 如何网站建设网页宣传片拍摄报价明细
  • 曲阜做网站智能建造概论
  • 嘉峪关网站seo英文商城网站
  • 易混淆的点:栈的增长方向 和 缓冲区内的数据写入方向是相反的
  • 全流程掌握生态环评核心技术——涵盖生物量模拟、生物多样性计算、脆弱度评价及公路铁路、机场、水利项目实战
  • 【Embedded System】嵌入式C语言基础知识
  • PsTools 学习笔记(7.4):PsExec —— 远程进程的退出与控制台输出重定向
  • 双端迭代器:从 `next_back()` 到零拷贝“滑动窗口”——Rust DoubleEndedIterator 全景指南
  • 模型过拟合基本解决办法说明(个人学习向)
  • 自己架设服务器做网站厦门网络推广外包
  • 八年级信息做网站所用软件网站备案核实单
  • 如何用 Python xlwings库自动化操作 Excel?
  • 基于MATLAB的梯度投影稀疏重建算法
  • [特殊字符] FBro工作流自动化平台 - 让浏览器自动化更简单
  • JAVA后端结合网页搜图+阿里万相2.5实现自动化修图与返回
  • 和平区网站制作手机企业网站怎么做
  • 如何在好医生网站做二类学分dede中英文网站切换