目前市面上主流的限流中间件:
1、SpringCloud alibaba限流组件 Sentinel
Sentinel 是阿里巴巴开源的流量控制、熔断降级、系统保护组件,本文深入介绍了 Sentinel 的核心概念、规则配置、系统保护、熔断降级、流控策略以及与 Hystrix 的区别,帮助读者全面理解 Sentinel 的工作原理和使用方法。
官网:https://github.com/alibaba/Sentinel/wiki2012年,Sentinel诞生于阿里巴巴,其主要目标是流量控制。2013-2017年,Sentinel迅速发展,并成为阿里巴巴所有微服务的基本组成部分。 它已在6000多个应用程序中使用,涵盖了几乎所有核心电子商务场景。2018年,Sentinel演变为一个开源项目。2020年,Sentinel Golang发布。
为什么有些企业不让引入外部框架呢?
- 我们内部封装或用的是Dubbo开发微服务,无法使用Cloud相关限流组件,你谈谈自研框架有过落地经验吗?
- 我们银行外包团队,安全性要求,不可以引入太多的组件,有自研思路吗?
那么具体要如何实现呢?
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();
}
}