Redis+Lua脚本+AOP+反射+自定义注解,打造内部基础架构限流组件

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值