1.开发自定义限流注解给全团队赋能共用,一个注解搞定
2.可配置【时间窗口内可以随意灵活调整时间和次数】 + 可拔插
3.支持高并发【redis下干的】且满足事务一致性要求,lua脚本
注意!限流是在Controller层做的,不要干到Service层【做业务逻辑的】
1.自定义注解RedisLimitAnnotation实现业务解耦
2.高并发实时配置下的LuaScript处理
3.自定义AOP切面类
RedisLimitController
@Slf4j
@RestController
public class RedisLimitController{
@GetMapping("/redis/limit/test")
public String redisLimit(){
return "业务正常返回,订单流水:" + IdUtil.fastUUID();
}
}
限流后
时间窗口在这等价于redis 的 key的过期时间
@Slf4j
@RestController
// 1秒内只允许2个人点击
@RedisLimitAnnotation(key = "redis-limit:test" , permitsPerSecond = 2,expire = 1, msg = "当前排队人数过多,请稍后再试!")
public class RedisLimitController{
@GetMapping("/redis/limit/test")
public String redisLimit(){
return "业务正常返回,订单流水:" + IdUtil.fastUUID();
}
}
RedisConfig
redis序列化的工具配置类【一定要开启】
public class RedisConfig{
// 执行 keys *
// 野生:"\xac\xed\x00\aord:102" 序列化后:"ord:102"
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactor){
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactor);
// 设置key序列化方式String
redisTemplate.setKeySerializer(new StringRedisSerializer());
// 设置value的序列化方式json,使用GenericJackson2JsonRedisSerializer替换默认序列化
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
RedisLimitAnnotation
@Retentiom(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Document
public @interface RedisLimitAnnotation{
// 资源key唯一,不同接口不同流量控制 模拟Sentinel资源key resource
String key() default "";
// 最多访问限制次数
long permitsPerSecond() default 2;
// 过期时间 滑动窗空时间 单位秒 默认60秒
long expire() default 60;
// 得不到令牌提示语
String msg() default "系统繁忙 点击太快请稍后重试";
}
Aop内核心的业务逻辑
// 如果类上由限流的注解
if(redisLimitAnnotation != null){
String key = redisLimitAnnotation.key();
String className = method.getDeclaringClass().getName();
String methodName = method.getName();
String limitKey = key+"_"+className+"_"+methodName;
// 每秒通过请求数量
long limit = redisLimitAnnotation.permitsPerSecond();
// 几秒钟通过limit个请求
long expire = redisLimitAnnotation.expire();
List<String> keys = new ArrayList<>();
keys.add(key);
// Lua脚本
Long count = stringRedisTemplate.execute(redisLuaScript,keys,String.valueof(limit),String.valueof(expire));
if(count != null && count==0){
log.error("获取key失败");
rerturn redisLimitAnnotation.msg();
}
// 放行
joinPoint.proceed();
}
调用Lua脚本的关键
private DefaultRedisScript<Long> redisLuaScript;
@PostContrust
public void init(){
redisLuaScript = new DefaultRedisScript<>();
// Lua脚本返回值
redisLuaScript.setResultType(Long.class);
redisLuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimiter.lua")))
}
tonumber 是 Lua 语言的一个内置函数,用于将参数转换为数字。如果转换成功,tonumber 返回数字,否则返回 nil。
redis.call('get',key) or "0"
- 这是 Lua 的逻辑或运算符。在 Lua 中,如果左侧的表达式(在这里是 redis.call('get', key))的结果为 nil 或 false,则 or 运算符会返回其右侧的值(在这里是 "0"),如果该键存在,返回其值。
- 因此,如果键 key 不存在于 Redis 中,redis.call('get', key) 将返回 nil,此时整个表达式的结果将是 "0"。
--获取key,针对哪个接口进行限流
local key = KEYS[1]
--获取注解上标注的限流次数
local limit = tonumber(ARGV[1])
-- redis call 一条失败全部失败 redis pcall 一条失败 不影响其他继续执行
local currentLimit = tonumber(redis.call('get',key) or "0")
if currentLimit+1 > limit
then return 0
else
-- 自增长1
redis.call("INCRYBY",key,1)
--设置过期时间
redis.call("EXPIRE",key,ARGV[2])
return currentLimit+1
end