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

在 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 的限流需求,可根据实际业务场景调整参数。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Uniapp 原生组件层级过高问题及解决方案

文章目录 一、引言&#x1f3c5;二、问题描述&#x1f4cc;三、问题原因❓四、解决方案&#x1f4af;4.1 使用 cover-view 和 cover-image4.2 使用 subNVue 子窗体4.3 动态隐藏原生组件4.4 使用 v-if 或 v-show 控制组件显示4.5 使用 position: fixed 布局 五、总结&#x1f38…

x_xor_md5

x_xor_md5 打开附件&#xff0c;用010打开 发现有一行值是重复的。 根据题目提示我们把全文与重复行进行异或 str1 [0x01,0x78,0x0C,0x4C,0x10,0x9E,0x32,0x37,0x12,0x0C,0xFB,0xBA,0xCB,0x8F,0x6A,0x53]result[]txt [0x69,0x35,0x41,0x01,0x1C,0x9E,0x75,0x78,0x5D,0x48,0xF…

【C语言标准库函数】标准输入输出函数详解[6]:字符文件读取写入

目录 一、字符文件写入函数 1.1. fputc函数 1.1.1. fputc函数简介 1.1.2. 使用场景 1.1.3. 注意事项 1.1.4. 示例 1.2. fputs函数 1.2.1. fputs函数简介 1.2.2. 使用场景 1.2.3. 注意事项 1.2.4. 示例 二、字符文件读取函数 2.1. fgetc函数 2.1.1. 函数简介 2.1…

基于深度学习的半导体故障诊断与寿命预测算法研究

摘要 半导体产业在现代科技领域占据着核心地位&#xff0c;其设备的稳定性和可靠性至关重要。传统的半导体故障诊断和寿命预测方法存在效率低、准确性差等问题。本文聚焦于深度学习算法在半导体故障诊断与寿命预测方面的应用&#xff0c;详细阐述了卷积神经网络&#xff08;CN…

制药行业 BI 可视化数据分析方案

一、行业背景 随着医药行业数字化转型的深入&#xff0c;企业积累了海量的数据&#xff0c;包括销售数据、生产数据、研发数据、市场数据等。如何利用这些数据&#xff0c;挖掘其价值&#xff0c;为企业决策提供支持&#xff0c;成为医药企业面临的重大挑战。在当今竞争激烈的…

基于Python flask-sqlalchemy的SQLServer数据库管理平台

适应场景&#xff1a; 主要用于帮助DBA自动化很多日常工作&#xff0c;包括&#xff1a; 数据库状态监控 性能问题诊断 日志分析 自动巡检 问题告警 系统截图&#xff1a; main.py from flask import Blueprint, render_template, request, flash, redirect, url_for f…

TensorRT 8.6.1教程1-TensorRT简介

区分计算节点和数据节点 视频 TensorRT 教程 | 基于 8.6.1 版本 | 第一部分_哔哩哔哩_bilibili cookbook

MySQL创建存储过程和存储函数

【图书推荐】《MySQL 9从入门到性能优化&#xff08;视频教学版&#xff09;》-CSDN博客 《MySQL 9从入门到性能优化&#xff08;视频教学版&#xff09;&#xff08;数据库技术丛书&#xff09;》(王英英)【摘要 书评 试读】- 京东图书 (jd.com) MySQL9数据库技术_夏天又到了…

【项目总结】易到家家政服务平台 —— 派单调度(7)

派单调度需求分析 在抢单业务中&#xff0c;用户下单成功由服务人员或机构进行抢单&#xff0c;抢单成功服务人员上门服务&#xff0c;除了抢单业务系统还设计了派单业务&#xff0c;由系统根据用户订单的特点自动派给合适的服务人员。 流程如下&#xff1a; 首先获取待分配…

visual studio 在kylin v10上跨平台编译时c++标准库提示缺少无法打开的问题解决

情况1&#xff1a;提示无法打开 源文件 "string"之类导致无法编译 情况2:能编译&#xff0c;但无法打开这些库文件或标准库使用提示下划红色问题 解决方案&#xff1a; 一、通过工具->选项->跨平台里&#xff0c;在“远程标头IntelliSense管理器”更新下载一下…

