某业务中需要对后端接口进行限流,我们可以直接引入阿里巴巴的Sentinel快速实现,但是某企业中出于安全考虑,需要部门自己研发一套,可以采用Redis+Lua脚本+AOP+反射+自定义注解 来实现
思路来源于链接
项目结构:
启动类:不需要额外加东西,默认即可
pom.xml:
bash
<?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相关
bash
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:常规的序列化配置
bash
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**
bash
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、最大访问次数、过期窗口时间、限流出现提示语
bash
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脚本
bash
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:
bash
--获取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次,浏览器快速访问这个接口三次,浏览器出现错误提示,成功!