SpringBoot通过Redis调用Lua脚本实践笔记

概述

redis+lua可以做什么?

  • 限流
  • 分布式锁
  • 信号量
  • 事务

相比Redis事务来说,Lua脚本有以下优点

  • 减少网络开销: 不使用 Lua 的代码需要向 Redis 发送多次请求,而脚本只需一次即可,减少网络传输;
  • 原子操作:Redis 将整个脚本作为一个原子执行,无需担心并发,也就无需事务;
  • 复用:脚本会永久保存 Redis 中,其他客户端可继续使用。

代码实现

依赖

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.8.0</version>
</dependency>

redis配置

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
redis:
database: 0
host: localhost
port: 6379
password:
timeout: 6000ms
lettuce:
pool:
max-active: 1000
max-wait: -1ms
max-idle: 10
min-idle: 5

配置RedisTemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @author 当我遇上你
* @公众号 当我遇上你
* @since 2020-05-21
*/
@Configuration
public class RedisConfig {

@Bean
public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory) {
RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}

Lua脚本

resources/scripts/ 路径下创建 test.lua 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
--获取KEY
local key1 = KEYS[1]

--获取参数
local avg1 = ARGV[1]
local avg2 = ARGV[2]

--打印日志到reids
--注意,这里的打印日志级别,需要和redis.conf配置文件中的日志设置级别一致才行
redis.log(redis.LOG_WARNING,"key1=" ..key1)
redis.log(redis.LOG_WARNING,"avg=" ..avg1, avg2)

--将参数String转为数字类型
--限流时间窗
local expire = tonumber(ARGV[1])
redis.log(redis.LOG_WARNING,"时间窗=" ..expire)

--限流阈值
local limit = tonumber(ARGV[2])
redis.log(redis.LOG_WARNING,"限流频次=" ..limit)

--当前并发数
local current = tonumber(redis.call('get', key1) or "0")
redis.log(redis.LOG_WARNING,"当前并发=" ..current)

if current + 1 > limit then
return 0
else
redis.call("INCRBY", key1, "1")
redis.call("expire", key1, expire)
return 1
end

加载lua脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @author 当我遇上你
* @公众号 当我遇上你
* @since 2020-05-21
*/
@Configuration
public class LuaConfiguration {

@Bean
public DefaultRedisScript<Long> redisScript() {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setResultType(Long.class);
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("scripts/test.lua")));
return redisScript;
}
}

测试限流效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* @author 当我遇上你
* @公众号 当我遇上你
* @since 2020-05-21
*/
@SpringBootTest
class RateLimitingServiceTest {


@Autowired
private RedisTemplate redisTemplate;

@Autowired
private DefaultRedisScript<Long> redisScript;

@Test
void luaTest() {
List<String> keys = Arrays.asList("aaa");
// 10秒内小于或等于3次时返回1,否则返回0
for (int i = 0; i < 4; i++) {
Object execute = redisTemplate.execute(redisScript, keys, 10, 3);
System.out.println(execute);
}
}
}

结果输出

1
2
3
4
1
1
1
0

可见限流生效

最后

本文到此结束,感谢阅读。原创不易, 如果您觉得不错,请关注公众号【当我遇上你】支持一下。