Spring Boot通过自定义注解和Redis+Lua脚本实现接口限流

news2024/11/28 6:26:02

在这里插入图片描述

😄 19年之后由于某些原因断更了三年,23年重新扬帆起航,推出更多优质博文,希望大家多多支持~
🌷 古之立大事者,不惟有超世之才,亦必有坚忍不拔之志
🎐 个人CSND主页——Micro麦可乐的博客
🐥《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程,入门到实战
🌺《RabbitMQ》专栏主要介绍使用JAVA开发RabbitMQ的系列教程,从基础知识到项目实战
🌸《设计模式》专栏以实际的生活场景为案例进行讲解,让大家对设计模式有一个更清晰的理解
💕《Jenkins实战》专栏主要介绍Jenkins+Docker的实战教程,让你快速掌握项目CI/CD,是2024年最新的实战教程
🌞《Spring Boot》专栏主要介绍我们日常工作项目中经常应用到的功能以及技巧,代码样例完整
如果文章能够给大家带来一定的帮助!欢迎关注、评论互动~

Spring Boot通过自定义注解和Redis+Lua脚本实现接口限流

  • 前言
  • 操作思路
  • 这么做有什么优势
  • 开始实现
    • ❶ 项目初始化
    • ❷ 创建限流注解
    • ❸ 创建Lua脚本
    • ❹ 创建Redis处理器
    • ❺ 编写限流切面
    • ❻ 编写Controller
  • 接口测试
  • 总结

前言

本文源码下载地址:https://download.csdn.net/download/lhmyy521125/89412365

在我们日常开发的项目中为了保证系统的稳定性,很多时候我们需要对系统接口做限流处理,它可以有效防止恶意请求对系统造成过载。常见的限流方案主要有:

  • 网关限流NGINXZuul 等 API 网关
  • 服务器端限流:服务端接口限流
  • 令牌桶算法:通过定期生成令牌放入桶中,请求需要消耗令牌才能通过
  • 熔断机制HystrixResilience4j

之前博主写过了一篇 【使用Spring Boot自定义注解 + AOP实现基于IP的接口限流和黑白名单】,在一些小型应用中,足以满足我们的需求,但是在并发量大的时候,就会有点力不从心,本章节博主将给大家介绍
使用自定义注解和 Redis+Lua脚本实现接口限流

操作思路

使用redis
Redis是一种高性能的键值存储系统,支持多种数据结构。由于其高吞吐量和低延迟的特点,Redis非常适合用于限流

应用Lua脚本
Lua脚本可以在Redis中原子执行多条命令。通过在Redis中执行Lua脚本,可以确保限流操作的原子性和一致性

限流策略
本文我们将采用类似令牌桶算法(Token Bucket)来实现限流

令牌桶算法的基本思想:系统会以固定的速率向桶中加入令牌,每次请求都需要消耗一个令牌,当桶中没有令牌时,拒绝请求

这么做有什么优势

高效性
Redis以其高性能著称,每秒可以处理数十万次操作。使用Redis进行限流,确保了在高并发场景下的高效性。同时,Lua脚本在Redis中的执行是原子的,这意味着脚本中的一系列命令要么全部执行,要么全部不执行,避免了竞争条件,确保了限流逻辑的一致性

灵活性
通过自定义注解,我们可以为不同的接口设置不同的限流策略,而不需要修改大量的代码。这种方法允许开发者根据实际需求灵活地调整限流参数,例如每秒允许的请求数和令牌的有效期,从而更好地应对不同的业务场景

易于维护和扩展
使用Spring AOP和注解,可以方便地将限流逻辑应用于不同的接口。这种方式不仅减少了代码的耦合度,还使得限流逻辑的维护和扩展变得更加简单。例如,当需要为某个新的接口添加限流时,只需在方法上添加相应的注解即可,而不需要在代码中加入复杂的限流逻辑

分布式限流
Redis作为一个分布式缓存系统,可以方便地部署在集群环境中,实现分布式限流。通过将限流数据存储在Redis中,可以在多个应用实例之间共享限流状态,确保在分布式环境下限流策略的一致性

开始实现

❶ 项目初始化

首先,创建一个 Spring Boot 项目,并添加必要的依赖。在 pom.xml 文件中添加以下内容:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
</dependencies>

配置 application.yml 加入 redis 配置

