Redis+Lua脚本+AOP+自定义注解自定义基础限流组件

Redis+Lua脚本+AOP+自定义注解自定义基础限流组件

背景

项目中很多接口是有防重防刷要求的,比如下单啊、支付啊这种,如果用户频繁的点,后端没做好保护的话,很容易出问题

市面上有主流的一体化解决方案,比如 resilience4j啊,比如 Sentinel,但是有些公司有要求不允许引入这些第三方库,又该如何应对呢

限流组件设计目标

Redis+Lua脚本+AOP+反射+自定义注解,打造基础架构限流组件,对于限流组件的设计,总体要求是:

可配置:也就是规定时间内,可以随意调整时间和次数

可插拔:就像上面那个注解,接口加了注解自带限流功能,不加注解没有限流

可通用:不能业务代码耦合,可独立模块,供其它团队使用

高可用:高并发下实时起效

环境搭建

这里使用 SpringBoot3+Redis 快速搭建项目骨架

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.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后才解封放开,可以重新访问)

java 复制代码
@RestController
@RequestMapping("/limit")
public class RedisLimitController {
    @GetMapping("/redis/test")
    @RedisLimitAnnotation(key = "redisLimit", permitsPerSecond = 3, expire = 10, msg = "当前访问人数较多,请稍后重试")
    public String redisLimit() {
        return "正常业务返回," + UUID.randomUUID().toString();
    }
}
yaml 复制代码
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
java 复制代码
@Configuration
public class RedisConfig {
    @Bean
    public 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 注解及切面的设计开发了

注解及切面

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RedisLimitAnnotation {
    // 资源 key,唯一
    String key() default "";

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

    // 过期时间,单位秒默认30s
    long expire() default 30;

    // 默认提示
    String msg() default "你点击太快,请稍后再搞.";
}
java 复制代码
@Slf4j
@Aspect
@Component
public class RdisLimitAspect {
    Object result = null;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private DefaultRedisScript<Long> redisScript;

    @PostConstruct
    public 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的 key
            String 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 脚本

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
else
  redis.call('INCRBY', key, 1)
  redis.call('EXPIRE', key, ARGV[2])
  return currentLimit + 1
end

最后效果如下

相关推荐
m0_639906941 小时前
Redis入门|五种数据结构、使用场景、持久化RDB&AOF
数据结构·数据库·redis
IT规划师7 小时前
Redis 数据类型全解 - String 不只是 get set:90% 开发者忽略的 8 个高阶用法与内存陷阱(一)
redis·.net core
Flynn Liang8 小时前
内存/Redis/PGVector/Milvus/ES 怎么选?Spring AI VectorStore 抽象与向量库选型对照表
redis·elasticsearch·milvus·rag·springai·pgvector·向量库
hweiyu0010 小时前
Redis命令:SUNIONSTORE
redis·缓存
麻瓜code10 小时前
秒杀优化三连:Lua + 阻塞队列 + Redis Stream,响应从 3.4 秒到毫秒级
redis·junit·lua
努力努力再努力wz11 小时前
【Lua 入门系列】Lua 函数深入理解:函数对象、多返回值、可变参数、闭包、递归与尾调用优化
开发语言·lua
没有气的汽水14 小时前
一次生产 Redis 内存打满的排查、清理和复盘
redis
hey you~14 小时前
云呼叫中心坐席自动分配实战:ACD 策略、Redis+Lua 实现与排查清单
数据库·redis·lua·云呼叫中心·acd·坐席分配·坐席状态机
我不会起名字32216 小时前
Redis pipeline 用了反而更慢:3 类误用与正确的批量姿势
redis·pipeline·lua·rtt·批量