Redis 限流解决方案:结合 Lua 脚本、AOP 和自定义注解的实现
目录
- 一、概述
- 二、技术栈
- 三、配置项
- 1、配置文件
- 2、Redis 配置类
- 四、流量控制功能设计
- 1、需求分析
- 2、设计思路
- 3、自定义注解
- 4、AOP 切面实现
- 5、Lua 脚本
- 6、示例 Controller
- 五、总结
一、概述
为了保护系统的稳定性,避免在高并发情况下系统崩溃,我们设计并实现了一个基于 Redis、Lua 脚本、AOP、反射和自定义注解的限流组件。该组件能灵活配置并支持高可用、高并发的流量控制。
二、技术栈
- Redis:作为流量控制的基础存储,用于计数和过期控制。
- Lua 脚本:通过 Lua 脚本来保证限流操作的原子性,避免因并发请求引起的竞争条件。
- AOP:通过切面编程将流量控制逻辑与业务代码解耦,做到高效且优雅的控制。
- 自定义注解:为不同的业务方法灵活添加限流控制,支持高度可配置性。
三、配置项
1、配置文件
首先是 Redis 配置文件(application.properties
):
# Redis 配置
spring.data.redis.database=0
spring.data.redis.host=192.168.121.140
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
2、Redis 配置类
RedisConfig
配置类主要完成 Redis 连接池及序列化方式的设置:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;@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;}
}
四、流量控制功能设计
1、需求分析
- 可配置:支持随时调整限流的时间和次数。
- 可插拔:支持按业务需求选择不同的限流策略。
- 可通用:不和代码业务逻辑写死,可单独配置
- 高可用:在高并发下仍能保证流量控制的稳定性。
2、设计思路
- 自定义注解:通过
@RedisLimitAnnotation
注解来标识需要限流的方法,并配置限流的时间窗口、请求次数及提示消息。 - Lua 脚本:通过 Redis Lua 脚本实现原子操作,确保限流操作的可靠性。
- AOP 切面:利用 AOP 切面来拦截带有限流注解的方法,进行限流操作。
3、自定义注解
RedisLimitAnnotation
注解用于标识需要限流的方法,提供了可配置的属性。
import java.lang.annotation.*;/*** 限流注解,用于标识需要限流的方法*/
@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 "系统繁忙,请稍后再试!";
}
4、AOP 切面实现
RedisLimitAop
类实现了基于 AOP 的限流逻辑,拦截带有 @RedisLimitAnnotation
注解的方法,执行 Redis 限流操作。
@Slf4j
@Aspect
@Component
public class RedisLimitAop {@Resourceprivate StringRedisTemplate stringRedisTemplate;private DefaultRedisScript<Long> redisLuaScript;@PostConstructpublic void init() {redisLuaScript = new DefaultRedisScript<>();redisLuaScript.setResultType(Long.class);redisLuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimiter.lua")));}@Around("@annotation(com.donglin.interview2.annotations.RedisLimitAnnotation)")public Object around(ProceedingJoinPoint joinPoint) {MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();RedisLimitAnnotation redisLimitAnnotation = method.getAnnotation(RedisLimitAnnotation.class);if (redisLimitAnnotation != null) {String key = redisLimitAnnotation.key();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));if (count != null && count == 0) {return redisLimitAnnotation.msg();}}try {return joinPoint.proceed();} catch (Throwable e) {throw new RuntimeException(e);}}
}
5、Lua 脚本
rateLimiter.lua
脚本用于实现 Redis 限流逻辑。它会检查当前访问次数,如果超过限流次数,则返回 0,否则自增访问计数并设置过期时间。
-- 获取当前 key
local key = KEYS[1]
-- 获取最大访问次数
local limit = tonumber(ARGV[1])
-- 获取当前访问次数
local currentLimit = tonumber(redis.call('get', key) or "0")-- 超过访问次数限制,返回 0
if currentLimit + 1 > limit thenreturn 0
else-- 自增访问次数redis.call('INCRBY', key, 1)-- 设置过期时间redis.call('EXPIRE', key, ARGV[2])return currentLimit + 1
end
6、示例 Controller
通过在 Controller 方法上添加 @RedisLimitAnnotation
注解,控制请求的频率。
@Slf4j
@RestController
public class RedisLimitController {@GetMapping("/redis/limit/test")@RedisLimitAnnotation(key = "redisLimit", permitsPerSecond = 3, expire = 10, msg = "当前排队人数较多,请稍后再试!")public String redisLimit() {return "正常业务返回,订单流水:" + IdUtil.fastUUID();}
}
五、总结
通过 Redis、Lua 脚本、AOP 以及自定义注解的组合,我们实现了一个高效且灵活的流量控制方案。此方案不仅支持高并发场景,还具备高度的可配置性和可插拔性,能够应对各种业务需求。