不能引入第3方组件,如何自研限流组件框架,赋能团队

目前市面上主流的限流中间件:

1、SpringCloud alibaba限流组件 Sentinel

Sentinel 是阿里巴巴开源的流量控制、熔断降级、系统保护组件,本文深入介绍了 Sentinel 的核心概念、规则配置、系统保护、熔断降级、流控策略以及与 Hystrix 的区别,帮助读者全面理解 Sentinel 的工作原理和使用方法。
官网:https://github.com/alibaba/Sentinel/wiki

2012年,Sentinel诞生于阿里巴巴,其主要目标是流量控制。2013-2017年,Sentinel迅速发展,并成为阿里巴巴所有微服务的基本组成部分。 它已在6000多个应用程序中使用,涵盖了几乎所有核心电子商务场景。2018年,Sentinel演变为一个开源项目。2020年,Sentinel Golang发布。

为什么有些企业不让引入外部框架呢?

  1. 我们内部封装或用的是Dubbo开发微服务,无法使用Cloud相关限流组件,你谈谈自研框架有过落地经验吗?
  2. 我们银行外包团队,安全性要求,不可以引入太多的组件,有自研思路吗?

那么具体要如何实现呢?

1、技术栈:Redis + Lua脚本 + AOP + 反射 + 自定义注解,打造自研限流组件。
2、环境搭建:

(1)新建Model
(2)改pom文件:

<?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.2.0</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>

  <groupId>top.leighteen.Interview</groupId>
  <artifactId>Interview</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>Interview</name>
  <description>Interview</description>

  <properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <hutool.version>5.8.27</hutool.version>
    <druid.version>1.1.20</druid.version>
    <mybatis.springboot.version>3.0.2</mybatis.springboot.version>
    <mysql.version>8.0.11</mysql.version>
    <mapper.version>4.2.3</mapper.version>
    <transmittable.version>2.14.3</transmittable.version>
    <persistence-api.version>1.0.2</persistence-api.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--hutool-->
    <dependency>
      <groupId>cn.hutool</groupId>
      <artifactId>hutool-all</artifactId>
      <version>${hutool.version}</version>
    </dependency>
    <!--bootstrap-->
    <!--<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
  </dependency>-->
    <!--nacos-config-->
    <!--<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
  </dependency>-->
    <!--nacos-discovery-->
    <!--<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  </dependency>-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-configuration-processor</artifactId>
      <optional>true</optional>
    </dependency>
    <!--lombok-->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
    <!--test-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <!--SpringBoot集成druid连接池-->
    <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid.version}</version>
        </dependency>
        <!--mybatis和springboot整合-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis.springboot.version}</version>
        </dependency>
        <!--Mysql数据库驱动8 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>
        <!--persistence-->
        <dependency>
            <groupId>javax.persistence</groupId>
            <artifactId>persistence-api</artifactId>
            <version>${persistence-api.version}</version>
        </dependency>
        <!--通用Mapper4-->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper</artifactId>
            <version>${mapper.version}</version>
        </dependency>
        <!-- 线程传递值 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>transmittable-thread-local</artifactId>
            <version>${transmittable.version}</version>
        </dependency>
        <!--aop-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!--SpringBoot与Redis整合依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>2023.0.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2023.0.0.0-RC1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

(3)写yml:

server.port=24618

spring.application.name=Interview2

# ==========config nacos address===================
#spring.cloud.nacos.discovery.server-addr= localhost:8848

# ==========druid-mysql8 driver===================
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/db2024?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=123456

# ========================mybatis==================
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.atguigu.interview2.entities
mybatis.configuration.map-underscore-to-camel-case=true

# ========================Thread Pool Config==================
# 线程池配置 System.out.println(Runtime.getRuntime().availableProcessors());
thread.pool.corePoolSize=16
thread.pool.maxPoolSize=32
thread.pool.queueCapacity=50
thread.pool.keepAliveSeconds=2

# ========================redis=====================
spring.data.redis.database=0
spring.data.redis.host=192.168.98.137
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

 

(4)业务类:

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;

/**
 * @auther leighteen
 * @create 2023-05-16 19:06
 */
@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;
    }
}

3、需求和设计

1、总体需求
(1)可配置

  • 规定时间内,可以随意灵活调整时间和次数
  • 支持设定1秒钟内满足几次访问,超过设定会启动限流功能,保护系统不过载

(2)可拔插

  • 按照促销活动,vip等级,方法使用频繁度等业务规则,要求Controller里面的业务方法有标识性控制机制

(3)可通用

  • 自己开发自定义限流共用模块给全团队赋能公用
  • 不要和业务逻辑代码写死,可以独立出来并配置

(4)高可用

  • 高并发下可以实时起效

2、解决思路
(1)使用自定义注解实现业务解耦,可配置(规定时间内可以随意灵活调整时间和次数),可拔插
(2)高并发实时配置下的LuaScript处理,支持高并发(redis)且满足事务一致性要求(Lua脚本)
(3)自定义AOP切面类,非业务逻辑功能,直接抽取出来,不混合业务

