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

java通用自研接口限流组件

某业务中需要对后端接口进行限流,我们可以直接引入阿里巴巴的Sentinel快速实现,但是某企业中出于安全考虑,需要部门自己研发一套,可以采用Redis+Lua脚本+AOP+反射+自定义注解来实现
思路来源于链接

项目结构:
在这里插入图片描述
启动类:不需要额外加东西,默认即可
在这里插入图片描述
pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>org.example</groupId>
    <artifactId>my-rate-limit</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>my-rate-limit</name>
    <description>my-rate-limit</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>21</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.22.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

application.properties:配置redis相关

spring.application.name=my-rate-limit
server.port=8081
# redis
spring.data.redis.database=0
spring.data.redis.host=127.0.0.1
spring.data.redis.port=6379
spring.data.redis.password=
spring.data.redis.lettuce.pool.max-active=8
spring.data.redis.lettuce.pool.max-wait=-1ms
spring.data.redis.lettuce.pool.max-idle=8
spring.data.redis.lettuce.pool.min-idle=0

RedisConfig:常规的序列化配置

package org.example.myratelimit.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * RedisConfig
 * @author xiajunfeng
 * @date 2025/03/08
 */
@Configuration
@EnableAspectJAutoProxy //V2  开启AOP自动代理
public class RedisConfig
{
    /**
     * @param lettuceConnectionFactory
     * @return
     *
     * redis序列化的工具配置类,下面这个请一定开启配置
     * 127.0.0.1:6379> keys *
     * 1) "ord:102"  序列化过
     * 2) "\xac\xed\x00\x05t\x00\aord:102"   野生,没有序列化过
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory)
    {
        RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();

        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        //设置key序列化方式string
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //设置value的序列化方式json
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }
}

以上都为常规配置,配置完成后,开始写接口

RedisLimitController:接口中用到了自定义注解**@RedisLimitAnnotation**

package org.example.myratelimit.controller;

import lombok.extern.slf4j.Slf4j;
import org.example.myratelimit.annotation.RedisLimitAnnotation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * RedisLimitController
 * @author xiajunfeng
 * @date 2025/03/08
 */
@Slf4j
@RestController
public class RedisLimitController {

    /**
     * 10秒内最多访问3次
     * @return
     */
    @GetMapping("/redis/limit/test")
    @RedisLimitAnnotation(key = "redisLimit", permitsPerSecond = 3, expire = 10, msg = "当前访问人数较多,请稍后再试,自定义提示!")
    public String redisLimit()
    {
        return "正常业务返回,订单流水:xxxx" ;
    }

}

RedisLimitAnnotation:自定义注解,定义了key、最大访问次数、过期窗口时间、限流出现提示语

package org.example.myratelimit.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author xiajunfeng
 */

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface RedisLimitAnnotation {
    /**
     * 资源的key,唯一
     * 作用:不同的接口,不同的流量控制
     */
    String key() default "";

    /**
     * 最多的访问限制次数
     */
    long permitsPerSecond() default 3;

    /**
     * 过期时间(计算窗口时间),单位秒默认30
     */
    long expire() default 30;

    /**
     * 默认温馨提示语
     */
    String msg() default "default message:系统繁忙or你点击太快,请稍后再试,谢谢";
}


RedisLimitAop:使用AOP技术实现关键限流逻辑,调用了lua脚本

package org.example.myratelimit.aop;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.example.myratelimit.annotation.RedisLimitAnnotation;
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.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

/**
 * RedisLimitAop
 * @author xiajunfeng
 * @date 2025/03/08
 */
@Slf4j
@Aspect
@Component
public class RedisLimitAop {
    Object result = null;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     *  redis调用lua脚本
     */
    private DefaultRedisScript<Long> redisLuaScript;
    @PostConstruct
    public void init(){
        redisLuaScript = new DefaultRedisScript<>();
        redisLuaScript.setResultType(Long.class);
        redisLuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimiter.lua")));
    }

