淺析 redis lua 實現

關於 redis lua 的使用大家都不陌生,應用場景需要把複雜邏輯的原子性,比如計數器,分佈式鎖。見過沒用 lua 實現的鎖,不出 bug 也算是神奇

好奇實現的細節,閱讀了幾個版本,本文源碼展示爲 3.2 版本, 7.0 重構比較多,看着乾淨一些

一致性

redis> EVAL "return { KEYS[1], KEYS[2], ARGV[1], ARGV[2], ARGV[3] }" 2 key1 key2 arg1 arg2 arg3
1) "key1"
2) "key2"
3) "arg1"
4) "arg2"
5) "arg3"

上面是簡單的測試用例,其中 2 表示緊隨其後的兩個參數是 key, 我們通過 KEYS[i] 來獲取,後面的是參數,通過 ARGV[i] 獲取。我司歷史上遇到過一次 redis 主從數據不一致的情況,原因比較簡單:

Lua 腳本需要 hgetall 拿到所有數據,但是依賴順序,恰好此時底層結構 master 己經變成了 hashtable, 但是 slave 還是 ziplist, 獲取到的第一個數據當成 key 去做其它邏輯,導致主從不一致發生

引出使用 redis lua 最佳實踐之一:無論單機還是集羣模式,對於 key 的操作必須通過參數列表,顯示的傳進去,而不能依賴腳本或是隨機邏輯

結論其實顯而易見,會有數據不一致的風險,同時對於 cluster 模式,要求所有 keys 所在的 slot 必須在同一個 shard 內,這個檢測是在 smart client 或者是 cluster proxy 端

導致問題的原因在於,redis 舊版本同步時,本質上還是直接執行的 lua 腳本,這種模式叫做 verbatim replication. 如果只同步 lua 腳本修改的內容可以避免這類 issue, 類似於 mysql binlog 的 SQL 模式和 ROW 模式的區別 (也不完全一樣)

實際上 redis 也是這麼做的,3.2 版本引入 redis.replicate_commands(), 只同步變更的內容,稱爲 effects replication 模式。5.0 lua 默認爲該模式,在 7.0 中移除了舊版本的 verbatim replication 的支持

int luaRedisGenericCommand(lua_State *lua, int raise_error) {
......
    /* If we are using single commands replication, we need to wrap what
     * we propagate into a MULTI/EXEC block, so that it will be atomic like
     * a Lua script in the context of AOF and slaves. */
    if (server.lua_replicate_commands &&
        !server.lua_multi_emitted &&
        server.lua_write_dirty &&
        server.lua_repl != PROPAGATE_NONE)
    {
        execCommandPropagateMulti(server.lua_caller);
        server.lua_multi_emitted = 1;
    }

    /* Run the command */
    int call_flags = CMD_CALL_SLOWLOG | CMD_CALL_STATS;
    if (server.lua_replicate_commands) {
        /* Set flags according to redis.set_repl() settings. */
        if (server.lua_repl & PROPAGATE_AOF)
            call_flags |= CMD_CALL_PROPAGATE_AOF;
        if (server.lua_repl & PROPAGATE_REPL)
            call_flags |= CMD_CALL_PROPAGATE_REPL;
    }
    call(c, call_flags);
......
}

3.2 版本中,當 lua 虛擬機執行 redis.call 或者 redis.pcall 時調用 luaRedisGenericCommand, 如果開啓了 lua_replicate_commands 選項,那麼生成一個 multi 事務命令用於複製

同時 call 去真正執行命令時,call_flags 打上 CMD_CALL_PROPAGATE_AOFCMD_CALL_PROPAGATE_REPL 標籤,執行命令時生成同步命令

void evalGenericCommand(client *c, int evalsha) {
......
    /* If we are using single commands replication, emit EXEC if there
     * was at least a write. */
    if (server.lua_replicate_commands) {
        preventCommandPropagation(c);
        if (server.lua_multi_emitted) {
            robj *propargv[1];
            propargv[0] = createStringObject("EXEC",4);
            alsoPropagate(server.execCommand,c->db->id,propargv,1,
                PROPAGATE_AOF|PROPAGATE_REPL);
            decrRefCount(propargv[0]);
        }
    }
......
}