4、具体实现(Redis + Lua脚本 + AOP + 反射 + 自定义注解)

1、自定义注解:

import java.lang.annotation.*;

/**
 * @auther leighteen
 * @create 2023-05-23 15:44
 */

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

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

    /**
     * 过期时间也可以理解为单位时间或滑动窗口时间,单位秒,默认60
     */
    long expire() default 60;

    /**
     * 得不到令牌的提示语
     */
    String msg() default "default message:系统繁忙or你点击太快,请稍后再试,谢谢";
}

2、AOP自定义日志切面类:

import top.leighteen.interview.annotations.RedisLimitAnnotation;
import top.leighteen.interview.exception.RedisLimitException;
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.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 org.springframework.core.io.ClassPathResource;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

/**
 * @auther leighteen
 * @create 2023-05-23 15:45
 */

@Slf4j
@Aspect
@Component
public class RedisLimitAop
{
    Object result = null;

    @Resource
    private StringRedisTemplate stringRedisTemplate;


    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(com.atguigu.interview2.annotations.RedisLimitAnnotation)")
    public Object around(ProceedingJoinPoint joinPoint)
    {
        System.out.println("---------环绕通知1111111");

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        //拿到RedisLimitAnnotation注解,如果存在则说明需要限流,容器捞鱼思想
        RedisLimitAnnotation redisLimitAnnotation = method.getAnnotation(RedisLimitAnnotation.class);

        if (redisLimitAnnotation != null)
        {
            //获取redis的key
            String key = redisLimitAnnotation.key();
            String className = method.getDeclaringClass().getName();
            String methodName = method.getName();

            String limitKey = key +"\t"+ className+"\t" + methodName;
            log.info(limitKey);

            if (null == key)
            {
                throw new RedisLimitException("it's danger,limitKey cannot be null");
            }

            long limit = redisLimitAnnotation.permitsPerSecond();
            long expire = redisLimitAnnotation.expire();
            List<String> keys = new ArrayList<>();
            keys.add(key);

            Long count = stringRedisTemplate.execute(
                redisLuaScript,
                keys,
                String.valueOf(limit),
                String.valueOf(expire));

            System.out.println("Access try count is "+count+" \t key= "+key);
            if (count != null && count == 0)
            {
                System.out.println("启动限流功能key: "+key);
                //throw new RedisLimitException(redisLimitAnnotation.msg());
                return redisLimitAnnotation.msg();
            }
        }


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

        System.out.println("---------环绕通知2222222");
        System.out.println();
        System.out.println();

        return result;
    }


    /**
     * 构建redis lua脚本,防御性编程,程序员自我保护,闲聊
     * 能用LuaScript,就不要用java拼装
     * @return
     */
    /*private String buildLuaScript() {
        StringBuilder luaString = new StringBuilder();
        luaString.append("local key = KEYS[1]");
        //获取ARGV内参数Limit
        luaString.append("\nlocal limit = tonumber(ARGV[1])");
        //获取key的次数
        luaString.append("\nlocal curentLimit = tonumber(redis.call('get', key) or '0')");
        luaString.append("\nif curentLimit + 1 > limit then");
        luaString.append("\nreturn 0");
        luaString.append("\nelse");
        //自增长 1
        luaString.append("\n redis.call('INCRBY', key, 1)");
        //设置过期时间
        luaString.append("\nredis.call('EXPIRE', key, ARGV[2])");
        luaString.append("\nend");
        return luaString.toString();
    }*/
}

3、LuaScript:

--获取KEY,针对那个接口进行限流
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 = "当前排队人数较多,请稍后再试!")

4、RedisLimitController:

import cn.hutool.core.util.IdUtil;
import top.leighteen.interview.annotations.RedisLimitAnnotation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @auther leighteen
 * @create 2023-05-23 15:44
 */

@Slf4j
@RestController
public class RedisLimitController
{
    /**
     * Redis+Lua脚本+AOP+反射+自定义注解,打造我司内部基础架构限流组件
     * http://localhost:24618/redis/limit/test
     *
     * 在redis中,假定一秒钟只能有3次访问,超过3次报错
     * key = redisLimit
     * Value = permitsPerSecond设置的具体值
     * 过期时间 = expire设置的具体值,
     * permitsPerSecond = 3, expire = 10
     * 表示本次10秒内最多支持3次访问,到了3次后开启限流,过完本次10秒钟后才解封放开,可以重新访问
     */
    @GetMapping("/redis/limit/test")
    @RedisLimitAnnotation(key = "redisLimit", permitsPerSecond = 3, expire = 10, msg = "当前排队人数较多,请稍后再试!")
    public String redisLimit()
    {
        return "正常业务返回,订单流水:"+ IdUtil.fastUUID();
    }
}
  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Leighteen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值