当前位置:   article > 正文

Redis使用Lua脚本,Redis中多命令保持原子性神器 - Lua_lua redis

lua redis

一、认识Lua

Lua是一种轻量级脚本语言,它是用C语言编写的,跟数据库的存储过程有点类似。

在Redis从2.6版本之后,开始引入Lua脚本,也就是说,Redis可以用Lua来执行Redis命令。

官方文档:
https://redis.io/commands/eval/

使用Lua脚本来执行Redis命令的好处:

  • 一次发送多条命令,减少网络开销。
  • Redis会将整个脚本作为一个整体来执行,不会被其他请求打断,保持原子性
  • 对于复杂的组合命令,我们可以放在文件中,可以实现命令复用。

Lua语法请自行学习,推荐菜鸟教程简单入门:https://www.runoob.com/lua/lua-tutorial.html

二、Redis中使用Lua脚本

1、基本使用

使用eval命令,可以调用Lua,语法格式:

eval lua-script key-num [key1 key2 key3 ...] [value1 value2 value3 ...]
  • 1
  • eval 表示执行Lua语言的命令。
  • lua-script 表示Lua脚本语言内容。
  • key-num 表示参数中有多少个key,需要注意的是Redis中key是从1开始的,如果没有key的参数,那么写0。
  • [key1 key2 key3 …] 是key作为参数传递给Lua语言,也可以不填,但是需要和key-num的个数对应起来。
  • [value1 value2 value3 …] 这些参数传递给Lua语言,它们是可填可不填的。

实例:返回一个字符串,0个参数:

127.0.0.1:6379> eval "return 'Hello World'" 0
"Hello World"
  • 1
  • 2

Redis中的Lua脚本真正的用途是用来执行Redis命令。也就是说,Redis中运行Lua脚本,用Lua脚本来执行Redis命令。

2、在Lua脚本中调用Redis命令

语法格式:

redis.call(command, key [param1, param2 ...])
  • 1
  • command 是命令,包括set、get、del等
  • key是被操作的键
  • param1, param2 代表给key的参数
# 使用示例
127.0.0.1:6379> eval "return redis.call('set', 'zhangsan', '123')" 0
OK
127.0.0.1:6379> get zhangsan
"123"
# 上面的值是写死的,我们可以使用传参的方式:
127.0.0.1:6379> eval "return redis.call('set', KEYS[1], ARGV[1])" 1 lisi 321
OK
127.0.0.1:6379> get lisi
"321"
# 如果KEY和ARGV有多个,继续往后追加即可
127.0.0.1:6379> eval "return redis.call('hset', KEYS[1], ARGV[1], ARGV[2])" 1 wangwu today run
(integer) 1
127.0.0.1:6379> hget wangwu today
"run"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

3、Lua脚本文件

在redis-cli中直接写Lua脚本不够方便,也不能实现编辑和复用,通常我们会把Lua脚本放在文件里面,然后执行这个文件。

创建一个Lua脚本文件:

# my.lua:
redis.call('hset', KEYS[1], ARGV[1], ARGV[2])
return redis.call('hget', KEYS[1], ARGV[1])

# 执行结果
>redis-cli --eval my.lua 1 shandong , qingdao shinan
"shinan"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

划重点! keys...与values...之间用逗号隔开,逗号两侧必须有空格!

# my.lua
redis.call('hset', KEYS[1], ARGV[1], ARGV[2])
redis.call('hset', KEYS[1], ARGV[3], ARGV[4])
redis.call('set', KEYS[2], ARGV[5])
return redis.call('hget', KEYS[1], ARGV[1])..redis.call('hget', KEYS[1], ARGV[3])..redis.call('get', KEYS[2])

# 执行结果
D:\Redis-x64-3.2.100>redis-cli --eval my.lua 2 k1 k2 , v1 v2 v3 v4 v5
"v2v4v5"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

4、缓存Lua脚本

Lua脚本比较长的情况下, 如果每次调用脚本都需要把整个脚本传给Redis服务端,会产生比较大的网络开销。为了解决这个问题,Redis可以缓存Lua脚本并生成SHA1摘要码,后面可以直接通过摘要码来执行Lua脚本。