略去無關代碼,evalGenericCommand 函數最後判斷,如果處於 effects replication 模式,那麼只通過事務去執行產生的命令,而不是同步 lua 腳本,生成一個 exec 命令

另外爲了保證 deterministic 確定性,redis lua 做了以下事情:

  1. redis lua 不允許獲取系統時間或者外部狀態

  2. 修改了僞隨機函數 math.random, 使用同一個種子,使得每次獲取得到隨機序列是一樣的 (除非指定了 math.randomseed)

  3. 有些命令返回的結果是沒有排序的,比如 SMEMBERS, 4.0 版本 redids lua 會額外的做一次排序再返回。但是 5.0 後去掉了這個排序,因爲前面提到的 effects replication 避免了這個問題,但是使用時不要假設有任何排序,是否排序要看普通命令的文檔說明

  4. 當用戶的腳本調用 RANDOMKEY, SRANDMEMBER, TIME 隨機命令後,嘗試去修改數據庫,會報錯。但是隻讀的 lua 腳本可以調用這些 non-deterinistic 命令

緩存

一般我們用 eval 命令執行 lua 腳本內容,但是對於高頻執行的腳本,每次都要從文本中解析生成 function 開銷會很高,所以引入了 evalsha 命令

> script load "redis.call('incr', KEYS[1])"
"072f8f1f3cac2bbf3017c4af7c73e742aa085ac5"

先調用 script load 生成對應腳本的 hash 值,每次執行時只需要傳入 hash 值即可

> EVALSHA da0bf4095ef4b6f337f03ba9dcd326dbc5fc8ace 1 testkey
(nil)

對於 failover, 或第一次執行時 redis 不存在該 lua 函數則報錯

> EVALSHA da0bf4095ef4b6f337f03ba9dcd326dbc5fc8aca 1 testkey
(error) NOSCRIPT No matching script. Please use EVAL.

所以,我們在封裝 redis client 時要處理異常情況

  1. Client 初始化計算 sha 值後,直接 evalsha 調用腳本

  2. 如果失敗,返回錯誤是 NOSCRIPT, 再調用 SCRIPT LOAD 創建 lua 函數,Client 再正常調用 evalsha

void scriptCommand(client *c) {
......
    } else if (c->argc == 3 && !strcasecmp(c->argv[1]->ptr,"load")) {
        char funcname[43];
        sds sha;

        funcname[0] = 'f';
        funcname[1] = '_';
        sha1hex(funcname+2,c->argv[2]->ptr,sdslen(c->argv[2]->ptr));
        sha = sdsnewlen(funcname+2,40);
        if (dictFind(server.lua_scripts,sha) == NULL) {
            if (luaCreateFunction(c,server.lua,funcname,c->argv[2])
                    == C_ERR) {
                sdsfree(sha);
                return;
            }
        }
        addReplyBulkCBuffer(c,funcname+2,40);
        sdsfree(sha);
        forceCommandPropagation(c,PROPAGATE_REPL|PROPAGATE_AOF);
......
}

命令入口函數 scriptCommandLOAD 名字很不直觀,以爲是個只讀命令,但實際上做了很多事情:

  1. 計算 sha 值

  2. luaCreateFunction 創建運行時函數

  3. forceCommandPropagation 設置 flag 參數用於複製到從庫或者 AOF

Lua 源碼走讀

初始化

初始化只需看 scriptingInit 函數,主要功能是加載 lua 庫 (cjson, table, string, math ...), 移除不支除的函數 (loadfile, dofile), 註冊我們常用的命令表到 lua table 中 (call, pcall, log, math, random ...), 最後創建虛擬的 redisClient 用於執行命令

