背景

2022年4月27日,Redis正式发布了7.0更新(其实早在2022年1月31日,Redis已经预发布了7.0rc-1,经过社区的考验后,确认没重大Bug才会正式发布)。

在众多新特性中,Redis团队把​​Redis Functions​​放在了第一位:

Redis 7.0 新功能 Redis Functions 是什么?可轻易实现「复杂原子操作」的神器!_Redis

可见官方对这个特性是相当重视。今天我们来一起学习下​​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脚本。例如可以这样用:

> EVAL "return ARGV[1]" 0 hello
"hello"

在lua中,Redis提供了Redis的几乎所有命令API,你可以读取key、设置key等。在执行lua过程中,Redis不会打断,只有执行完毕,才会去执行其它命令。也就是说,这段脚本是「原子操作」。

但是这种方式有些问题:

  1. 每次执行脚本时,需要先编译再执行,效率低(当然Redis也会针对脚本计算哈希,把编译结果存下来,如果后面的脚本跟之前一致,就不再编译了,直接取之前的结果)。但是第一次执行时,依然存在效率问题,尤其是Redis刚启动时,或者脚本初次执行时。
  2. 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):

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

语法:

FUNCTION LOAD [REPLACE] function-code

你可以使用该语句加载一个lua模块到redis中,一个模块包含一个或多个函数。后续在redis可以调用这些函数。有如下特性:

  • lua代码需要编译,在FUNCTION LOAD后会自动编译,以后每次调用时都不需要编译,调用速度都是快的。(也许你知道Redis有​​EVAL​​​,它可以直接执行一段lua代码,但它第一次执行时需要编译,所以初次调用速度不如先​​FUNCTION LOAD​​再调用)
  • 支持模块级别的重新加载。只要增加个参数​​REPLACE​​,就可以用新模块替换同名旧模块(若该模块是第一次加载,也会加载成功的)。
  • Redis重要特性之一是内存数据可持久化保存。当你加载函数后,关闭Redis时,注册的函数也会被持久化到硬盘。重启Redis时自动重新加载之前加载的函数。
  • Redis Function中执行代码是原子操作,执行过程中不会被打断。

注册完毕了,必然要执行,怎么执行呢,就是用​​FCALL​​。

FCALL

FCALL myfunc 0 hahahahaha

指定了函数名,​​hahahahaha​​​就是参数1。​​0​​我们之后再介绍。

这样执行后,不出意外,会输出​​hahahahaha​​。

Redis 7.0 新功能 Redis Functions 是什么?可轻易实现「复杂原子操作」的神器!_redis_02

解释FCALL

语法:

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​​终止它。

写在最后

推荐阅读官方文档:​​redis.io/docs/manual…​

我是HullQin,公众号线下聚会游戏的作者(欢迎关注公众号,联系我,交个朋友),转发本文前需获得作者​​HullQin​​​授权。我独立开发了​​《联机桌游合集》​​​,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋、UNO等游戏,不收费无广告。还独立开发了​​《合成大西瓜重制版》​​​。还开发了​​《Dice Crush》​​​参加Game Jam 2022。喜欢可以关注我噢~我有空了会分享做游戏的相关技术,会在这2个专栏里分享:​​《教你做小游戏》​​​、​​《极致用户体验》​​。