Redis+Lua脚本+AOP+自定义注解自定义基础限流组件
- 背景
- 限流组件设计目标
- 环境搭建
- 注解及切面
背景
项目中很多接口是有防重防刷要求的,比如下单啊、支付啊这种,如果用户频繁的点,后端没做好保护的话,很容易出问题
市面上有主流的一体化解决方案,比如 resilience4j啊,比如 Sentinel,但是有些公司有要求不允许引入这些第三方库,又该如何应对呢
限流组件设计目标
Redis+Lua脚本+AOP+反射+自定义注解,打造基础架构限流组件,对于限流组件的设计,总体要求是:
可配置:也就是规定时间内,可以随意调整时间和次数
可插拔:就像上面那个注解,接口加了注解自带限流功能,不加注解没有限流
可通用:不能业务代码耦合,可独立模块,供其它团队使用
高可用:高并发下实时起效
环境搭建
这里使用 SpringBoot3+Redis 快速搭建项目骨架
<?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.3.0</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.example.springbootV3</groupId><artifactId>springbootV3</artifactId><version>0.0.1-SNAPSHOT</version><name>springbootV3</name><properties><java.version>17</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-web</artifactId><version>3.1.3</version></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><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
采购 AOP 注解形式(如下注解表示10s内最多支持3次访问,到了3次后开启限流,过完本次10s后才解封放开,可以重新访问)
@RestController
@RequestMapping("/limit")
public class RedisLimitController {@GetMapping("/redis/test")@RedisLimitAnnotation(key = "redisLimit", permitsPerSecond = 3, expire = 10, msg = "当前访问人数较多,请稍后重试")public String redisLimit() {return "正常业务返回," + UUID.randomUUID().toString();}
}
spring.data.redis.database=0
spring.data.redis.host=192.168.133.128
spring.data.redis.port=6379
spring.data.redis.password=123456
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
@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(lettuceConnectionFactory);redisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());redisTemplate.setHashKeySerializer(new StringRedisSerializer());redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());redisTemplate.afterPropertiesSet();return redisTemplate;}
}
接下来主要就是 @RedisLimitAnnotation 注解及切面的设计开发了
注解及切面
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RedisLimitAnnotation {// 资源 key,唯一String key() default "";// 最多的访问次数限制long permitsPerSecond() default 3;// 过期时间,单位秒默认30slong expire() default 30;// 默认提示String msg() default "你点击太快,请稍后再搞.";
}
@Slf4j
@Aspect
@Component
public class RdisLimitAspect {Object result = null;@Resourceprivate StringRedisTemplate stringRedisTemplate;private DefaultRedisScript<Long> redisScript;@PostConstructpublic void init(){redisScript = new DefaultRedisScript<>();redisScript.setResultType(Long.class);redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimiter.lua")));}@Around("@annotation(com.example.demo.annotation.RedisLimitAnnotation)")public Object around(ProceedingJoinPoint pjg) {System.out.println("环绕通知开始..");MethodSignature signature = (MethodSignature) pjg.getSignature();Method method = signature.getMethod();RedisLimitAnnotation annotation = method.getAnnotation(RedisLimitAnnotation.class);if (annotation != null) {String key = annotation.key(); // 获取 redis的 keyString className = method.getDeclaringClass().getName();String methodName = method.getName();String limitKey = key + "\t" + className + "\t" + methodName;log.info(limitKey);if (key == null) {throw new RuntimeException("limitkey can not be null");}long permitsPerSecond = annotation.permitsPerSecond();long expire = annotation.expire();List<String> keys = new ArrayList<>();keys.add(key);Long count = stringRedisTemplate.execute(redisScript, keys, String.valueOf(permitsPerSecond), String.valueOf(expire));if (count != null && count == 0) {System.out.println("气东送限流功能key:" + key);return annotation.msg();}}try {result = pjg.proceed();} catch (Throwable throwable) {throw new RuntimeException(throwable);}System.out.println("环绕通知结束..");return result;}
}
最后是 Lua 脚本
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local currentLimit = tonumber(redis.call('get', key) or "0")
if currentLimit + 1 > limit
then return 0
elseredis.call('INCRBY', key, 1)redis.call('EXPIRE', key, ARGV[2])return currentLimit + 1
end
最后效果如下