void scriptingInit(int setup) {
    lua_State *lua = lua_open();

    if (setup) {
        server.lua_client = NULL;
        server.lua_caller = NULL;
        server.lua_timedout = 0;
        server.lua_always_replicate_commands = 0; /* Only DEBUG can change it.*/
        ldbInit();
    }

    luaLoadLibraries(lua);
    luaRemoveUnsupportedFunctions(lua);

    /* Initialize a dictionary we use to map SHAs to scripts.
     * This is useful for replication, as we need to replicate EVALSHA
     * as EVAL, so we need to remember the associated script. */
    server.lua_scripts = dictCreate(&shaScriptObjectDictType,NULL);

    /* Register the redis commands table and fields */
    lua_newtable(lua);

    /* redis.call */
    lua_pushstring(lua,"call");
    lua_pushcfunction(lua,luaRedisCallCommand);
    lua_settable(lua,-3);
  ......
}

這裏面涉及 c 如何與 lua 語言交互,如何互相調用的問題,不用深究用到了再學即可

lua_newtable(lua); 創建 lua table 併入棧,此時位置是  -1

lua_pushstring(lua,"call"); 入棧字符串 call

lua_pushcfunction(lua,luaRedisCallCommand); 入棧函數 luaRedisCallCommand

lua_settable(lua,-3); 生成命令表,此時 table 位置是 -3,然後一次從棧中彈出,即僞代碼爲 table["call"] = luaRedisCallCommand

eval "redis.call('incr', KEYS[1])" 1 testkey

這也就是爲什麼我們的 lua 腳本可以執行 redis 命令的原因,函數查表去執行。其它命令也同理

    /* Replace math.random and math.randomseed with our implementations. */
    lua_getglobal(lua,"math");

    lua_pushstring(lua,"random");
    lua_pushcfunction(lua,redis_math_random);
    lua_settable(lua,-3);

    lua_pushstring(lua,"randomseed");
    lua_pushcfunction(lua,redis_math_randomseed);
    lua_settable(lua,-3);

    lua_setglobal(lua,"math");

這裏也看到同時修改了 random 函數行爲

執行命令

eval 函數總入口是 evalCommand, 這裏參考 3.2 源碼,非 debug 模式下執行調用 evalGenericCommand, 函數比較長,主要分三大塊

