暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Redis源码学习(62)-Redis脚本(中)

马基雅维利incoding 2021-03-11
952

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;
    };

    这些数据字段之中:

    1. redisServer.lua
      ,这是一个Lua解释器对象,整个Redis服务器所有的客户端只需要一个Lua解释器。

    2. redisServer.lua_client
      ,这是一个伪客户端,用于执行Lua脚本之中通过redis.call
      调用的Redis命令。

    3. redisServer.lua_caller
      ,当前正在执行Lua脚本的客户端,如果当前没有执行Lua脚本,这个指针会被设置为NULL

    4. redisServer.lua_scripts
      ,这是一个哈希表,用于存储脚本SHA1哈希值与缓存的Lua脚本代码段的映射关系。

    5. redisServer.lua_scripts_mem
      ,记录了缓存Lua脚本代码段占用的内存。

    6. redisServer.lua_time_limit
      ,配置的Lua脚本执行时间上限,如果脚本执行的时间超过这个上限则认为是执行超时。

    7. redisServer.lua_time_start
      ,记录当前Lua脚本执行的启动时间戳。

    8. redisServer.lua_write_dirty
      ,如果当前Lua脚本中执行的Redis命令修改了键空间之中的数据,这个字段会被设置为1。

    9. redisServer.lua_random_dirty
      ,如果当前Lua脚本中执行的Redis命令是带有随机标记CMD_RANDOM
      ,这个字段会被设置为1。

    10. redisServer.lua_replicate_commands
      ,这是一个标记字段,标记当前的Redis是否会对Lua脚本之中的命令执行单一的复制功能,而不是将整个脚本赋值给Slave
      脚本。

    11. redisServer.lua_repl
      ,当我们开启了Lua脚本之中单一命令的复制功能,那么这个字段用于标记当前的复制标记,以指定Redis对命令的复制行为。

    12. redisServer.lua_timeout
      ,如果脚本执行的时间超过了redisServer.lua_time_limit
      设置的上限,那么这个字段会被设置为1。

    13. redisServer.lua_kill
      ,如果脚本执行时间超时,其他用户可以执行SCRIPT KILL
      命令将这个字段设置为1,后续脚本会判断这个字段是否为1来决定是否终止脚本运行。

    14. redisServer.lua_always_replicate_commands
      ,系统配置字段,默认Redis会将整个Lua脚本代码复制给Slave
      从节点。

    15. 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的虚拟栈之中,这里需要注意如下内容:

      1. redisProtocolToLuaType_Bulk
        这个函数中,如果块数据为空,那么会调用lua_pushboolean
        向虚拟栈中压入一个false
        值;否则调用lua_pushstring
        函数将块数据作为Lua字符串压入Lua虚拟栈。

      2. redisProtocolToLuaType_Status
        以及redisProtocolToLuaType_Error
        这两个函数,都会向Lua虚拟栈之中压入一个Lua的表数据,并向这个表中插入域值对["ok"] = status
        或者["err"] = error_message

      通过上面这个5个函数,Redis定义了一个更高级的函数接口用于将Redis的数据压入Lua虚拟栈:

        char *redisProtocolToLuaType(lua_State *lua, char* reply);

        这个函数会根据reply
        之中的特殊前缀也就是第一个字符,来对应数据类型的转换:

        1. :
          ,需要转换为整数。

        2. $
          ,需要转换为块数据。

        3. +
          ,需要转换为状态数据。

        4. -
          ,需要转换为错误数据。

        5. *
          ,需要转换为多块数据。

        Lua到Redis数据的转换

        而Lua数据到Redis数据的转换则是通过单独的一个函数来实现的:

          void luaReplyToRedisReply(client *c, lua_State *lua);

          这个函数的逻辑为:

          1. 首先会通过lua_type
            函数获取Lua虚拟栈的栈顶元素类型。

          2. 如果栈顶元素类型为LUA_TSTRING
            LUA_TBOOLEAN
            LUA_TNUMBER
            ,那么执行将对应的数据转换为Redis的Reply数据。

          3. 如果栈顶元素类型为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);

                  在这个函数之中,会执行如下的逻辑:

                  1. 通过lua_gettop
                    接口我们可以获取Lua虚拟栈中的参数个数并检查参数的个数,redis.call
                    以及redis.pcall
                    函数至少需要一个参数作为Redis命令的名字。

                  2. 从Lua的虚拟栈之中解析出redis.call
                    或者redis.pcall
                    函数的参数。这里需要注意的是,这些参数必须是整数或者字符串形式的参数,我们在脚本之中向redis.call
                    函数中传输其他类型的参数。

                  3. 将解析出来的参数传入Redis用于在脚本之中执行原生命令的伪客户端redisServer.lua_client
                    之中。

                  4. 通过lookupCommand
                    接口查找待执行的命令,判断参数个数,并检查这个命令是否禁止在Lua脚本之中执行(命令携带了CMD_NOSCRIPT
                    标记)。

                  5. 如果脚本命令是一个写命令CMD_WRITE
                    ,那么会检查当前的脚本之中是否可以执行写命令。

                  6. 检查是否已经达到内存使用的上限,

                  7. 根据命令的类型,如果是写命令CMD_WRITE
                    ,则将redisServer.lua_write_dirty
                    设置为1;如果是随机命令CMD_RANDOM
                    ,则将redisServer.lua_random_dirty
                    字段设置为1。

                  8. 通过call
                    接口来执行Redis的原生命令。

                  9. redisServer.lua_client
                    这个伪客户端之中将应用层输出缓存之中的内容,通过前面介绍的redisProtocolToLuaType
                    接口转换为Lua数据类型,作为返回值压入Lua虚拟栈。

                  10. 最后,如果执行的这个Redis原生命令是带有不确定性的命令,那么会通过luaSortArray
                    接口对Lua栈之中的数据进行排序。

                  文章转载自马基雅维利incoding,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                  评论