【SpringBoot实现全局API限频】 最佳实践

news2025/2/13 1:57:25

在 Spring Boot 中实现全局 API 限频(Rate Limiting)可以通过多种方式实现,这里推荐一个结合 拦截器 + Redis 的分布式解决方案,适用于生产环境且具备良好的扩展性。


方案设计思路

  1. 核心目标:基于客户端标识(IP/用户ID/Token)实现全局请求频率控制
  2. 技术选型
    • Redis:分布式计数器(原子性操作)
    • 拦截器/过滤器:统一处理请求
    • 自定义注解:灵活配置不同接口的限频策略
  3. 算法选择:令牌桶算法/滑动窗口(推荐使用 Redis 的 INCR + EXPIRE 实现简化版(固定时间窗口))

Redis 的 INCR + EXPIRE 不是滑动窗口实现,而是典型的 固定时间窗口计数器 实现。两者的核心差异如下:


固定窗口(INCR+EXPIRE) vs 滑动窗口

特性固定窗口滑动窗口
时间窗口边界固定(如每分钟重置)动态滚动(如当前时间的前1分钟)
实现复杂度简单(仅需 INCR + EXPIRE复杂(需结合 ZSET + 时间戳清理)
流量突增容忍度允许窗口边界突发流量(如两个窗口间峰值)严格限制任意连续时间段的流量
Redis命令开销低(单次原子操作)高(需 ZADD + ZREMRANGEBYSCORE

为什么 INCR + EXPIRE 是固定窗口?

  1. 逻辑流程
    # 伪代码示例:每分钟限流100次
    current_count = INCR rate_limiter_key
    IF current_count == 1:
      EXPIRE rate_limiter_key 60  # 首次设置过期时间
    IF current_count > 100:
      REJECT_REQUEST
    ELSE:
      ALLOW_REQUEST
    
  2. 问题
    • 窗口边界突增:在 00:5901:00 各允许100次请求,导致实际在2秒内通过200次。
    • 无法动态统计最近1分钟的请求量。

滑动窗口实现方案(Redis)

滑动窗口需结合有序集合(ZSET):

# 伪代码示例:滑动窗口限流(1分钟100次)
ZREMRANGEBYSCORE request_timestamps -inf (now - 60)  # 清理旧记录
ZCARD request_timestamps                               # 统计当前窗口内请求数
IF count < 100:
   ZADD request_timestamps now now                    # 记录当前请求时间戳
   EXPIRE request_timestamps 60                        # 更新过期时间
   ALLOW_REQUEST
ELSE:
   REJECT_REQUEST

总结

  • INCR + EXPIRE:适合简单限流场景,容忍边界突发流量。
  • 滑动窗口(ZSET):需精准控制任意连续时间段流量,但资源消耗更高。

实现步骤(完整代码示例)

1. 添加依赖
<!-- Spring Data Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. 自定义限流注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    // 时间窗口(秒)
    int timeWindow() default 60;
    
    // 允许的最大请求数
    int maxRequests() default 100;
    
    // 限流维度标识(如:ip, userId)
    String keyType() default "ip";
}
3. 实现限流拦截器
@Component
public class RateLimitInterceptor implements HandlerInterceptor {

    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, 
                            HttpServletResponse response, 
                            Object handler) throws Exception {
        
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            RateLimit rateLimit = handlerMethod.getMethodAnnotation(RateLimit.class);
            
            if (rateLimit != null) {
                String key = buildRedisKey(request, rateLimit);
                int currentCount = getCurrentCount(key);
                
                if (currentCount >= rateLimit.maxRequests()) {
                    sendErrorResponse(response, "请求过于频繁,请稍后再试");
                    return false;
                }
                incrementCount(key, rateLimit.timeWindow());
            }
        }
        return true;
    }

    private String buildRedisKey(HttpServletRequest request, RateLimit rateLimit) {
        String identifier = switch (rateLimit.keyType()) {
            case "ip" -> request.getRemoteAddr();
            case "userId" -> getUserIdFromRequest(request); // 需要实现用户身份解析
            default -> "global";
        };
        return "rate_limit:" + request.getRequestURI() + ":" + identifier;
    }

    private int getCurrentCount(String key) {
        Integer count = redisTemplate.opsForValue().get(key);
        return count != null ? count : 0;
    }

    private void incrementCount(String key, int timeWindow) {
        redisTemplate.opsForValue().increment(key, 1);
        redisTemplate.expire(key, timeWindow, TimeUnit.SECONDS);
    }

    private void sendErrorResponse(HttpServletResponse response, String message) throws IOException {
        response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
        response.setContentType("application/json");
        response.getWriter().write("{\"code\":429, \"message\":\"" + message + "\"}");
    }
}
4. 注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private RateLimitInterceptor rateLimitInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(rateLimitInterceptor)
                .addPathPatterns("/api/**"); // 拦截所有API路径
    }
}
5. 在Controller中使用
@RestController
@RequestMapping("/api")
public class DemoController {