spring:
    #redis
    redis:
        # 地址
        host: 127.0.0.1
        # 端口,默认为6379
        port: 6379
        # 数据库索引
        database: 0
        # 密码
        password:
        # 连接超时时间
        timeout: 10s
        lettuce:
            pool:
                # 连接池中的最小空闲连接
                min-idle: 0
                # 连接池中的最大空闲连接
                max-idle: 8
                # 连接池的最大数据库连接数
                max-active: 8
                # #连接池最大阻塞等待时间(使用负值表示没有限制)
                max-wait: -1ms

❷ 创建限流注解

定义一个自定义注解RateLimit,主要有三个属性 限流的key允许的请求数令牌有效期

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
    String key() default "";  // 限流的key
    int limit() default 10;   // 每秒允许的请求数
    int timeout() default 1;  // 令牌有效期(秒)
}

❸ 创建Lua脚本

编写一个Lua脚本,用于限流操作:

-- rate_limit.lua
-- 获取限流的键(标识符)
local key = KEYS[1]
-- 获取每秒允许的最大请求数
local limit = tonumber(ARGV[1])
-- 获取键的过期时间(秒)
local expire_time = tonumber(ARGV[2])

-- 获取当前的请求数
local current = redis.call('get', key)
-- 如果当前请求数存在且已经超过或达到限制,返回0(拒绝请求)
if current and tonumber(current) >= limit then
    return 0
else
    -- 如果当前请求数不存在或未超过限制,增加请求数
    current = redis.call('incr', key)
    -- 如果这是第一次请求,设置过期时间
    if tonumber(current) == 1 then
        redis.call('expire', key, expire_time)
    end
    -- 返回1(允许请求)
    return 1
end

脚本工作原理总结

  • 每次请求进来时,脚本会首先获取当前的请求数。
  • 如果请求数已经达到设定的限制,则拒绝该请求。
  • 否则,增加请求数,并在首次请求时设置过期时间。
  • 返回结果表示是否允许请求。

❹ 创建Redis处理器

创建Redis处理器,用于执行Lua脚本:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;

import java.util.Collections;

@Component
public class RedisRateLimitHandler {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private DefaultRedisScript<Long> redisScript;

    public RedisRateLimitHandler() {
        redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rate_limit.lua")));
        redisScript.setResultType(Long.class);
    }

    public boolean isAllowed(String key, int limit, int expireTime) {
        Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), String.valueOf(limit), String.valueOf(expireTime));
        return result != null && result == 1;
    }
}

❺ 编写限流切面

使用 AOP 实现限流逻辑,IP判断模拟用户判断

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

@Aspect
@Component
public class RateLimitAspect {

    @Autowired
    private RedisRateLimitHandler redisRateLimitHandler;

    @Autowired
    private HttpServletRequest request;

    @Around("@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
        String key = rateLimit.key();
        int limit = rateLimit.limit();
        int expireTime = rateLimit.timeout();
        switch (key) {
            case LimitTypeConstants.IP:
                //获取IP地址
                key = request.getRemoteAddr();
                break;
            case LimitTypeConstants.USER:
                /**
                 *   模拟当前获取当前用户限流配置 比如高级会员 1小时允许请求多少次普通会员允许多少次
                 *   key = user.token;
                 *   limit = user.user.token;
                 *   expireTime = 3600 //1小时;
                 */
                key = "user-token";
                break;
            default:
                key = rateLimit.key();
                break;
        }

        boolean allowed = redisRateLimitHandler.isAllowed(key, limit, expireTime);
        if (allowed) {
            return joinPoint.proceed();
        } else {
            throw new RuntimeException("请求太多-超出速率限制");
        }
    }
}

❻ 编写Controller

创建一个简单的限流测试Controller,并在需要限流的方法上使用 @RateLimit 注解,需要编写异常处理,返回 RateLimitAspect 异常信息,并以字符串形式返回

import com.toher.lua.limit.LimitTypeConstants;
import com.toher.lua.limit.RateLimit;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/api")
@RestController
public class RedisLuaController {

    //由于是简单的测试项目,这里就直接定义异常处理,并为采用全局异常处理
    @ExceptionHandler(value = Exception.class)
    public String handleException(Exception ex) {
        return ex.getMessage();
    }


    @GetMapping("/limit-ip")
    @RateLimit(key = LimitTypeConstants.IP, limit = 5, timeout = 30)
    public String rateLimitIp() {
        return "IP Request successful!";
    }


    @GetMapping("/limit-export")
    @RateLimit(key = LimitTypeConstants.USER, limit = 5, timeout = 30)
    public String rateLimitUser(){
        return "USER Request successful!";
    }

