[TOC]
# 目录
## Lua 初步认识
### Lua 特点
- 短小精干
- 嵌套式开发、插件开发
- 完美继承Redis [Redis内置Lua解释器执行过程原子性脚本预编译]
### IDEA插件安装
- Lua
- EmmyLua
### Lua基本用法
- Hello Lua
- 一个简易脚本(Lua入门级语法)
### [安装Lua](windows下配置lua开发环境)
1. [下载 Windows x86](http://luadist.org/), 如果是MAC推荐使用brew工具直接install lua
1. 安装IDEA插件, 搜索lua, 然后就选择同名插件lua. idea restart.
1. 配置Lua SDK的位置: IDEA -> File -> Project Structure 选择添加Lua 或者 EmmyLua, 路径指向Lua SDK的bin文件夹.
### 脚本显示
#### Hello Lua 演示
<br>
<details>
<summary>点击查看</summary>
```lua
---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by Administrator.
--- DateTime: 2020/12/24 9:06
---
print('Hello Lua')
```
</details>
#### 限流脚本演示
<br>
<details>
<summary>点击查看</summary>
```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
```
</details>
#### Redis预加载Lua
<br>
<details>
<summary>点击查看</summary>
- 在Redis中执行Lua脚本
- Lua脚本预导入Redis
```shell
[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
```
</details>
## Redis + Lua 限流组件封装 (一)
- 编写Lua限流脚本
- spring-data-redis组件继承Lua和Redis
- DefaultRedisScript加载Lua脚本
- RedisTemplate配置(调用Redis)
- 在Controller中添加测试方法验证限流效果
### 基础版
#### 引用接口方式
<br>
<details>
<summary>点击查看</summary>
<br>
**新建 ratelimiter-annotation 项目, 作为限流功能引用**
Maven
```xml
<?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
```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
```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
```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
1. IDEA File --> Project Structure --> Libranies + --> Java (ratelimiter-annotation-1.0-SNAPSHOT.jar)
Maven依赖
```xml
<?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
```yaml
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
```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
```java
@SpringBootApplication
public class RatelimiterApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(RatelimiterApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
```
访问测试
```xml
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: 您的访问被阻塞了!
```
</details>
### 升级高大尚版
#### 自定义注解方式引用
- 基于Aspectj创建自定义注解AccessLimit
- 配置限流规则的切面
- 为目标方法添加@AccessLimit注解, 验证效果
<br>
<details>
<summary>点击查看</summary>
<br>
***继续使用上面的代码延伸***
ratelimiter-annotation
com.example.springcloud.annotation.AccessLimiter
```java
/**
* @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
```java
/**
* @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
1. IDEA File --> Project Structure --> Libranies + --> Java (ratelimiter-annotation-1.0-SNAPSHOT.jar)
**ratelimiter-test**
```java
/**
* @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";
}
}
```
测试效果和基础版一样
</details>
SpringBoot集成Redis+Lua限流