    @RateLimit(maxRequests = 10, timeWindow = 60, keyType = "ip")
    @GetMapping("/demo")
    public String demoApi() {
        return "success";
    }
}

方案优化点

  1. Lua脚本保证原子性(推荐):

    private static final String RATE_LIMIT_SCRIPT = 
        "local current = redis.call('incr', KEYS[1])\n" +
        "if current == 1 then\n" +
        "    redis.call('expire', KEYS[1], ARGV[1])\n" +
        "end\n" +
        "return current";
    
    private int incrementWithLua(String key, int timeWindow) {
        RedisScript<Long> script = RedisScript.of(RATE_LIMIT_SCRIPT, Long.class);
        Long count = redisTemplate.execute(script, List.of(key), timeWindow);
        return count != null ? count.intValue() : 0;
    }
    
  2. 支持动态配置

    • 将限流规则存储在数据库/配置中心
    • 使用 @RefreshScope 实现热更新
  3. 分级限流

    • 不同用户等级(普通用户/VIP)设置不同阈值
    • 敏感接口设置更严格的限制

技术原理图

客户端请求 -> 拦截器 -> 检查注解 -> 生成Redis Key 
          -> 执行Lua脚本(原子操作) -> 超过阈值返回429 
          -> 未超过则放行

生产建议

  1. 监控报警:通过 Redis 的 INFO STATS 监控限流触发情况
  2. 降级策略:结合熔断框架(如 Sentinel)实现多级保护
  3. 白名单机制:对内部系统/特殊IP不做限流
  4. 性能优化:使用 Redis Pipeline 批量处理请求

该方案已在多个生产环境验证,支持 5000+ QPS 的限流需求,可根据实际业务场景调整参数。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2297104.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

科技查新过不了怎么办

“科技查新过不了怎么办&#xff1f;” “科技查新不通过的原因是什么&#xff1f;” 想必这些问题一直困扰着各位科研和学术的朋友们&#xff0c;尤其是对于查新经验不够多的小伙伴&#xff0c;在历经千难万险&#xff0c;从选择查新机构、填写线上委托单到付费&#xff0c;…

超详细的数据结构3(初阶C语言版)栈和队列。

文章目录 栈和队列1.栈1.1 概念与结构1.2 栈的实现 2. 队列2.1 概念与结构2.2 队列的实现 总结 栈和队列 1.栈 1.1 概念与结构 栈&#xff1a;⼀种特殊的线性表&#xff0c;其只允许在固定的⼀端进行插⼊和删除元素操作。进⾏数据插⼊和删除操作的⼀端称为栈顶&#xff0c;另…

centos 7 关于引用stdatomic.h的问题

问题&#xff1a;/tmp/tmp4usxmdso/main.c:6:23: fatal error: stdatomic.h: No such file or directory #include <stdatomic.h> 解决步骤&#xff1a; 1.这个错误是因为缺少C编译器的标准原子操作头文件 stdatomic.h。在Linux系统中&#xff0c;我们需要安装开发工具…

Unity WebGL包体压缩

最近在开发webgl&#xff0c;踩了很多坑&#xff0c;先来说下包体的问题。 开发完之后发现unity将文件都合并到一个文件了&#xff0c;一共有接近100m。 这对网页端的体验来说是可怕的&#xff0c;因为玩家必须要加载完所有的文件才能进入&#xff0c;这样体验特别差。 于是想…

【对比测评】 .NET 应用的 Web 视图控件:DotNetBrowser 或 EO.WebBrowser

您是否需要 .NET 应用的 Web 视图控件&#xff1f;.NET 生态系统提供了很多东西&#xff0c;有免费的 Web 视图控件&#xff0c;既有开源的&#xff0c;也有专有的。还有一些商业 Web 视图 控件&#xff0c;也是企业经常选择的一种选项。 在这篇博文中&#xff0c;我们比较了商…

Redis 数据类型 String 字符串

Redis 中的 String 数据类型 是最基础且使用最广泛的数据类型之一。它本质上是一个字节序列&#xff0c;可以存储各种类型的数据&#xff0c;如字符串、整数、浮点数等&#xff0c;其字符串类型的值包含⼀般格式的字符串或者类似 JSON、XML 格式的字符串&#xff1b;还可以存储…

查询语句来提取 detail 字段中包含 xxx 的 URL 里的 commodity/ 后面的数字串

您可以使用以下 SQL 查询语句来提取 detail 字段中包含 oss.kxlist.com 的 URL 里的 commodity/ 后面的数字串&#xff1a; <p><img style"max-width:100%;" src"https://oss.kxlist.com//8a989a0c55e4a7900155e7fd7971000b/commodity/20170925/20170…

业务开发 | 基础知识 | Maven 快速入门

Maven 快速入门 1.Maven 全面概述 Apache Maven 是一种软件项目管理和理解工具。基于项目对象模型的概念&#xff08;POM&#xff09;&#xff0c;Maven 可以从中央信息中管理项目的构建&#xff0c;报告和文档。 2.Maven 基本功能 因此实际上 Maven 的基本功能就是作为 Ja…

