缓存数据库Redis结合Lua脚本解析

4.1k 词

redis作为一款优秀的缓存数据库,已成为许多的公司项目开发的必备底层数据库之一了,在我们使用redis时,除了平常对五种基础数据结构进行单一操作,有时会需要依赖redis来处理一段相对复杂的逻辑,而这段逻辑可能需要通过redis client发送多条redis命令来达到我们的目的,然而这种处理方式,不仅效率低,而且无法保证事务的原子性;redis从2.6.0版本开始提供了一种新的解决方案,内置lua解释器,通过 redis Eval 命令来执行lua脚本,达到执行自定义逻辑的redis命令的目的。

解析

Eval 命令的基本语法如下:

1
redis 127.0.0.1:6379> EVAL script numkeys key [key ...] arg [arg ...]

如果我们想在lua脚本中调用redis的命令该如何操作?可以在脚本中使用redis.call()或redis.pcall()直接调用,两者用法类似,只是在遇到错误时,返回错误的提示方式不同。例如:

1
eval "return redis.call('set',KEYS[1],'bar')" 1 foo

实例:

1
2
3
10.109:9>eval "return {KEYS[1],ARGV[1]}" 1 key1 ff
1) "key1"
2) "ff"

由于redis是单线程执行命令的,因此我们需要保证我们lua脚本足够精简,才不至于会阻塞redis线程,因此脚本内容尽量不用循环,避免阻塞redis线程,导致后续网络请求也被阻塞。

项目应用

实现功能

redis实现消息队列先进先出,并限制队列最大长度,超出长度则顶出队列最后一个元素

demo代码

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.Collections;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
* Created by lilm on 17-11-10.
*/
(SpringJUnit4ClassRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private StringRedisTemplate redisTemplate;
* push redis 队列脚本
* 1. 检查队列长度是否超出配置长度
* 2. 若超出, 弹出队列最后一个元素, 并将当前元素插入第一位
* 3. 没超出则将当前元素插入第一位
*/
private static DefaultRedisScript<Long> queueScript = null;
// 创建一个锁对象
private Lock lock = new ReentrantLock();
private Long l = 0L;
// 最大缓存消息数
private final static Long MAX_CACHED_NUM = 300L;
private final static String QUEUE_KEY = "demo-queue";
private void push() {
try {
lock.lock();
Long num = redisTemplate.execute(
getQueueScript(), Collections.singletonList(QUEUE_KEY),
MAX_CACHED_NUM.toString(), String.valueOf(l)
);
logger.info("push data:{} to queue return:{}", l, num);
} catch (Exception e) {
logger.error("redis error:", e);
} finally {
l++;
lock.unlock();
}
}
private static RedisScript<Long> getQueueScript() {
if (queueScript == null) {
queueScript = new DefaultRedisScript<Long>();
queueScript.setResultType(Long.class);
// ClassPathResource指定路径不需要前缀 classpath:
queueScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/queue_script.lua")));
}
return queueScript;
}
* 线程池持有三十个线程,每个线程持续写入100次,推入数据为0~2999
* 由于push方法是线程安全的,最终redis中demo-queue的结果应该是:
* 1. list中总共300条数据
* 2. 第一条为 2999 第300条为 2700,中间数据依次加1
*/
@Test
public void testQueue() {
ExecutorService service = Executors.newFixedThreadPool(50);
try {
for (int i = 0; i < 30; i ++) {
Thread t = new Thread(() -> {
int x = 0;
while (true) {
if (x == 100) {
break;
}
push();
x++;
}
});
try {
service.execute(t);
} finally {
logger.info("子线程{}已开启", i + 1);
}
}
logger.info("已启动所有的子线程");
service.shutdown();
while (true) {
if (service.isTerminated()) {
logger.info("所有的子线程都结束了!");
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

lua脚本内容:

1
2
3
4
5
6
7
8
9
10
11
-- push redis 队列脚本
-- 1. 检查队列长度是否超出配置长度
-- 2. 若超出, 弹出队列最后一个元素, 并将当前元素插入第一位
-- 3. 没超出则将当前元素插入第一位
local num = redis.call('LLEN', KEYS[1])
if num >= tonumber(ARGV[1]) then
redis.call('RPOP', KEYS[1])
num = num - 1
end
redis.call('LPUSH', KEYS[1], ARGV[2])
return num + 1

redis处理结果:

demo代码使用springboot+junit+spring-data-redis实现,附 源码地址

使用redis加lua脚本的好处是使程序逻辑更加简单,只需调用脚本执行即可,lua脚本执行可以减少网络延迟以及多余的传输流量,redis在执行lua脚本之后会将脚本sha1值缓存,下次调用时可以只携带脚本sha1值执行,进一步的减小网络开销。

注意

使用redis+lua脚本时一定要精简我们的脚本,太过复杂的逻辑将会降低redis执行效率,阻塞线程,甚至影响到系统性能;同时复杂的脚本一旦出现bug,因为是在lua解释器中执行将很难去排查问题。