SpringCould+vue3项目的后台用户管理的CURD【Taurus教育平台】

文章目录 一.SpringCouldvue3项目的后台用户管理的CURD【Taurus教育平台】 1.1 背景 二.用户列表&#xff08;分页查询&#xff09; 2.1 前端Vue3 &#xff08;Vue3-Element-Admin&#xff09;2.2 后端SpringCould 处理 三. 用户信息删除 3.1 前端Vue3 &#xff08;Vue3-Eleme…

Eclipse 插件开发相关概念

整理了Eclipse插件开发的概念&#xff0c;用于熟悉入门 SWT&#xff08;Standard Widget Toolkit&#xff09;标准图形工具箱 Java开发的GUI程序技术&#xff0c;由Eclipse开发&#xff0c;相比AWT、Swing更美观&#xff1b;对于目标平台上已经有的控件&#xff0c;SWT会直接使…

算法之 数论

文章目录 质数判断质数3115.质数的最大距离 质数筛选204.计数质数2761.和等于目标值的质数对 2521.数组乘积中的不同质因数数目 质数 质数的定义&#xff1a;除了本身和1&#xff0c;不能被其他小于它的数整除&#xff0c;最小的质数是 2 求解质数的几种方法 法1&#xff0c;根…

AndroidStudio查看Sqlite和SharedPreference

1.查看Sqlite 使用App Inspection&#xff0c;这是个好东西 打开方式&#xff1a;View → Tool Windows → App Inspection 界面如图&#xff1a; App inspection不但可以看Sqlite还可以抓包network和background task连抓包工具都省了。 非常好使 2.查看sharedPreference 使…

谈一谈数据库中的死锁问题

文章目录 死锁是什么&#xff1f;死锁的四个必要条件避免死锁的策略 本篇文章是基于《MySQL45讲》来写的个人理解与感悟。 死锁是什么&#xff1f; 死锁是指两个或两个以上的进程在执行过程中&#xff0c;由于竞争资源或者由于彼此通信而造成的一种阻塞的现象。若无外力作用&a…

网络工程师 (31)VLAN

前言 VLAN&#xff08;Virtual Local Area Network&#xff09;即虚拟局域网&#xff0c;是一种将物理局域网划分成多个逻辑上独立的虚拟网络的技术。 一、定义与特点 定义&#xff1a;VLAN是对连接到的第二层交换机端口的网络用户的逻辑分段&#xff0c;不受网络用户的物理位置…

从深入理解 netty——》AI

想了很久&#xff0c;准备写一个系列从深入理解 netty——》AI。 先说下为啥要从netty开始&#xff0c;看看netty的重要性 rocketmq异步消息组件nacos微服务注册中心spring cloud gateway网关redission分布式缓存es全文检索sentinel流量控制&#xff0c;服务保护seata分布式…

从 0 开始本地部署 DeepSeek:详细步骤 + 避坑指南 + 构建可视化(安装在D盘)

个人主页&#xff1a;chian-ocean 前言&#xff1a; 随着人工智能技术的迅速发展&#xff0c;大语言模型在各个行业中得到了广泛应用。DeepSeek 作为一个新兴的 AI 公司&#xff0c;凭借其高效的 AI 模型和开源的优势&#xff0c;吸引了越来越多的开发者和企业关注。为了更好地…

在anaconda环境中构建flask项目的exe文件

一、创建并激活虚拟环境 conda create -n flask_env python3.9 # python版本根据项目需求安装 conda activate flask_env # 激活环境二、安装必要依赖 推荐使用conda&#xff0c;pip没尝试过&#xff0c;但是deepseek给出了命令 conda install flask …

腾讯云服务器中Ubuntu18.04搭建python3.7.0与TensorFlow1.15.0与R-4.0.3环境

所有踩过的坑&#xff0c;都化成了这条平坦的路 云服务器配置 基础配置选择竞价实例&#xff08;便宜/需求小&#xff09; 选择地区&#xff08;距离自己近的就行&#xff09; 实例配置选择异构计算&#xff08;能力较强&#xff0c;性价比高&#xff09;根据GPU显存需求选择…