SpringBoot集成Redis+Lua限流 | Eddie'Blog
SpringBoot集成Redis+Lua限流

SpringBoot集成Redis+Lua限流

eddie 313 2020-12-25

目录

Lua 初步认识

Lua 特点

  • 短小精干
  • 嵌套式开发、插件开发
  • 完美继承Redis [Redis内置Lua解释器执行过程原子性脚本预编译]

IDEA插件安装

  • Lua
  • EmmyLua

Lua基本用法

  • Hello Lua
  • 一个简易脚本(Lua入门级语法)

安装Lua

  1. 下载 Windows x86, 如果是MAC推荐使用brew工具直接install lua
  2. 安装IDEA插件, 搜索lua, 然后就选择同名插件lua. idea restart.
  3. 配置Lua SDK的位置: IDEA -> File -> Project Structure 选择添加Lua 或者 EmmyLua, 路径指向Lua SDK的bin文件夹.

脚本显示

Hello Lua 演示


点击查看
---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by Administrator.
--- DateTime: 2020/12/24 9:06
---

print('Hello Lua')

限流脚本演示


点击查看
--- 模拟限流
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by Administrator.
--- DateTime: 2020/12/24 9:08
---

--- 用于限流的Key
local key = 'My Key'

--- 限流的最大阔值 =2
local limit = 2

--- 当前流量大小
local currentLimit = 0

--- 是否大于限流标准
if currentLimit + 1 > limit then
    print('reject')
    return false
else
    print('accept')
    return true
end

Redis预加载Lua


点击查看
  • 在Redis中执行Lua脚本
  • Lua脚本预导入Redis
[root@8_100 ~]# docker exec -it redis sh
/data # cd /usr/local/bin 
/usr/local/bin # ./redis-cli

# Hello 打印
127.0.0.1:6379> eval "return 'hello redis+lua'" 0
"hello redis+lua"

# 参数传递
127.0.0.1:6379> eval "return {KEYS[1],ARGV[1]}" 2 K1 K2 V1 V2
1) "K1"
2) "V1"
127.0.0.1:6379> eval "return {KEYS[2],ARGV[2]}" 2 K1 K2 V1 V2
1) "K2"
2) "V2"

# 预加载
127.0.0.1:6379> script load "return 'hello redis+lua'"
"53b2700be01e76aa1b060e09c828dae642520f2e"
127.0.0.1:6379> evalsha "53b2700be01e76aa1b060e09c828dae642520f2e" 0
"hello redis+lua"

# 预加载
127.0.0.1:6379> script load "return 'hello lua '..KEYS[1]"
"1689a78376800076aa8b094894d8e7f7ab78f710"

# 根据ID查看脚本是否存在
127.0.0.1:6379> script exists "1689a78376800076aa8b094894d8e7f7ab78f710"
1) (integer) 1

# 运行预加载给出的ID获取值
127.0.0.1:6379> evalsha "1689a78376800076aa8b094894d8e7f7ab78f710" 1 key1 val1
"hello lua key1"

# 清空所有脚本缓存
127.0.0.1:6379> script flush
OK

Redis + Lua 限流组件封装 (一)

  • 编写Lua限流脚本
  • spring-data-redis组件继承Lua和Redis
    • DefaultRedisScript加载Lua脚本
    • RedisTemplate配置(调用Redis)
  • 在Controller中添加测试方法验证限流效果

基础版

引用接口方式


点击查看

新建 ratelimiter-annotation 项目, 作为限流功能引用

Maven

<?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 http://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>2.4.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>ratelimiter-annotation</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>18.0</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

</project>

src/main/resources/ratelimiter.lua

---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by eddie.lee
--- DateTime: 2020/12/24 15:49
---


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

-- 调用脚本传入的限流大小
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

src/main/java/com/example/springcloud/RedisConfiguration.java

@Configuration
public class RedisConfiguration {

    // 如果本地也配置了StringRedisTemplate,可能会产生冲突
    // 可以指定@Primary,或者指定加载特定的@Qualifier
    @Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
        return new StringRedisTemplate(factory);
    }

    @Bean
    public DefaultRedisScript loadRedisScript() {
        DefaultRedisScript redisScript = new DefaultRedisScript();
        redisScript.setLocation(new ClassPathResource("ratelimiter.lua"));
        redisScript.setResultType(java.lang.Boolean.class);
        return redisScript;
    }

}

src/main/java/com/example/springcloud/AccessLimiter.java

@Slf4j
@Service
public class AccessLimiter {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RedisScript<Boolean> rateLimitLua;

