背景
2022年4月27日,Redis正式发布了7.0更新(其实早在2022年1月31日,Redis已经预发布了7.0rc-1,经过社区的考验后,确认没重大Bug才会正式发布)。
在众多新特性中,Redis团队把Redis Functions
放在了第一位:
可见官方对这个特性是相当重视。今天我们来一起学习下Redis Functions
。
学习前,需了解:Redis旧版本中的lua脚本
Redis为了给开发者更灵活的能力,内置了lua解释器,可以让开发者执行功能强大的「原子操作」。
例如以下场景,只通过Redis本身的数据结构,实现起来是比较低效的:
- 某个key的string value自乘2。
- 若keyA=某个数字,则同时设置keyA和keyB。
- ……
由于Redis本身并没有暴露上述命令,所以我们需要通过transactions或者watch来实现,而watch又不能保证100%成功,可能还需要引入重试。也可以通过分布式锁来实现。但这些都让系统变得更复杂、效率更低。
最高效的方式,其实是Redis内部实现好,暴露相关指令出来(例如incr指令),因为指令是原子的,所以可以放心使用。当然Redis即使暴露再多指令也没用,肯定无法覆盖各种复杂的实际场景,所以最好的方式就是提供编程能力,允许用户自定义「原子操作」,就是通过lua脚本实现。
有个命令是EVAL
:
EVAL script numkeys [key [key ...]] [arg [arg ...]]
就可以执行一段lua脚本。例如可以这样用:
代码语言:txt复制> EVAL "return ARGV[1]" 0 hello
"hello"
在lua中,Redis提供了Redis的几乎所有命令API,你可以读取key、设置key等。在执行lua过程中,Redis不会打断,只有执行完毕,才会去执行其它命令。也就是说,这段脚本是「原子操作」。
但是这种方式有些问题:
- 每次执行脚本时,需要先编译再执行,效率低(当然Redis也会针对脚本计算哈希,把编译结果存下来,如果后面的脚本跟之前一致,就不再编译了,直接取之前的结果)。但是第一次执行时,依然存在效率问题,尤其是Redis刚启动时,或者脚本初次执行时。
- lua脚本复用比较困难,因为每次都要加载一段新的脚本,函数复用成本较高。
所以,我们继续看看Redis Functions
是怎么解决这些问题的。
Redis Functions 相关指令介绍
首先我们先看看Redis Functions提供了哪些指令。
了解Redis Functions必须知道的指令
为了了解这个Redis Functions功能,你至少需要知道这2条:
Function Load
FCALL
Function Load
先看个例子
首先你可以新建一个lua
脚本,名字叫hello.lua
:
#!lua name=mylib
redis.register_function('myfunc', function(keys, args) return args[1] end)
注意上面的redis.register_function
方法,是必须的,就是通过该API(Redis给lua提供的API),来注册函数到Redis中,2个参数分别是函数名称、函数引用。
然后可以执行下面这个shell脚本(前提是你已经安装了redis、redis-cli,并启动了redis):
代码语言:shell复制cat mylib.lua | redis-cli -x FUNCTION LOAD
这样,就给redis注册了一个名叫mylib
的库,这个库里注册了一个叫myfunc
的函数。函数作用是直接return参数1。
也可在redis-cli中直接注册
当然,你也可以直接在redis-cli中调用FUNCTION LOAD
注册:
FUNCTION LOAD "#!lua name=mylib n redis.register_function('myfunc', function(keys, args) return args[1] end)"
解释 FUNCTION LOAD
语法:
代码语言:shell复制FUNCTION LOAD [REPLACE] function-code
你可以使用该语句加载一个lua模块到redis中,一个模块包含一个或多个函数。后续在redis可以调用这些函数。有如下特性:
- lua代码需要编译,在FUNCTION LOAD后会自动编译,以后每次调用时都不需要编译,调用速度都是快的。(也许你知道Redis有
EVAL
,它可以直接执行一段lua代码,但它第一次执行时需要编译,所以初次调用速度不如先FUNCTION LOAD
再调用) - 支持模块级别的重新加载。只要增加个参数
REPLACE
,就可以用新模块替换同名旧模块(若该模块是第一次加载,也会加载成功的)。 - Redis重要特性之一是内存数据可持久化保存。当你加载函数后,关闭Redis时,注册的函数也会被持久化到硬盘。重启Redis时自动重新加载之前加载的函数。
- Redis Function中执行代码是原子操作,执行过程中不会被打断。
注册完毕了,必然要执行,怎么执行呢,就是用FCALL
。
FCALL
代码语言:shell复制FCALL myfunc 0 hahahahaha
指定了函数名,hahahahaha
就是参数1。0
我们之后再介绍。
这样执行后,不出意外,会输出hahahahaha
。
解释FCALL
语法:
代码语言:shell复制FCALL function numkeys [key [key ...]] [arg [arg ...]]
function就是函数名,就是你的lua文件中,redis.register_function的第一个参数。
numkeys是指后面的参数中代表Redis Key的参数有多少个。
后面可以跟一系列参数,参数有2种:代表Redis key的参数、其它参数。
也就是说,如果你要在lua函数中访问Redis的key,必须通过参数传进来,千万不要自己去拼接。 自己拼接key参数在单机Redis没问题,但是分布式Redis中会有大问题。分布式Redis需要知道你在这个「原子操作」中可能读/写哪些key,来做一些并发控制,以免读到脏数据。
其它指令
FCALL_RO
: 只读模式调用函数(意思是在函数执行时,你无法写入Redis数据,但可以读取)。分布式Redis针对这种模式会有优化。FUNCTION DELETE
: 删除加载的lua模块(注意是模块维度删除)。FUNCTION DUMP
: 序列化已加载的函数,返回byte串。FUNCTION RESTORE
: 通过byte串(恢复)加载函数。有3种模式:FLUSH(清空已加载的再恢复)、APPEND(新增,但是同名的不再加载)、REPLACE(新增,并替代同名)。FUNCTION FLUSH
: 清空已加载函数。FUNCTION KILL
: 该命令可以终止正在执行的只读的函数。FUNCTION LIST
: 返回已加载的模块、函数列表。FUNCTION STATS
: 获取正在执行的函数的状态(函数名、参数信息、已经执行了多久)。因为有些函数耗时太久,会导致Redis这个单线程一直卡着,所以通过查询状态,用户可以决策是否通过FUNCTION KILL
终止它。
写在最后
推荐阅读官方文档:https://redis.io/docs/manual/programmability/functions-intro/
我是HullQin,公众号线下聚会游戏的作者(欢迎关注我,交个朋友)。转发本文前需获得作者HullQin授权。我独立开发了《联机桌游合集》,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋等游戏,不收费无广告。还开发了《Dice Crush》参加Game Jam 2022。喜欢可以关注我噢~我有空了会分享做游戏的相关技术,会在这个专栏里分享:《教你做小游戏》。