void evalGenericCommand(client *c, int evalsha) {
    lua_State *lua = server.lua;
    char funcname[43];
    long long numkeys;
    int delhook = 0, err;

    /* When we replicate whole scripts, we want the same PRNG sequence at
     * every call so that our PRNG is not affected by external state. */
    redisSrand48(0);

    /* We set this flag to zero to remember that so far no random command
     * was called. This way we can allow the user to call commands like
     * SRANDMEMBER or RANDOMKEY from Lua scripts as far as no write command
     * is called (otherwise the replication and AOF would end with non
     * deterministic sequences).
     *
     * Thanks to this flag we'll raise an error every time a write command
     * is called after a random command was used. */
    server.lua_random_dirty = 0;
    server.lua_write_dirty = 0;
    server.lua_replicate_commands = server.lua_always_replicate_commands;
    server.lua_multi_emitted = 0;
    server.lua_repl = PROPAGATE_AOF|PROPAGATE_REPL;

    /* Get the number of arguments that are keys */
    if (getLongLongFromObjectOrReply(c,c->argv[2],&numkeys,NULL) != C_OK)
        return;
    if (numkeys > (c->argc - 3)) {
        addReplyError(c,"Number of keys can't be greater than number of args");
        return;
    } else if (numkeys < 0) {
        addReplyError(c,"Number of keys can't be negative");
        return;
    }

命令執行前的檢查階段,設置隨機種子,設置一些 flag, 並檢查 keys 個數是否正確

     /* We obtain the script SHA1, then check if this function is already
     * defined into the Lua state */
    funcname[0] = 'f';
    funcname[1] = '_';
    if (!evalsha) {
        /* Hash the code if this is an EVAL call */
        sha1hex(funcname+2,c->argv[1]->ptr,sdslen(c->argv[1]->ptr));
    } else {
        /* We already have the SHA if it is a EVALSHA */
        int j;
        char *sha = c->argv[1]->ptr;

        /* Convert to lowercase. We don't use tolower since the function
         * managed to always show up in the profiler output consuming
         * a non trivial amount of time. */
        for (j = 0; j < 40; j++)
            funcname[j+2] = (sha[j] >= 'A' && sha[j] <= 'Z') ?
                sha[j]+('a'-'A') : sha[j];
        funcname[42] = '\0';
    }

    /* Push the pcall error handler function on the stack. */
    lua_getglobal(lua, "__redis__err__handler");

    /* Try to lookup the Lua function */
    lua_getglobal(lua, funcname);
    if (lua_isnil(lua,-1)) {
        lua_pop(lua,1); /* remove the nil from the stack */
        /* Function not defined... let's define it if we have the
         * body of the function. If this is an EVALSHA call we can just
         * return an error. */
        if (evalsha) {
            lua_pop(lua,1); /* remove the error handler from the stack. */
            addReply(c, shared.noscripterr);
            return;
        }
        if (luaCreateFunction(c,lua,funcname,c->argv[1]) == C_ERR) {
            lua_pop(lua,1); /* remove the error handler from the stack. */
            /* The error is sent to the client by luaCreateFunction()
             * itself when it returns C_ERR. */
            return;
        }
        /* Now the following is guaranteed to return non nil */
        lua_getglobal(lua, funcname);
        serverAssert(!lua_isnil(lua,-1));
    }

lua 中保存腳本 funcname 格式是 f_{evalsha hash}, 如果每一次執行,調用 luaCreateFunction 讓 lua 虛擬機加載 user_script 腳本

   /* Populate the argv and keys table accordingly to the arguments that
     * EVAL received. */
    luaSetGlobalArray(lua,"KEYS",c->argv+3,numkeys);
    luaSetGlobalArray(lua,"ARGV",c->argv+3+numkeys,c->argc-3-numkeys);

    /* Select the right DB in the context of the Lua client */
    selectDb(server.lua_client,c->db->id);

    /* Set a hook in order to be able to stop the script execution if it
     * is running for too much time.
     * We set the hook only if the time limit is enabled as the hook will
     * make the Lua script execution slower.
     *
     * If we are debugging, we set instead a "line" hook so that the
     * debugger is call-back at every line executed by the script. */
    server.lua_caller = c;
    server.lua_time_start = mstime();
    server.lua_kill = 0;
    if (server.lua_time_limit > 0 && server.masterhost == NULL &&
        ldb.active == 0)
    {
        lua_sethook(lua,luaMaskCountHook,LUA_MASKCOUNT,100000);
        delhook = 1;
    } else if (ldb.active) {
        lua_sethook(server.lua,luaLdbLineHook,LUA_MASKLINE|LUA_MASKCOUNT,100000);
        delhook = 1;
    }

    /* At this point whether this script was never seen before or if it was
     * already defined, we can call it. We have zero arguments and expect
     * a single return value. */
    err = lua_pcall(lua,0,1,-2);

    /* Perform some cleanup that we need to do both on error and success. */
    if (delhook) lua_sethook(lua,NULL,0,0); /* Disable hook */
    if (server.lua_timedout) {
        server.lua_timedout = 0;
        /* Restore the readable handler that was unregistered when the
         * script timeout was detected. */
        aeCreateFileEvent(server.el,c->fd,AE_READABLE,
                          readQueryFromClient,c);
    }
    server.lua_caller = NULL;

luaSetGlobalArrayKEYS, ARGS 以參數形式入棧,設置一堆 debug/slow call 相關的參數,最後 lua_pcall 執行用戶腳本,lua 虛擬機執行腳本時,如果遇到 redis.call 就會回調 redis 函數 luaRedisCallCommand, 對應的 redis.pcall 執行 luaRedisPCallCommand 函數

    if (err) {
        addReplyErrorFormat(c,"Error running script (call to %s): %s\n",
            funcname, lua_tostring(lua,-1));
        lua_pop(lua,2); /* Consume the Lua reply and remove error handler. */
    } else {
        /* On success convert the Lua return value into Redis protocol, and
         * send it to * the client. */
        luaReplyToRedisReply(c,lua); /* Convert and consume the reply. */
        lua_pop(lua,1); /* Remove the error handler. */
    }

    /* If we are using single commands replication, emit EXEC if there
     * was at least a write. */
    if (server.lua_replicate_commands) {
        preventCommandPropagation(c);
        if (server.lua_multi_emitted) {
            robj *propargv[1];
            propargv[0] = createStringObject("EXEC",4);
            alsoPropagate(server.execCommand,c->db->id,propargv,1,
                PROPAGATE_AOF|PROPAGATE_REPL);
            decrRefCount(propargv[0]);
        }
    }

......
    if (evalsha && !server.lua_replicate_commands) {
        if (!replicationScriptCacheExists(c->argv[1]->ptr)) {
            /* This script is not in our script cache, replicate it as
             * EVAL, then add it into the script cache, as from now on
             * slaves and AOF know about it. */
            robj *script = dictFetchValue(server.lua_scripts,c->argv[1]->ptr);

            replicationScriptCacheAdd(c->argv[1]->ptr);
            serverAssertWithInfo(c,NULL,script != NULL);
            rewriteClientCommandArgument(c,0,
                resetRefCount(createStringObject("EVAL",4)));
            rewriteClientCommandArgument(c,1,script);
            forceCommandPropagation(c,PROPAGATE_REPL|PROPAGATE_AOF);
        }
    }
}

