环境:
- 操作系统: Ubuntu 20.04
- Redis:7.0.0
我们知道Redis主要用途是数据存储,而可编程性(programmability)意味着用户可以在Redis服务器上运行自定义的脚本,实现自定义逻辑。用户脚本是在Redis的嵌入式沙箱脚本引擎中运行的,也就是Lua解释器。
使用脚本的优点如下:
- 可编程性:实现自定义的逻辑;
- 性能:脚本是一次性执行的,节省了与Redis服务器往返交互的开销,另外,对数据的读写都是在服务器端进行了,因此性能非常高效;
- 原子性:脚本运行是原子的,避免了竞态条件(race condition);
注意:脚本虽然在服务器端运行,但是被视为客户端程序的一部分。脚本在服务器端只是被缓存起来,一旦丢失(比如清除脚本,或者重启服务器),客户端需要把脚本重新传到服务器端。从Redis 7.0起,Redis提供了一种被称为“函数”的服务器端的编程方式。
- Redis 6.2及以下,可用
eval
和script
命令来运行和管理脚本; - Redis 7.0及以上,可用
fcall
和function
命令来运行和管理脚本;
Lua脚本
eval
/ evalsha
命令
eval
命令的格式如下:
EVAL script numkeys [key [key ...]] [arg [arg ...]]
- script:脚本内容
- numkeys:参数中key的数量
- key:Redis的key,在脚本中用
KEYS[n]
来引用,注意要大写,n
从1开始计数 - arg:其它参数,在脚本中用
ARGV[n]
来引用,注意要大写,n
从1开始计数
比如,最简单的“hello world”脚本如下:
127.0.0.1:6379> eval "return 'hello world'" 0
"hello world"
127.0.0.1:6379>
本例中脚本内容为 return 'hello world'
,参数中key的数量为0。
再来看一个带参数的脚本:
127.0.0.1:6379> eval "return ARGV[1]" 0 "hello world"
"hello world"
127.0.0.1:6379>
注意:ARGV
必须用大写。
本例中,参数里key的数量为0,但是有一个其它参数,在脚本中用 ARGV[1]
来引用。
evalsha
命令的用法参见下面的 scirpt
部分。
redis.call()
/ redis.pcall()
方法
在Lua脚本中,可以通过 redis.call()
(或者 redis.pcall()
)来运行Redis命令。
redis.call()
格式如下:
redis.call(command [,arg...])
例如:
127.0.0.1:6379> eval "return redis.call('set', 'mykey1', 'hello')" 0
OK
127.0.0.1:6379> eval "return redis.call('get', 'mykey1')" 0
"hello"
127.0.0.1:6379>
该例中key和value的值都是硬编码,若要改为传参的方式,如下:
127.0.0.1:6379> eval "return redis.call('set', KEYS[1], ARGV[1])" 1 mykey1 world
OK
127.0.0.1:6379> eval "return redis.call('get', KEYS[1])" 1 mykey1
"world"
127.0.0.1:6379>
注意: KEYS
和 ARGV
是大小写敏感的,必须用大写。
redis.pcall()
和 redis.call()
的功能基本相同,区别是提供了错误处理能力。
比如:
127.0.0.1:6379> eval "return redis.call('get', 'mykey1')" 0
"world"
127.0.0.1:6379>
如果不小心把 get
命令拼错了:
127.0.0.1:6379> eval "return redis.call('gett', 'mykey1')" 0
(error) ERR Unknown Redis command called from script script: 8426105f7bad9a131b19c9b8c374df63bc9015ef, on @user_script:1.
127.0.0.1:6379>
可见,返回了一个Redis系统错误消息。
如果换成 redis.pcall()
,就能做错误处理,比如:
127.0.0.1:6379> eval "local reply = redis.pcall('gett', 'mykey1'); if reply['err'] ~= nil then reply['err'] = 'Something is wrong' end; return reply" 0
(error) Something is wrong
127.0.0.1:6379>
这样,就可以返回自定义的消息。
redis.call()
则不能处理错误:
127.0.0.1:6379> eval "local reply = redis.call('gett', 'mykey1'); if reply['err'] ~= nil then reply['err'] = 'Something is wrong' end; return reply" 0
(error) ERR Unknown Redis command called from script script: a8ca7042298e6fa8155fcdb0e35cc99566489e91, on @user_script:1.
127.0.0.1:6379>
可见,仍然返回了Redis的系统错误消息。
script
命令
script load
命令把脚本缓存在服务器,并返回其SHA1摘要,以后就可以通过 evalsha
命令来运行脚本。
127.0.0.1:6379> script load "return 'hello ' .. ARGV[1]"
"3a0817935cd31956a48cbc3f113ca7aabf07171d"
127.0.0.1:6379>
接下来就可以通过
127.0.0.1:6379> evalsha 3a0817935cd31956a48cbc3f113ca7aabf07171d 0 world
"hello world"
127.0.0.1:6379>
注意:脚本只是被缓存到服务器端,一旦服务器重启,或者运行了 script flush
命令,脚本缓存就没了。
127.0.0.1:6379> script flush
OK
127.0.0.1:6379>
再次运行 evalsha
命令:
127.0.0.1:6379> evalsha 3a0817935cd31956a48cbc3f113ca7aabf07171d 0 world
(error) NOSCRIPT No matching script. Please use EVAL.
127.0.0.1:6379>
可见,缓存的脚本已经没了。
可以用 script exists
命令查看脚本缓存是否存在:
127.0.0.1:6379> script exists 3a0817935cd31956a48cbc3f113ca7aabf07171d
1) (integer) 0
127.0.0.1:6379>
Redis函数
尽管 script load
命令可以把脚本缓存到服务器端,但这是不可靠的,缓存的脚本随时都可能会被丢掉(比如服务器重启,或者 script flush
命令)。这就意味着客户端需要做维护脚本缓存的工作,并带来了一系列麻烦,比如:
- 所有客户端都需要保存一份脚本,增加了脚本维护和升级的麻烦;
- SHA1摘要是一串无意义的序列值;
- 多个脚本之间无法交互;
Redis 7.0引入了“函数”的概念。函数是脚本的进化版。
函数提供了和脚本相同的核心功能,但是脚本是“附加”的,而函数是Redis的组成部分。Redis会通过持久化和复制来保证其可用性。而且客户端要使用函数,只依赖于API,而不是嵌入式的脚本逻辑。
Function并没有限定其实现语言。运行引擎理论上可以使用任何语言,不过显然Lua是首选。
function load
命令
用来创建函数。例如,下面的“hello world”函数:
127.0.0.1:6379> function load "#!lua name=mylib\nredis.register_function('myfunc1', function() return 'hello world' end)"
"mylib"
127.0.0.1:6379>
fcall
命令
用来调用函数:
127.0.0.1:6379> fcall myfunc1 0
"hello world"
127.0.0.1:6379>
function list
命令
用来显示函数信息:
127.0.0.1:6379> function list libraryname mylib
1) 1) "library_name"
2) "mylib"
3) "engine"
4) "LUA"
5) "functions"
6) 1) 1) "name"
2) "myfunc1"
3) "description"
4) (nil)
5) "flags"
6) (empty array)
127.0.0.1:6379>
function delete
命令
用来删除函数:
127.0.0.1:6379> function delete mylib
OK
127.0.0.1:6379>
创建函数的其它方式
也可以通过以下方式来创建函数。
创建 test4.lua
文件,内容如下:
#!lua name=mylib
local function my_hset(keys, args)
local hash = keys[1]
local time = redis.call('TIME')[1]
return redis.call('HSET', hash, '_last_modified_', time, unpack(args))
end
local function my_hgetall(keys, args)
redis.setresp(3)
local hash = keys[1]
local res = redis.call('HGETALL', hash)
res['map']['_last_modified_'] = nil
return res
end
local function my_hlastmodified(keys, args)
local hash = keys[1]
return redis.call('HGET', keys[1], '_last_modified_')
end
redis.register_function('my_hset', my_hset)
redis.register_function('my_hgetall', my_hgetall)
redis.register_function('my_hlastmodified', my_hlastmodified)
注:
- 在
my_hset
函数中,通过redis.call('TIME')[1]
获取系统时间; - 在
my_hgetall
函数中,通过redis.setresp(3)
将redis.call
设置为返回RESP3
应答。与默认的RESP2
不同,RESP3
提供了dictionary应答,允许函数删除其中的某个字段(在Lua中是设置为nil),在本例中是删除了_last_modified_
字段;
运行下列命令来创建函数:
➜ ~ cat temp/test4.lua | redis-cli -x FUNCTION LOAD REPLACE
"mylib"
调用 my_hset
函数:
127.0.0.1:6379> fcall my_hset 1 myhash myfield "some value" another_field "another value"
(integer) 3
127.0.0.1:6379>
通过Redis命令查看:
127.0.0.1:6379> hgetall myhash
1) "_last_modified_"
2) "1651933164"
3) "myfield"
4) "some value"
5) "another_field"
6) "another value"
127.0.0.1:6379>
可见,确实创建了 myhash
。
调用 my_hgetall
函数:
127.0.0.1:6379> fcall my_hgetall 1 myhash
1) "another_field"
2) "another value"
3) "myfield"
4) "some value"
127.0.0.1:6379>
可见,返回了 _last_modified_
之外的其它信息。
调用 my_hlastmodified
函数:
127.0.0.1:6379> fcall my_hlastmodified 1 myhash
"1651933164"
127.0.0.1:6379>
可见,返回了 my_hlastmodified
信息。
最终 function list
如下:
127.0.0.1:6379> function list
1) 1) "library_name"
2) "mylib"
3) "engine"
4) "LUA"
5) "functions"
6) 1) 1) "name"
2) "my_hlastmodified"
3) "description"
4) (nil)
5) "flags"
6) (empty array)
2) 1) "name"
2) "my_hgetall"
3) "description"
4) (nil)
5) "flags"
6) (empty array)
3) 1) "name"
2) "my_hset"
3) "description"
4) (nil)
5) "flags"
6) (empty array)
127.0.0.1:6379>