缓存Lua脚本涉及两个命令,首先是在服务端缓存lua脚本生成一个摘要码,用script load命令。

# 缓存
127.0.0.1:6379> script load "return 'hello world'"
"5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
# 调用
127.0.0.1:6379> evalsha "5332031c6b470dc5a0dd9b4bf2030dea6d65de91" 0
"hello world"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

我们实现一个自乘运算,让它乘以后面的参数:

# 定义一个lua
local curVal = redis.call("get", KEYS[1])
if curVal == false then
	curVal = 1
else
	curVal = tonumber(curVal)
end
curVal = curVal * tonumber(ARGV[1])
redis.call("set", KEYS[1], curVal)
return curVal

# 将lua合并为一行,语句间使用分号分割,缓存起来
script load 'local curVal = redis.call("get", KEYS[1]); if curVal == false then curVal = 1 else curVal = tonumber(curVal) end; curVal = curVal * tonumber(ARGV[1]); redis.call("set", KEYS[1], curVal); return curVal'
"2c9150e0a662c40d6766476fc288e61beb2344d0"
# 执行
127.0.0.1:6379> evalsha "2c9150e0a662c40d6766476fc288e61beb2344d0" 1 num 6
(integer) 6
127.0.0.1:6379> evalsha "2c9150e0a662c40d6766476fc288e61beb2344d0" 1 num 6
(integer) 36
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

5、脚本超时问题

Redis的指令执行本身是单线程的,这个线程还要执行客户端的Lua脚本,如果Lua脚本执行超时或者陷入了死循环,是不是就没办法为客户端提供服务了?

# 客户端1,执行死循环
127.0.0.1:6379> eval 'while(true) do end' 0
# 客户端2,无法再执行命令,即使客户端1强制退出也无法执行命令
127.0.0.1:6379> get tom
(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
  • 1
  • 2
  • 3
  • 4
  • 5

我们发现,脚本执行时间过长,其他命令都会执行失败。

脚本执行有一个超时时间的设置,默认为5秒钟。

lua-time-limit 5000
  • 1

超过5秒钟,其他客户端的命令不会等待,而是直接返回“BUSY”错误。

我们可以通过script kill命令,终止脚本的执行:

# 客户端1
127.0.0.1:6379> eval 'while(true) do end' 0
(error) ERR Error running script (call to f_eec1f08dafc6bfdf256e3820d971514a3a24267e): @user_script:1: Script killed by user with SCRIPT KILL...
(13.10s)
# 客户端2执行script kill
127.0.0.1:6379> script kill
OK
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

但是要注意的是,并不是所有的lua脚本执行的时候都可以kill,如果当前执行的Lua脚本对Redis的数据进行了修改(Set、DEL等),那么通过script kill命令是不能终止脚本运行的。

# 客户端1
127.0.0.1:6379> eval "redis.call('set', 'lua','666') while(true) do end" 0
# 客户端2
127.0.0.1:6379> script kill
(error) UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.
  • 1
  • 2
  • 3
  • 4
  • 5

这个时候执行script kill会返回UNKILLABLE错误。为什么要这样设计?为什么包含修改的脚本不能中断?因为要保证脚本运行的原子性。如果脚本执行了一部分被终止,就违背了脚本的原子性的目的。

遇到这钟情况,只能通过shutdown nosave命令,直接把Redis服务停掉。

127.0.0.1:6379> shutdown nosave
not connected>
  • 1
  • 2

正常关机是shutdown。shutdown nosave 和shutdown 的区别就在于,shutdown nosave不会进行持久化操作,意味着发生在上一次快照后的数据库修改都会丢失。

所以,我们有一些特殊需求的话,可以用Lua实现,但是要注意那些耗时的操作。

三、springboot使用redisTemplate操作lua脚本

springboot使用redisTemplate操作lua脚本

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/902753
推荐阅读
相关标签
  

闽ICP备14008679号