    @GetMapping("/limit")
    @RateLimit(key = "customer", limit = 5, timeout = 30)
    public String rateLimit(){
        return "customer Request successful!";
    }
}

接口测试

使用接口调试工具,请求接口测试,博主这里使用的是 Apifox,我们30秒内请求5次
前5次均返回 Request successful!
第6次会提示 请求太多-超出速率限制
在这里插入图片描述

总结

通过本文的步骤,我们成功地在Spring Boot项目中结合RedisLua脚本实现了一个灵活高效的接口限流功能。通过自定义注解AOP切面,可以方便地为不同的接口设置不同的限流策略。

如果本文对您有所帮助,希望 一键三连 给博主一点点鼓励,如果您有任何疑问或建议,请随时留言讨论。


在这里插入图片描述

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

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

相关文章

关于python中的列表和元组

变量就是一块内存空间&#xff0c;用来表示/存储数据 如果表示的数据较少&#xff0c;直接定义几个变量就行了 但是也有的时候&#xff0c;要表示的数据就比较多&#xff0c;如果只是通过定义一个变量来表示一个数据的话&#xff0c;这样的工作效率太低&#xff0c; 所以在p…

C++基础教程

目录 一.简介 二.基本语法 三. 面向对象 四.总结 一.简介 C 是一种高级语言&#xff0c;它是由 Bjarne Stroustrup 于 1979 年在贝尔实验室开始设计开发的。C 进一步扩充和完善了 C 语言&#xff0c;是一种面向对象的程序设计语言。C 可运行于多种平台上&#xff0c;如 Win…

梯度下降: 02. 批量梯度下降BGD,随机梯度下降SGD,小批量梯度下降MBGD

简介 本文从原理上介绍了三种梯度下降的方法,相同点,异同点,优缺点。 内容包含了数学公式的推导与说明 1. 梯度下降的3种方法 梯度下降分三类,原理基本相同,操作方式略有区别 批量梯度下降BGD(BatchGradient Descent):使用全量数据进行特征抽取,模型训练小批量梯度下降…

【C++题解】1265. 爱因斯坦的数学题

问题&#xff1a;1265. 爱因斯坦的数学题 类型&#xff1a;简单循环 题目描述&#xff1a; 爱因斯坦出了一道这样的数学题&#xff1a;有一条长阶梯&#xff0c;若每步跨 2 阶&#xff0c;则最最后剩一阶&#xff0c;若每步跨 3 阶&#xff0c;则最后剩 2 阶&#xff0c;若每…

webapi跨越问题

由于浏览器存在同源策略&#xff0c;为了防止 钓鱼问题&#xff0c;浏览器直接请求才不会有跨越的问题 浏览器要求JavaScript或Cookie只能访问同域下的内容 浏览器也是一个应用程序&#xff0c;有很多限制&#xff0c;不能访问和使用电脑信息&#xff08;获取cpu、硬盘等&#…

JWT 从入门到精通

什么是 JWT JSON Web Token&#xff08;JWT&#xff09;是目前最流行的跨域身份验证解决方案 JSON Web Token Introduction - jwt.ioLearn about JSON Web Tokens, what are they, how they work, when and why you should use them.https://jwt.io/introduction 一、常见会…

Kimichat使用案例010:快速识别出图片中的表格保存到Excel

文章目录 一、介绍二、图片信息三、输入内容四、输出内容五、markdown提示词六、markdown输出一、介绍 如果有一张图片格式的表格,想要快速复制到Excel表格中,那么一般要借助于OCR工具。之前试过不少在线OCR工具,识别效果差强人意。其实,kimichat就可以非常好的完成这个任务…

王学岗鸿蒙开发(北向)——————(四、五、六)ArkUi声明式组件

普通组件 1,注意&#xff0c;如上图&#xff0c;build只能有一个根节点 2,Entry表示程序的入口 Component表示自定义的组件 Preview表示可以预览 3&#xff0c;图片存放的地方 4&#xff0c; Image组件最好只给宽度&#xff0c;给了高度又给宽度容易失真。 build() {Row() {/…

Scanpy(3)单细胞数据分析常规流程

单细胞数据分析常规流程 面对高效快速的要求上,使用R分析数据越来越困难,转战Python分析,我们通过scanpy官网去学习如何分析单细胞下游常规分析。 数据3k PBMC来自健康的志愿者,可从10x Genomics免费获得。在linux系统上,可以取消注释并运行以下操作来下载和解压缩数据。…