    @Around("@annotation(org.example.myratelimit.annotation.RedisLimitAnnotation)")
    public Object around(ProceedingJoinPoint joinPoint){
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        RedisLimitAnnotation redisLimitAnnotation = method.getAnnotation(RedisLimitAnnotation.class);
        // redis key
        String key = redisLimitAnnotation.key();
        // @RedisLimitAnnotation中的key不能为空
        if(key == null){
            throw new RuntimeException("请在注解属性中定义key");
        }
        // 最多的访问限制次数
        long limit = redisLimitAnnotation.permitsPerSecond();
        // 过期时间(计算窗口时间)
        long expire = redisLimitAnnotation.expire();
        // redis key 数组
        List<String> keys = new ArrayList<>();
        keys.add(key);

        // 执行lua脚本后的返回结果
        Long count = stringRedisTemplate.execute(
            redisLuaScript,
            keys,
            String.valueOf(limit),
            String.valueOf(expire)
        );
        // 如果结果为0,证明已经成功运行限流功能
        if (count == 0) {
            System.out.println("启动了限流功能,key: "+key);
            // 返回@RedisLimitAnnotation中自定义的msg内容
            return redisLimitAnnotation.msg();
        }

        try {
            //放行
            result = joinPoint.proceed();
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }

        return result;
    }

}

rateLimiter.lua:

--获取KEY,针对那个接口进行限流,Lua脚本中的数组索引默认是从1开始的而不是从零开始。
local key = KEYS[1]
--获取注解上标注的限流次数
local limit = tonumber(ARGV[1])

local curentLimit = tonumber(redis.call('get', key) or "0")

--超过限流次数直接返回零,否则再走else分支
if curentLimit + 1 > limit
then return 0
-- 首次直接进入
else
    -- 自增长 1
    redis.call('INCRBY', key, 1)
    -- 设置过期时间
    redis.call('EXPIRE', key, ARGV[2])
    return curentLimit + 1
end

--@RedisLimitAnnotation(key = "redisLimit", permitsPerSecond = 2, expire = 1, msg = "当前排队人数较多,请稍后再试!")

编写完成后,启动服务,访问http://localhost:8081/redis/limit/test,我的自定义注解配置为@RedisLimitAnnotation(key = “redisLimit”, permitsPerSecond = 3, expire = 10, msg = “当前访问人数较多,请稍后再试,自定义提示!”)
即10秒内最多访问3次,浏览器快速访问这个接口三次,浏览器出现错误提示,成功!
在这里插入图片描述

相关文章:

  • C++基础系列【20】数据结构与算法基础
  • 基于springboot的档案管理系统
  • 企业级VPN与零信任客户端的深度隐私防护指南
  • C语言练习题--洛谷P1781宇宙总统
  • 深入浅出解析 FreeRTOS 软件定时器 定时器服务任务:机制、API 详解及实践应用
  • 【算法】009、单双链表反转
  • C++什么是深复制和浅复制,构造函数和析构函数,哪一个可以写成虚函数,为什么?
  • 轻量级 Transformer 架构多模态预训练框架
  • 完全背包(排列问题)——爬楼梯
  • 模拟退火算法(Simulated Annealing):从物理学到优化的经典方法
  • leetcode刷题--超详解
  • 力扣203题:移除链表元素及代码实现分析
  • PAT线上考试注意细节(甲/乙级)
  • JDBC连接池技术详解
  • Linux网络基础
  • 【数据结构初阶第十八节】八大排序系列(上篇)—[详细动态图解+代码解析]
  • 在人工智能软件的帮助下学习编程实例
  • 力扣 Hot 100 刷题记录 - 随机链表的复制
  • 力扣hot100——多维动态规划
  • 线性代数笔记28--奇异值分解(SVD)
  • 学校网站开发背景/seo优化自动点击软件
  • 青岛公司网站建设公司排名/今日新闻摘抄二十条
  • 我的网站要怎样做才能让人家搜到/最新国际新闻事件今天
  • 黄页营销网站视频免费大全/免费关键词挖掘网站
  • 如何把做的网站与域名连接/宁波网站优化公司哪家好
  • 做网站的图片/seo实战培训教程