基于Redis+Lua实现分布式限流

一、新建一个Mavne项目,取名为rate_limiter,并引入Lombok和guava的依赖。


    org.projectlombok
    lombok


    com.google.guava
    guava
    29.0-jre

二、在rate_limiter项目下新建一个名为ratelimiter_annotation的子模块,在该模块的pom文件中添加redis的依赖。


    org.springframework.boot
    spring-boot-starter-data-redis

三、在ratelimiter_annotation模块的src/main/java目录下创建service包,在service包下创建一个名为AccessLimiter的类。

@Service
@Slf4j
public class AccessLimiter {
    @Autowired
    private StringRedisTemplate redisTemplate;
    /**
     * DefaultRedisScript类用来加载脚本的,并设置相应的数据类型来接收lua脚本返回的数据,
     * 这个泛型类在使用时设置泛型是什么类型,脚本返回的结果就是用什么类型接收。
     * 该类只接收4种类型的返回类型(Long, Boolean, List, or deserialized value type)
    */
    @Autowired
    private DefaultRedisScript rateLimiterLua;

    public void limitAccess(String key,Integer limit){
        //执行lua脚本
        boolean acquire=redisTemplate.execute(
                rateLimiterLua,
                Lists.newArrayList(key),
                limit.toString());
        if (!acquire){
            log.error("your access is blocked,key={}",key);
            throw new RuntimeException("your access is blocked");
        }
    }
}

四、新建config包并创建名为RedisConfiguration的配置类

@Configuration
public class RedisConfiguration {

    @Bean
    public RedisTemplate redisTemplate(
            RedisConnectionFactory factory
    ){
        return new StringRedisTemplate(factory);
    }

    @Bean
    public DefaultRedisScript loadRedisScript(){
        DefaultRedisScript redisScript=new DefaultRedisScript();
        //设置lua脚本
        redisScript.setLocation(new ClassPathResource("ratelimiter.lua"));
        //设置返回类型
        redisScript.setResultType(java.lang.Boolean.class);
        return redisScript;
    }
}

五、在resources目录下新建lua脚本文件ratelimiter.lua。

--
-- Created by IntelliJ IDEA.
-- User: wanglei
--
-- 在lua脚本中,有两个全局的变量,是用来接收redis应用端传递的键值和其它参数的,
-- 分别为KEYS、ARGV。

-- 在应用端传递给KEYS时是一个数组列表,在lua脚本中通过索引方式获取数组内的值。

-- 在应用端,传递给ARGV的参数比较灵活,可以是多个独立的参数,但对应到Lua脚本中是,
-- 统一用ARGV这个数组接收,获取方式也是通过数组下标获取。


-- 通过KEYS获取方法签名特征
local methodKey = KEYS[1]
redis.log(redis.LOG_DEBUG, 'key is', methodKey)

-- 通过ARGV传入限流大小
local limit = tonumber(ARGV[1])

-- 获取当前流量大小
local count = tonumber(redis.call('get', methodKey) or "0")

-- 是否超出限流阈值
if count + 1 > limit then
    -- 拒绝服务访问
    return false
else
    -- 没有超过阈值
    -- 设置当前访问的数量+1
    redis.call("INCRBY", methodKey, 1)
    -- 设置过期时间
    redis.call("EXPIRE", methodKey, 1)
    -- 放行
    return true
end

六、在rate_limiter项目中再新建一个ratelimiter_test的子模块用于测试我们前面的脚本。在ratelimiter_test中引入以下依赖。


    org.springframework.boot
    spring-boot-starter-web



    ${project.groupId}
    ratelimiter_annotation
    ${project.version}

七、在ratelimiter_test的src/main/java下新建controller包,并在controller包下创建一个TestController的类。

@RestController
@Slf4j
public class TestController {
    @Autowired
    private AccessLimiter accessLimiter;

    @GetMapping("test")
    public String test(){
        accessLimiter.limitAccess("ratelimiter-test",1);
        return "success";
    }
}

八、在application.properties中添加redis的配置

spring.redis.database=0
spring.redis.host=localhsot
spring.redis.port=6379
spring.redis.password=root

九、创建一个启动类并启动项目,在postman中测试一下查看限流的结果。

@SpringBootApplication
public class RatelimiterTestApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(RatelimiterTestApplication.class, args);
    }

}

十、通过以上的几个步骤,已经实现了基于Redis+Lua的限流,但是代码还不够完美,现在我们将项目改造一下,通过自定义的注解在项目的任何位置都可以实现限流。

先在ratelimiter_annotation模块中引入aop的依赖。


    org.springframework.boot
    spring-boot-starter-aop

然后在ratelimiter_annotation模块中新建一个annotation的包,并在annotation包下创建一个名为AccessLimiter的注解。

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessLimiter {
    int limit();
    String methodKey() default "";
}

再创建一个aspect的包,并创建一个名为AccessLimiterAspect的类

@Slf4j
@Aspect
@Component
public class AccessLimiterAspect {

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private DefaultRedisScript rateLimiterLua;

    @Pointcut("@annotation(com.wl.annotation.AccessLimiter)")
    public void cut(){
        log.info("cut");
    }

    @Before("cut()")
    public void before(JoinPoint joinPoint){
        //1、获得方法签名,作为method key
        MethodSignature methodSignature= (MethodSignature) joinPoint.getSignature();
        Method method=methodSignature.getMethod();
        AccessLimiter annotation=method.getAnnotation(AccessLimiter.class);
        if (annotation==null){
            return;
        }
        String key=annotation.methodKey();
        Integer limit=annotation.limit();

        //如果没有设置methodKey,从调用方法签名自动生成一个
        if (StringUtils.isEmpty(key)){
            Class[] type=method.getParameterTypes();
            key=method.getName();

            if (type!=null){
                String paramTypes= Arrays.stream(type)
                        .map(Class::getName)
                        .collect(Collectors.joining(","));
                log.info("param types: "+paramTypes);
                key+="#"+paramTypes;
            }
        }

        //2、调用redis
        boolean acquire=redisTemplate.execute(
                rateLimiterLua,
                Lists.newArrayList(key),
                limit.toString());
        if (!acquire){
            log.error("your access is blocked,key={}",key);
            throw new RuntimeException("your access is blocked");
        }
    }
}

现在我们九可以使用我们自定义的注解了,我们在TestController新增一个方法

@GetMapping("test-annotation")
@com.wl.annotation.AccessLimiter(limit = 1)
public String testAnnotation(){
    return "success";
}

通过启动类再次启动我们的项目并测试一下testAnnotation接口。

你可能感兴趣的:(限流,redis,lua)