    public void limitAccess(String key, Integer limit) {
        // step 1 : request Lua script
        boolean acquired = stringRedisTemplate.execute(
                rateLimitLua, // Lua script的真身
                Lists.newArrayList(key), // Lua脚本中的Key列表
                limit.toString() // Lua脚本Value列表
        );

        if (!acquired) {
            log.info("您的访问被阻塞了! key=[{}]", key);
            throw new RuntimeException("您的访问被阻塞了!");
        }
    }
}

新建 ratelimiter-test 项目, 作为限流调用方

  1. ratelimiter-annotation 项目打包 mvn clean package
  2. IDEA File --> Project Structure --> Libranies + --> Java (ratelimiter-annotation-1.0-SNAPSHOT.jar)

Maven依赖

<?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 http://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>2.4.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>ratelimiter-test</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- 必需这样子引用, 不然会报 RedisConnectionFactory 的错误异常 -->
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>ratelimiter-annotation</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

</project>

src/main/resources/application.yml

spring:
  application:
    name: ratelimiter-test
  redis:
    database: 0
    host: 192.168.8.100
    port: 6379
server:
  port: 10086
logging:
  file:
    path: log/${spring.application.name}.log

src/main/java/com/example/springcloud/Controller.java

@Slf4j
@RestController
public class Controller {

    @Autowired
    private AccessLimiter accessLimiter;

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

src/main/java/com/example/springcloud/Controller.java

@SpringBootApplication
public class RatelimiterApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(RatelimiterApplication.class)
                .web(WebApplicationType.SERVLET)
                .run(args);
    }

}

访问测试

1. GET localhost:10086/test
2. 疯狂点击

当超过Lua脚本的阈值就会出现错误:
{
    "timestamp": "2020-12-25T05:44:30.665+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "message": "",
    "path": "/test"
}

后台也会打印:
2020-12-25 13:44:30.662  INFO 19044 --- [io-10086-exec-7] com.example.springcloud.AccessLimiter    : 您的访问被阻塞了! key=[ratelimiter-test]
java.lang.RuntimeException: 您的访问被阻塞了!

升级高大尚版

自定义注解方式引用

  • 基于Aspectj创建自定义注解AccessLimit
  • 配置限流规则的切面
  • 为目标方法添加@AccessLimit注解, 验证效果

点击查看

继续使用上面的代码延伸

ratelimiter-annotation

com.example.springcloud.annotation.AccessLimiter

/**
 * @author eddie.lee
 * @ProjectName ratelimiter-annotation
 * @Package com.example.springcloud.annotation
 * @ClassName AccessLimiter
 * @description
 * @date created in 2020-12-25 11:26
 * @modified by
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessLimiter {

    int limit();

    String methodKey() default "";

}

com.example.springcloud.annotation.AccessLimiterAspect

/**
 * @author eddie.lee
 * @ProjectName ratelimiter-annotation
 * @Package com.example.springcloud
 * @ClassName AccessLimiterAspect
 * @description
 * @date created in 2020-12-25 11:25
 * @modified by
 */
@Slf4j
@Aspect
@Component
public class AccessLimiterAspect {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RedisScript<Boolean> rateLimitLua;

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

    @Before("cut()")
    public void before(JoinPoint joinPoint) {
        // 1. 获得方法签名,作为method Key
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        AccessLimiter annotation = method.getAnnotation(AccessLimiter.class);
        if (annotation == null) {
            return;
        }

        String key = annotation.methodKey();
        Integer limit = annotation.limit();

        // 如果没设置methodkey, 从调用方法签名生成自动一个key
        if (StringUtils.isEmpty(key)) {
            Class[] type = method.getParameterTypes();
            key = method.getClass() + 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 acquired = stringRedisTemplate.execute(
                rateLimitLua, // Lua script的真身
                Lists.newArrayList(key), // Lua脚本中的Key列表
                limit.toString() // Lua脚本Value列表
        );

        if (!acquired) {
            log.error("your access is blocked, key={}", key);
            throw new RuntimeException("Your access is blocked");
        }
    }

}

重新打包

  1. ratelimiter-annotation 项目打包 mvn clean package
  2. IDEA File --> Project Structure --> Libranies + --> Java (ratelimiter-annotation-1.0-SNAPSHOT.jar)

ratelimiter-test

/**
 * @author eddie.lee
 * @ProjectName ratelimiter-test
 * @Package com.example.springcloud
 * @ClassName Controller
 * @description
 * @date created in 2020-12-25 9:02
 * @modified by
 */
@Slf4j
@RestController
public class Controller {

    @Autowired
    private AccessLimiter accessLimiter;

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

    // 提醒! 注意配置扫包 (com.example.springcloud路径不同)
    @GetMapping("test-annotation")
    @com.example.springcloud.annotation.AccessLimiter(limit = 1, methodKey = "ratelimiter-test")
    public String testAnnotation() {
        return "success";
    }
}

测试效果和基础版一样