代碼有點長,總體就是執行超時處理,生成 exec 用於複製,最後如果 replication 從庫沒有執行過這個 evlsha 腳本,並且當前模式不是 lua_always_replicate_commands 要把腳本真實內容也先同步到 replication

這裏還有最重要的是 luaReplyToRedisReply(c,lua); 將 lua 返回值,轉換成 redis RESP 格式

再來看一下 luaRedisGenericCommand 是如何調用 redis 函數

int luaRedisGenericCommand(lua_State *lua, int raise_error) {
    int j, argc = lua_gettop(lua);
    struct redisCommand *cmd;
    client *c = server.lua_client;
    sds reply;

    /* Cached across calls. */
    static robj **argv = NULL;
    static int argv_size = 0;
    static robj *cached_objects[LUA_CMD_OBJCACHE_SIZE];
    static size_t cached_objects_len[LUA_CMD_OBJCACHE_SIZE];
    static int inuse = 0;   /* Recursive calls detection. */

    /* By using Lua debug hooks it is possible to trigger a recursive call
     * to luaRedisGenericCommand(), which normally should never happen.
     * To make this function reentrant is futile and makes it slower, but
     * we should at least detect such a misuse, and abort. */
    if (inuse) {
        char *recursion_warning =
            "luaRedisGenericCommand() recursive call detected. "
            "Are you doing funny stuff with Lua debug hooks?";
        serverLog(LL_WARNING,"%s",recursion_warning);
        luaPushError(lua,recursion_warning);
        return 1;
    }
    inuse++;

    /* Require at least one argument */
    if (argc == 0) {
        luaPushError(lua,
            "Please specify at least one argument for redis.call()");
        inuse--;
        return raise_error ? luaRaiseError(lua) : 1;
    }

    /* Build the arguments vector */
    if (argv_size < argc) {
        argv = zrealloc(argv,sizeof(robj*)*argc);
        argv_size = argc;
    }

    for (j = 0; j < argc; j++) {
        char *obj_s;
        size_t obj_len;
        char dbuf[64];

        if (lua_type(lua,j+1) == LUA_TNUMBER) {
            /* We can't use lua_tolstring() for number -> string conversion
             * since Lua uses a format specifier that loses precision. */
            lua_Number num = lua_tonumber(lua,j+1);

            obj_len = snprintf(dbuf,sizeof(dbuf),"%.17g",(double)num);
            obj_s = dbuf;
        } else {
            obj_s = (char*)lua_tolstring(lua,j+1,&obj_len);
            if (obj_s == NULL) break; /* Not a string. */
        }

        /* Try to use a cached object. */
        if (j < LUA_CMD_OBJCACHE_SIZE && cached_objects[j] &&
            cached_objects_len[j] >= obj_len)
        {
            sds s = cached_objects[j]->ptr;
            argv[j] = cached_objects[j];
            cached_objects[j] = NULL;
            memcpy(s,obj_s,obj_len+1);
            sdssetlen(s, obj_len);
        } else {
            argv[j] = createStringObject(obj_s, obj_len);
        }
    }

    /* Check if one of the arguments passed by the Lua script
     * is not a string or an integer (lua_isstring() return true for
     * integers as well). */
    if (j != argc) {
        j--;
        while (j >= 0) {
            decrRefCount(argv[j]);
            j--;
        }
        luaPushError(lua,
            "Lua redis() command arguments must be strings or integers");
        inuse--;
        return raise_error ? luaRaiseError(lua) : 1;
    }

    /* Setup our fake client for command execution */
    c->argv = argv;
    c->argc = argc;

    /* Log the command if debugging is active. */
    if (ldb.active && ldb.step) {
        sds cmdlog = sdsnew("<redis>");
        for (j = 0; j < c->argc; j++) {
            if (j == 10) {
                cmdlog = sdscatprintf(cmdlog," ... (%d more)",
                    c->argc-j-1);
            } else {
                cmdlog = sdscatlen(cmdlog," ",1);
                cmdlog = sdscatsds(cmdlog,c->argv[j]->ptr);
            }
        }
        ldbLog(cmdlog);
    }

這裏的功能,主要是從 lua 虛擬機中獲取 eval 腳本的參數,賦值給 redisClient, 爲以後執行命令做準備

    /* Command lookup */
    cmd = lookupCommand(argv[0]->ptr);
    if (!cmd || ((cmd->arity > 0 && cmd->arity != argc) ||
                   (argc < -cmd->arity)))
    {
        if (cmd)
            luaPushError(lua,
                "Wrong number of args calling Redis command From Lua script");
        else
            luaPushError(lua,"Unknown Redis command called from Lua script");
        goto cleanup;
    }
    c->cmd = c->lastcmd = cmd;

lookupCommand 查表,找到要執行的 redis 命令

    /* There are commands that are not allowed inside scripts. */
    if (cmd->flags & CMD_NOSCRIPT) {
        luaPushError(lua, "This Redis command is not allowed from scripts");
        goto cleanup;
    }

如果是不允許在 lua 中執行的命令,報錯退出

    /* Write commands are forbidden against read-only slaves, or if a
     * command marked as non-deterministic was already called in the context
     * of this script. */
    if (cmd->flags & CMD_WRITE) {
        if (server.lua_random_dirty && !server.lua_replicate_commands) {
            luaPushError(lua,
                "Write commands not allowed after non deterministic commands. Call redis.replicate_commands() at the start of your script in order to switch to single commands replication mode.");
            goto cleanup;
        } else if (server.masterhost && server.repl_slave_ro &&
                   !server.loading &&
                   !(server.lua_caller->flags & CLIENT_MASTER))
        {
            luaPushError(lua, shared.roslaveerr->ptr);
            goto cleanup;
        } else if (server.stop_writes_on_bgsave_err &&
                   server.saveparamslen > 0 &&
                   server.lastbgsave_status == C_ERR)
        {
            luaPushError(lua, shared.bgsaveerr->ptr);
            goto cleanup;
        }
    }

    /* If we reached the memory limit configured via maxmemory, commands that
     * could enlarge the memory usage are not allowed, but only if this is the
     * first write in the context of this script, otherwise we can't stop
     * in the middle. */
    if (server.maxmemory && server.lua_write_dirty == 0 &&
        (cmd->flags & CMD_DENYOOM))
    {
        if (freeMemoryIfNeeded() == C_ERR) {
            luaPushError(lua, shared.oomerr->ptr);
            goto cleanup;
        }
    }

    if (cmd->flags & CMD_RANDOM) server.lua_random_dirty = 1;
    if (cmd->flags & CMD_WRITE) server.lua_write_dirty = 1;

設置 cmd->flags

    /* If this is a Redis Cluster node, we need to make sure Lua is not
     * trying to access non-local keys, with the exception of commands
     * received from our master or when loading the AOF back in memory. */
    if (server.cluster_enabled && !server.loading &&
        !(server.lua_caller->flags & CLIENT_MASTER))
    {
        /* Duplicate relevant flags in the lua client. */
        c->flags &= ~(CLIENT_READONLY|CLIENT_ASKING);
        c->flags |= server.lua_caller->flags & (CLIENT_READONLY|CLIENT_ASKING);
        if (getNodeByQuery(c,c->cmd,c->argv,c->argc,NULL,NULL) !=
                           server.cluster->myself)
        {
            luaPushError(lua,
                "Lua script attempted to access a non local key in a "
                "cluster node");
            goto cleanup;
        }
    }

如果是 cluster 模式,要保證 lua 的 keys 所在的 slots 必須在本地 shard

    /* If we are using single commands replication, we need to wrap what
     * we propagate into a MULTI/EXEC block, so that it will be atomic like
     * a Lua script in the context of AOF and slaves. */
    if (server.lua_replicate_commands &&
        !server.lua_multi_emitted &&
        server.lua_write_dirty &&
        server.lua_repl != PROPAGATE_NONE)
    {
        execCommandPropagateMulti(server.lua_caller);
        server.lua_multi_emitted = 1;
    }

如果是 effect replication 模式,生成 multi 事務命令用於複製

    /* Run the command */
    int call_flags = CMD_CALL_SLOWLOG | CMD_CALL_STATS;
    if (server.lua_replicate_commands) {
        /* Set flags according to redis.set_repl() settings. */
        if (server.lua_repl & PROPAGATE_AOF)
            call_flags |= CMD_CALL_PROPAGATE_AOF;
        if (server.lua_repl & PROPAGATE_REPL)
            call_flags |= CMD_CALL_PROPAGATE_REPL;
    }
    call(c,call_flags);

這裏纔去真正的執行命令,call_flags 參數用於控制是否複製,是否生成 AOF 等等

    /* Convert the result of the Redis command into a suitable Lua type.
     * The first thing we need is to create a single string from the client
     * output buffers. */
    if (listLength(c->reply) == 0 && c->bufpos < PROTO_REPLY_CHUNK_BYTES) {
        /* This is a fast path for the common case of a reply inside the
         * client static buffer. Don't create an SDS string but just use
         * the client buffer directly. */
        c->buf[c->bufpos] = '\0';
        reply = c->buf;
        c->bufpos = 0;
    } else {
        reply = sdsnewlen(c->buf,c->bufpos);
        c->bufpos = 0;
        while(listLength(c->reply)) {
            robj *o = listNodeValue(listFirst(c->reply));

            reply = sdscatlen(reply,o->ptr,sdslen(o->ptr));
            listDelNode(c->reply,listFirst(c->reply));
        }
    }
    if (raise_error && reply[0] != '-') raise_error = 0;
    redisProtocolToLuaType(lua,reply);
......
}

redisProtocolToLuaType 把 redis 結果轉換成 lua 類型返回給 lua 虛擬機

小結

感慨一下,redis 僅有的幾個數據結構就能滿足 90% 的業務需求,最近幾個版本優化非常明顯,大家趕緊升級吧,享受新版的福利

從生產環境上看,大版本穩定半年到一年,大膽升級準沒錯,還在抱殘守缺的用 redis 3.X 4.X 的活該遇到各種問題

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Qx5qzB2lVnOo546WheL9EQ