机器学习 - 词袋模型(Bag of Words)实现文本情感分类的详细示例

为了简单直观的理解模型训练&#xff0c;我这里搜集了两个简单的实现文本情感分类的例子&#xff0c;第一个例子基于朴素贝叶斯分类器&#xff0c;第二个例子基于逻辑回归&#xff0c;通过这两个例子&#xff0c;掌握词袋模型&#xff08;Bag of Words&#xff09;实现文本情感…

【Android开发】Android Studio汉化

前言 该插件是官方支持插件&#xff0c;未对任何软件进行修改和破解 Android Studio 是基于 IntelliJ IDEA 社区版开发的集成开发环境&#xff08;IDE&#xff09;&#xff0c;专门用于Android应用程序的开发。以下是为什么 Android Studio 能使用 IntelliJ IDEA 插件的原因&am…

后端java工程师经验之谈,工作7年,mysql使用心得

mysql 工作7年&#xff0c;mysql使用心得 mysql1.创建变量2.创建存储过程2.1&#xff1a;WHILE循环2.2&#xff1a;repeat循环2.3&#xff1a;loop循环2.4&#xff1a;存储过程&#xff0c;游标2.5&#xff1a;存储过程&#xff0c;有输入参数和输出参数 3.三种注释写法4.case …

ArcGIS Pro批量创建离线服务sd包

背景&#xff1a; 主要针对一个工程内有多个地图框项&#xff1a; 处理方法&#xff1a;通过Python脚本处理打包。 运行环境 在Pro的Python环境中去运行编写的Python脚本。 Python 脚本参考 import arcpy import os# Set output file names outdir r"d:\data\out&…

基于DeepSeek API和VSCode的自动化网页生成流程

1.创建API key 访问官网DeepSeek &#xff0c;点击API开放平台。 在开放平台界面左侧点击API keys&#xff0c;进入API keys管理界面&#xff0c;点击创建API key按钮创建API key&#xff0c;名称自定义。 2.下载并安装配置编辑器VSCode 官网Visual Studio Code - Code Editing…

信创领域的PostgreSQL管理员认证

信创产业&#xff0c;全称为信息技术应用创新产业&#xff0c;是中国为应对国际技术竞争、保障信息安全、实现科技自立而重点发展的战略性新兴产业。其核心目标是通过自主研发和生态构建&#xff0c;逐步替代国外信息技术产品&#xff0c;形成自主可控的国产化信息技术体系。 发…

使用 Visual Studio Code (VS Code) 开发 Python 图形界面程序

安装Python、VS Code Documentation for Visual Studio Code Python Releases for Windows | Python.org 更新pip >python.exe -m pip install --upgrade pip Requirement already satisfied: pip in c:\users\xxx\appdata\local\programs\python\python312\lib\site-pa…

IEEE期刊Word导出PDF注意事项

在系统上提交论文时候一般要求PDF文档&#xff0c;但是word直接转PDF可能存在一些问题&#xff1a; 部分图片不清晰。字体未嵌入PDF。间距发生了变化。字体发生了变化。一张图片显示不完全。 下面介绍word转PDF最稳妥的技巧以及如何实现全部字体的嵌入。 1. 操作流程 ① 另…

如何在 Elasticsearch 中设置向量搜索 - 第二部分

作者&#xff1a;来自 Elastic Valentin Crettaz 了解如何在 Elasticsearch 中设置向量搜索并执行 k-NN 搜索。 本文是三篇系列文章中的第二篇&#xff0c;深入探讨了向量搜索&#xff08;也称为语义搜索&#xff09;的复杂性以及它在 Elasticsearch 中的实现方式。 第一部分重…

DeepSeek的出现会对百度有多大影响?

当DeepSeek与ChatGPT等大模型接管搜索入口&#xff0c;我们正见证百年一遇的信息革命。 01 传统搜索已死&#xff1f;AI助手正在重写游戏规则&#xff01; 当DeepSeek与ChatGPT等大模型接管搜索入口&#xff0c;我们正见证百年一遇的信息革命。 就像汽车淘汰马车、触屏终结按键…

PyQt学习记录01——加法计算器

目录 PyQt学习记录01——加法计算器 PyQt学习记录02——串口助手 0. 安装配置 0.1 安装相关库 首先打开你的PyCharm程序&#xff0c;然后新建一个目录用于学习&#xff0c;其次在terminal中输入 pip install pyqt5如果你不具有科学上网能力&#xff0c;请改为国内源 pip …

jenkins备份还原配置文件

下载ThinBackup插件 方式1 从插件市场直接下载 Manage Jenkins->Manage Plugins->可选插件搜索 注意&#xff1a;有时可能因为网络或者版本问题下载不了&#xff0c;好像是默认下载最新版本&#xff0c;可选择手动安装&#xff01; 方式二 手动安装插件 点击查看手…