【Python机器学习】PCA——特征提取(2)

上一篇写过了用单一最近邻分类器训练后的精度只有0.22. 现在用PCA。想要度量人脸的相似度&#xff0c;计算原始像素空间中的距离是一种相当糟糕的方法。用像素表示来比较两张图像时&#xff0c;我们比较的是每个像素的灰度值与另一张图像对应位置的像素灰度值。这种表示与人们…

IDEA下项目发送到Gitee

一、首先在Gitee创建一个仓库&#xff08;什么都不选&#xff0c;这是最简单的方式&#xff0c;否则需要 pull push等一些操作&#xff0c;我嫌麻烦&#xff09; 二、按图点击&#xff08;创建存储区&#xff0c;选择你要上传的项目&#xff09; 三、按图点击后正常文件名会变绿…

信息安全与密码技术概述

1. 信息安全的法律法规 2016年11月7日&#xff0c;中华人民共和国第十二届全国人民代表大会常务委员会第二十四次会议通过《中华人民共和国网络安全法》&#xff0c;自2017年6月1日起施行。 2019年10月26日&#xff0c;中华人民共和国第十三届全国人民代表大会常务委员会第十四…

Ubuntu虚拟机使用纯命令行对根分区进行扩展

Ubuntu虚拟机使用纯命令行对根分区进行扩展 前排提示 因为Ubuntu再安装时&#xff0c;根分区是没有使用LVM进行磁盘管理的&#xff0c;所以如果想扩展根分区&#xff0c;我们不得不使用另外一种暴力的方法。简单来说就是利用fdisk删除原来的根分区再基于原来的起始块号重新建…

PHP超详细安装及应用

目录 所需安装包如下 一、PHP安装 依赖包安装 安装扩展工具&#xff08;先将PHP所需的软件包全部拖进centos根目录下&#xff09; 安装libmcrypt 安装mhash 安装mcrypt 安装PHP 二、设置LAMP组件环境&#xff08;要保证mysql、http都安装完成了&#xff09; Php.ini的建…

MySQL常用的库操作、表操作、INSERT、DELETE

库操作 查询数据库&#xff1a; show databases&#xff1b; 创建数据库&#xff1a; create database chat&#xff1b; 删除数据库&#xff1a; drop database chat&#xff1b; 选择数据库&#xff1a; use chat&#xff1b; 表操作 查询表&#xff1a; show tables&am…

数据库同步软件PanguSync常见错误解决方法

​​​​​​在部署PanguSync数据库同步软件的过程中&#xff0c;常常会遇见一些错误提示&#xff0c;某些老铁可能会一脸懵逼&#xff0c;本文对一些常见的错误信息进行了总结&#xff0c;并提供了解决方法。 1.")"附近有语法错误 该问题是由于源表未设置主键&…

中国现代书法第一人颜廷利:全球知名哲学家思想家教育家

在人生的旅途中&#xff0c;如果一个人的所有追求仅仅是为了满足自己的需求和欲望&#xff0c;而非出于善意、行善或造福他人&#xff0c;那么无论其遭受了多少苦难&#xff0c;这样的生活是难以赢得他人的同情或怜悯的。这是东方哲学家、科学家颜廷利教授在其著作《升命学说》…

如何借助ChatGPT写文献综述?从文献搜索到综述生成,顶级高效指令值得收藏

欢迎关注EssayBot&#xff0c;为大家带来最酷最有效的智能AI学术科研写作攻略。关于使用ChatGPT等AI工具的相关问题可以添加作者七哥沟通 在学术研究的过程中&#xff0c;文献综述是学术论文的核心组成部分&#xff0c;其主要目的在于对特定问题的先行研究成果进行总结和梳理&a…

往前冲,

最近换了个手机&#xff0c;原因是之前的手机存储空间一直不够&#xff0c;隔一段时间就提醒我删除东西&#xff0c;刚好那天去三诺出差&#xff0c;那边有一个工程师给我推荐了红米手机Turbo3&#xff0c;存储直接干到了1T。 第二天&#xff0c;就下定决心要换手机&#xff0c…

哈希表与哈希扩容

一&#xff0c;哈希表 哈希表简单的理解&#xff1a;在记录的存储位置和它的关键字之间建立一个确定的对应关系f&#xff0c;使每个关键字和结构中一个唯一的存储位置相对应。 哈希表基于数组的&#xff0c;正因为数组创建后难于扩展某些哈希表被基本填满时&#xff0c;性能下…