Redis脚本代码实现
Redis脚本数据结构
在Redis服务器的全局变量之中,定义执行Lua脚本的相关数据结构:
struct redisServer {...lua_State *lua;client *lua_client;client *lua_caller;dict *lua_scripts;unsigned long long lua_scripts_mem;mstime_t lua_time_limit;mstime_t lua_time_start;int lua_write_dirty;int lua_random_dirty;int lua_replicate_commands;int lua_multi_emmitted;int lua_repl;int lua_timeout;int lua_kill;int lua_always_replicate_comnands;dict *repl_scriptcache_dict;list *repl_scriptcache_fifo;unisgned int repl_scriptcache_size;};
这些数据字段之中:
redisServer.lua
,这是一个Lua解释器对象,整个Redis服务器所有的客户端只需要一个Lua解释器。redisServer.lua_client
,这是一个伪客户端,用于执行Lua脚本之中通过redis.call
调用的Redis命令。redisServer.lua_caller
,当前正在执行Lua脚本的客户端,如果当前没有执行Lua脚本,这个指针会被设置为NULL
。redisServer.lua_scripts
,这是一个哈希表,用于存储脚本SHA1哈希值与缓存的Lua脚本代码段的映射关系。redisServer.lua_scripts_mem
,记录了缓存Lua脚本代码段占用的内存。redisServer.lua_time_limit
,配置的Lua脚本执行时间上限,如果脚本执行的时间超过这个上限则认为是执行超时。redisServer.lua_time_start
,记录当前Lua脚本执行的启动时间戳。redisServer.lua_write_dirty
,如果当前Lua脚本中执行的Redis命令修改了键空间之中的数据,这个字段会被设置为1。redisServer.lua_random_dirty
,如果当前Lua脚本中执行的Redis命令是带有随机标记CMD_RANDOM
,这个字段会被设置为1。redisServer.lua_replicate_commands
,这是一个标记字段,标记当前的Redis是否会对Lua脚本之中的命令执行单一的复制功能,而不是将整个脚本赋值给Slave
脚本。redisServer.lua_repl
,当我们开启了Lua脚本之中单一命令的复制功能,那么这个字段用于标记当前的复制标记,以指定Redis对命令的复制行为。redisServer.lua_timeout
,如果脚本执行的时间超过了redisServer.lua_time_limit
设置的上限,那么这个字段会被设置为1。redisServer.lua_kill
,如果脚本执行时间超时,其他用户可以执行SCRIPT KILL
命令将这个字段设置为1,后续脚本会判断这个字段是否为1来决定是否终止脚本运行。redisServer.lua_always_replicate_commands
,系统配置字段,默认Redis会将整个Lua脚本代码复制给Slave
从节点。redisServer.repl_scriptcache_dict
,作为一个集合使用,记录某个sha1
的哈希值所对应的Lua脚本,是否已经被复制给slave
从节点。
Redis脚本实现逻辑
首先Redis会通过sha1hex
这个函数来计算Lua脚本代码段的SHA1哈希值:
void sha1hex(char *digest, char *script, size_t len);
脚本的哈希值可以通过digest
参数进行返回。
下面我们按照不同的功能分组,来看一下Luau脚本的代码实现逻辑。
Redis与Lua的数据转换
这里我们先来看一下,前面在概述之中介绍的Redis数据类型与Lua数据之间的转换。
Redis到Lua数据的转换
首先Redis定义了下面5个函数,用于实现底层的Redis数据到Lua数据的转换:
char *redisProtocolToLuaType_Int(lua_State *lua, char *reply);char *redisProtocolToLuaType_Bulk(lua_State *lua, char *reply);char *redisProtocolToLuaType_Status(lua_State *lua, char *reply);char *redisProtocolToLuaType_Error(lua_State *lua, char *reply);char *redisProtocolToLuaType_MultiBulk(lua_State *lua, char *reply)
这些函数的名字显式地指明了这些函数的功能,分别用于将reply
数据之中的整数、块数据、状态信息、错误信息,多块数据压入到Lua的虚拟栈之中,这里需要注意如下内容:
redisProtocolToLuaType_Bulk
这个函数中,如果块数据为空,那么会调用lua_pushboolean
向虚拟栈中压入一个false
值;否则调用lua_pushstring
函数将块数据作为Lua字符串压入Lua虚拟栈。redisProtocolToLuaType_Status
以及redisProtocolToLuaType_Error
这两个函数,都会向Lua虚拟栈之中压入一个Lua的表数据,并向这个表中插入域值对["ok"] = status
或者["err"] = error_message
。
通过上面这个5个函数,Redis定义了一个更高级的函数接口用于将Redis的数据压入Lua虚拟栈:
char *redisProtocolToLuaType(lua_State *lua, char* reply);
这个函数会根据reply
之中的特殊前缀也就是第一个字符,来对应数据类型的转换:
:
,需要转换为整数。$
,需要转换为块数据。+
,需要转换为状态数据。-
,需要转换为错误数据。*
,需要转换为多块数据。
Lua到Redis数据的转换
而Lua数据到Redis数据的转换则是通过单独的一个函数来实现的:
void luaReplyToRedisReply(client *c, lua_State *lua);
这个函数的逻辑为:
首先会通过
lua_type
函数获取Lua虚拟栈的栈顶元素类型。如果栈顶元素类型为
LUA_TSTRING
、LUA_TBOOLEAN
、LUA_TNUMBER
,那么执行将对应的数据转换为Redis的Reply数据。如果栈顶元素类型为
LUA_TTABLE
,则会根据Lua表中的数据内容,将其转化为Redis的状态、错误以及多块数据。
辅助函数
前面提到了,Redis在执行Lua脚本的过程之中,会对带有不确定性的命令的输出结果进行排序,这个功能Redis是通过下面这个辅助函数来实现的:
void luaSortArray(lua_State *lua);
luaSortArray
这个函数会通过Lua语言之中的table.sort
接口,对Lua虚拟栈之中的命令返回数据进行排序,使之有序。
在Lua语言之中的一些全局函数例如loadfile
,Redis不希望能在脚本之中能够被使用,因此通过luaRemoveUnsupportedFunctions
接口将这些函数从Lua环境之中移除。
void luaRemoveUnsupportedFunctions(lua_State *lua);
这个函数会通过将指定的Lua全局函数的函数名设置为nil
,从而在Lua环境之中将这些函数移除。
同时,为了防止数据的残留,Redis禁止在Lua脚本之中声明任何全局变量;不过我们可以使用local
关键字声明局部变量,这样Lua环境可以在脚本执行结束之后,局部变量可以自动被回收, Redis通过下面这个函数,实现这个功能:
void scriptingEnableGlobalsProtection(lua_State *lua);
Lua脚本之中的Redis接口
前面我们介绍了,可以在Lua脚本代码段之中通过redis.call
以及redis.pcall
接口调用Redis的原生命令。除了上述两个接口之外,Redis还定义了若干个C语言接口供Lua脚本代码段之中进行调用。
| Redis的C语言接口 | lua代码 | 接口含义 |
|---|---|---|
int luaRedisCallCommand(lua_State *lua) | redis.call | Lua代码中用于调用Redis原生命令的接口 |
int luaRedisPCallCommand(lua_State *lua) | redis.pcall | Lua代码中用于调用Redis原生命令的接口 |
int luaRedisSha1hexCommand(lua_State *lua) | redis.sha1hex | Lua代码中用于计算一个字符串的SHA1哈希值 |
int luaRedisErrorReplyCommand(lua_State *lua) | redis.error_reply | 在Lua代码中,向Lua虚拟栈压入错误信息 |
int luaRedisStatusReplyCommand(lua_State *lua) | redis.status_reply | 在Lua代码中,向Lua虚拟栈压入状态信息 |
int luaRedisReplicateCommandsCommand(lua_State *lua) | redis.replicate_commands | 如果截止这个接口前,脚本没有执行过写命令的话,这个接口可以开启单一命令的复制并返回true;否则这个接口会返回 false,并继续坚持整个脚本的复制工作。 |
int luaRedisSetReplCommand(lua_State *lua) | redis.set_repl | 用于设置当前脚本之中命令复制的标记。 |
int luaLogCommand(lua_State *lua) | redis.log | 通过这个接口可以向Redis服务器的日志系统之中添加日志 |
在上述这些函数接口之中,比较重要的是在Lua脚本代码段之中,通过redis.call
以及redis.pcall
来调用Redis原生命令的接口,这两个接口都是通过下面这个函数来实现的:
int luaRedisGenericCommand(lua_State *lua, int raise_error);
在这个函数之中,会执行如下的逻辑:
通过
lua_gettop
接口我们可以获取Lua虚拟栈中的参数个数并检查参数的个数,redis.call
以及redis.pcall
函数至少需要一个参数作为Redis命令的名字。从Lua的虚拟栈之中解析出
redis.call
或者redis.pcall
函数的参数。这里需要注意的是,这些参数必须是整数或者字符串形式的参数,我们在脚本之中向redis.call
函数中传输其他类型的参数。将解析出来的参数传入Redis用于在脚本之中执行原生命令的伪客户端
redisServer.lua_client
之中。通过
lookupCommand
接口查找待执行的命令,判断参数个数,并检查这个命令是否禁止在Lua脚本之中执行(命令携带了CMD_NOSCRIPT
标记)。如果脚本命令是一个写命令
CMD_WRITE
,那么会检查当前的脚本之中是否可以执行写命令。检查是否已经达到内存使用的上限,
根据命令的类型,如果是写命令
CMD_WRITE
,则将redisServer.lua_write_dirty
设置为1;如果是随机命令CMD_RANDOM
,则将redisServer.lua_random_dirty
字段设置为1。通过
call
接口来执行Redis的原生命令。从
redisServer.lua_client
这个伪客户端之中将应用层输出缓存之中的内容,通过前面介绍的redisProtocolToLuaType
接口转换为Lua数据类型,作为返回值压入Lua虚拟栈。最后,如果执行的这个Redis原生命令是带有不确定性的命令,那么会通过
luaSortArray
接口对Lua栈之中的数据进行排序。




