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

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解释器中执行将很难去排查问题。

糖果

糖果
LUA教程

如果不小心安装错 SQL Server 为 Evaluation 的版本,要小心当超过 180 天之后,系统就会无法正常使用了 这几天遇到一个蛮特别的案例,原本收到的问题是 “维护计划” 忽然无法使用,即便是里面没有任何的Task,都无法顺利地执行。但从对方所提供的错误消...… Continue reading

PLUM NIZ静电容键盘怎么样?

Published on September 25, 2020

程序员如何选择合适的机械键盘

Published on September 18, 2020