redis源碼分析 - lua環境的創建與分析

redis中 lua 環境的創建和初始化

redis 中,lua 環境的初始化,是從 redis.c/initServer() 函數中,調用 scriptingInit() 函數開始的。

關於 scriptingInit() 的描述

/* Initialize the scripting environment.
 * It is possible to call this function to reset the scripting environment
 * assuming that we call scriptingRelease() before.
 * See scriptingReset() for more information. */

也就是說,這個函數是初始化lua環境的,當然,如果調用了 scriptingRelease() 在調用該函數,可以重置 lua 腳本環境。我們進入到函數中看一下代碼。

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

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

    /* redis.pcall */
    lua_pushstring(lua,"pcall");
    lua_pushcfunction(lua,luaRedisPCallCommand);
    lua_settable(lua,-3);

    /* redis.log and log levels. */
    lua_pushstring(lua,"log");
    lua_pushcfunction(lua,luaLogCommand);
    lua_settable(lua,-3);

    lua_pushstring(lua,"LOG_DEBUG");
    lua_pushnumber(lua,REDIS_DEBUG);
    lua_settable(lua,-3);

    lua_pushstring(lua,"LOG_VERBOSE");
    lua_pushnumber(lua,REDIS_VERBOSE);
    lua_settable(lua,-3);

    lua_pushstring(lua,"LOG_NOTICE");
    lua_pushnumber(lua,REDIS_NOTICE);
    lua_settable(lua,-3);

    lua_pushstring(lua,"LOG_WARNING");
    lua_pushnumber(lua,REDIS_WARNING);
    lua_settable(lua,-3);

    /* redis.sha1hex */
    lua_pushstring(lua, "sha1hex");
    lua_pushcfunction(lua, luaRedisSha1hexCommand);
    lua_settable(lua, -3);

    /* redis.error_reply and redis.status_reply */
    lua_pushstring(lua, "error_reply");
    lua_pushcfunction(lua, luaRedisErrorReplyCommand);
    lua_settable(lua, -3);
    lua_pushstring(lua, "status_reply");
    lua_pushcfunction(lua, luaRedisStatusReplyCommand);
    lua_settable(lua, -3);

    /* Finally set the table as 'redis' global var. */
    lua_setglobal(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");

    /* Add a helper function that we use to sort the multi bulk output of non
     * deterministic commands, when containing 'false' elements. */
    {
        char *compare_func =    "function __redis__compare_helper(a,b)\n"
                                "  if a == false then a = '' end\n"
                                "  if b == false then b = '' end\n"
                                "  return a<b\n"
                                "end\n";
        luaL_loadbuffer(lua,compare_func,strlen(compare_func),"@cmp_func_def");
        lua_pcall(lua,0,0,0);
    }

    /* Add a helper function we use for pcall error reporting.
     * Note that when the error is in the C function we want to report the
     * information about the caller, that's what makes sense from the point
     * of view of the user debugging a script. */
    {
        char *errh_func =       "local dbg = debug\n"
                                "function __redis__err__handler(err)\n"
                                "  local i = dbg.getinfo(2,'nSl')\n"
                                "  if i and i.what == 'C' then\n"
                                "    i = dbg.getinfo(3,'nSl')\n"
                                "  end\n"
                                "  if i then\n"
                                "    return i.source .. ':' .. i.currentline .. ': ' .. err\n"
                                "  else\n"
                                "    return err\n"
                                "  end\n"
                                "end\n";
        luaL_loadbuffer(lua,errh_func,strlen(errh_func),"@err_handler_def");
        lua_pcall(lua,0,0,0);
    }

    /* Create the (non connected) client that we use to execute Redis commands
     * inside the Lua interpreter.
     * Note: there is no need to create it again when this function is called
     * by scriptingReset(). */
    if (server.lua_client == NULL) {
        server.lua_client = createClient(-1);
        server.lua_client->flags |= REDIS_LUA_CLIENT;
    }

    /* Lua beginners often don't use "local", this is likely to introduce
     * subtle bugs in their code. To prevent problems we protect accesses
     * to global variables. */
    scriptingEnableGlobalsProtection(lua);

    server.lua = lua;
}

1、 lua_open() 函數,創建一個lua環境
2、 luaLoadLibraries(lua),在新創建的lua環境中載入相應的庫,同時刪除庫中不需要的函數,防止從外部引入不安全的代碼 luaRemoveUnsupportedFunctions(lua)

void luaLoadLibraries(lua_State *lua) {
    luaLoadLib(lua, "", luaopen_base);
    luaLoadLib(lua, LUA_TABLIBNAME, luaopen_table);
    luaLoadLib(lua, LUA_STRLIBNAME, luaopen_string);
    luaLoadLib(lua, LUA_MATHLIBNAME, luaopen_math);
    luaLoadLib(lua, LUA_DBLIBNAME, luaopen_debug);
    luaLoadLib(lua, "cjson", luaopen_cjson);
    luaLoadLib(lua, "struct", luaopen_struct);
    luaLoadLib(lua, "cmsgpack", luaopen_cmsgpack);
    luaLoadLib(lua, "bit", luaopen_bit);

#if 0 /* Stuff that we don't load currently, for sandboxing concerns. */
    luaLoadLib(lua, LUA_LOADLIBNAME, luaopen_package);
    luaLoadLib(lua, LUA_OSLIBNAME, luaopen_os);
#endif
}

/* Remove a functions that we don't want to expose to the Redis scripting
 * environment. */
void luaRemoveUnsupportedFunctions(lua_State *lua) {
    lua_pushnil(lua);   //將空值入棧,此時棧頂元素爲空值
    lua_setglobal(lua,"loadfile");  //出棧,取出棧頂元素,即空值,並將其作爲 loadfile 的值,也就相當於取消了 loadfile 這個函數的作用,置空了
}

3、 lua_newtable(lua) 創建一張空表,併入棧
4、 將 c 中的luaRedisCallCommand 函數壓入棧,作爲表的函數;將 c 中的luaRedisPCallCommand 函數入棧,作爲表的函數;將 c 中的luaLogCommand 函數入棧作爲表的函數;將表複製爲 redis,即創建 redis 全局 table。下面對其中一個註冊函數進行解釋

lua_newtable(lua);

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

首先創建一個空表,入棧,此時棧頂元素爲 table。然後將字符串 “call” 入棧,再將 c 函數 luaRedisCallCommand 函數入棧

lua_settable(lua, -3)

意思是 t[k]=v,t 爲索引-3,棧中,-3 位元素 table,-1 爲 luaRedisCallCommand,-2 爲字符串call,v 表示棧頂元素,k表示棧頂元素的下一個元素,索引上面這個函數的意思就是

table["call"]=luaRedisCallCommand

就是將lua 中的call 函數註冊爲 c 中的 luaRedisCallCommand 函數,也可以記爲

table.call=luaRedisCallCommand

後面代碼意思都雷同,都是註冊 c 函數到 lua 中。

注意: lua_settable(lua_State *L, int index) 會將棧頂兩個元素彈出。

lua_setglobal(lua, "redis")

將表作爲 redis的值,即創建的表爲 redis 表,該表中包含以下函數:

  • redis.call 函數和 redis.pcall 函數,用於執行 redis 命令
  • redis.log 記錄日誌,日誌級別對應爲 redid.LOG_DEBUGredis.LOG_VERBOSEredis.LOG_NOTICEredis.LOG_WARNING
  • redis.sha1hex,計算 SHA1 校驗和
  • redis.error_replyredis.status_reply 函數,返回 redis 錯誤信息

5、 用 c 中自制的隨機函數替換 Lua 中原有的隨機函數
6、 創建排序輔助函數, Lua 環境使用這個輔助函數來對一部分 redis 命令的結果進行排序,從而消除這些命令的不確定性。

function __redis__compare_helper(a,b)
    if a == false 
    then 
        a = '' 
    end
    if b == false 
    then 
        b = '' 
    end
    return a<b
end

7、 創建 redis.pcall 函數的錯誤報告輔助函數,這個函數可以提供更加詳細發出錯信息,比如能夠在 c 函數的出錯信息中提供調用者的信息

local dbg = debug;
function __redis_err_handler(err)
    local i = dbg.getinfo (2, 'sS1')
    if i and i.what = 'C'
    then
        i = dgb.getinfo (3, 'nS1')
    end
    if i
    then
        return i.source .. ':' .. i.currentLine .. ':' .. err
    else
        return err
    end
end

8、 對 lua 環境中的全局環境進行保護,防止用戶在執行 lua 腳本的過程中,將額外的全局變量添加到 Lua 環境中
9、 將完成修改的 lua 環境保存到服務器狀態的 lua 屬性中。

創建排序輔助函數 __redis__compare_helper

在 redis 中產生不同輸出的命令稱爲“帶有不確定性的命令”,比如:

  • SINTER
  • SUNION
  • SDIFF
  • SMEMBERS
  • HKEYS
  • HVALS
  • KEYS

比如對 SMEMBERS 來說,在兩個值相同,但是順序不同的集合中,使用 SMEMBERS 得到的結果是不同的,輸出的值的順序不同。這樣的輸出就是不確定性,因爲它本身不會排序。而 lua 中創建的這個輔助排序函數,可以用來消除這種不確定性。當 lua 執行完一個帶有不確定性的命令時,程序會使用 __redis__compare_helper 作爲對比函數,自動調用 tables.sort 函數對命令進行一次排序,一次來保證相同的數據集總是產生相同的輸出。

那,到底是什麼一個代碼流程呢,因爲 redis.call 和 redis.pcall 在初始化的時候,已經使用 c 中的函數進行了註冊,所以當調用 redis.call 或者 redis.pcall 的時候實際喚醒調用的是 c 函數 luaRedisCallCommand 或者 luaRedisPCallCommand

int luaRedisCallCommand(lua_State *lua) {
    return luaRedisGenericCommand(lua,1);
}

int luaRedisPCallCommand(lua_State *lua) {
    return luaRedisGenericCommand(lua,0);
}

這兩個函數,都是調用的 luaRedisGenericCommand 函數,此時,在 luaGenericCommand 函數中,在滿足條件的情況下,調用 luaSortArray 函數。

if ((cmd->flags & REDIS_CMD_SORT_FOR_SCRIPT) &&
    (reply[0] == '*' && reply[1] != '-')) {
        luaSortArray(lua);
}

滿足上面條件的時候,才調用 luaSortArray 這個函數,函數定義如下所示

/* Sort the array currently in the stack. We do this to make the output
 * of commands like KEYS or SMEMBERS something deterministic when called
 * from Lua (to play well with AOf/replication).
 *
 * The array is sorted using table.sort itself, and assuming all the
 * list elements are strings. */
void luaSortArray(lua_State *lua) {
    /* Initial Stack: array */
    lua_getglobal(lua,"table");
    lua_pushstring(lua,"sort");
    lua_gettable(lua,-2);       /* Stack: array, table, table.sort */
    lua_pushvalue(lua,-3);      /* Stack: array, table, table.sort, array */
    if (lua_pcall(lua,1,0,0)) {
        /* Stack: array, table, error */

        /* We are not interested in the error, we assume that the problem is
         * that there are 'false' elements inside the array, so we try
         * again with a slower function but able to handle this case, that
         * is: table.sort(table, __redis__compare_helper) */
        lua_pop(lua,1);             /* Stack: array, table */
        lua_pushstring(lua,"sort"); /* Stack: array, table, sort */
        lua_gettable(lua,-2);       /* Stack: array, table, table.sort */
        lua_pushvalue(lua,-3);      /* Stack: array, table, table.sort, array */
        lua_getglobal(lua,"__redis__compare_helper");   //table.sort(list [,compare])
        /* Stack: array, table, table.sort, array, __redis__compare_helper */
        lua_call(lua,2,0);
    }
    /* Stack: array (sorted), table */
    lua_pop(lua,1);             /* Stack: array (sorted) */
}

將 table.sort 中的 comp 參數作爲 __redis_compare_helper 輔助排序函數進行排序

lua 環境協作組件

lua 服務器創建了兩個用於與 lua 環境進行寫作的組件,分別是負責執行 lua 腳本的 redis 命令的僞客戶端和保存 lua 腳本的 lua_scripts 字典。

typedef struct Server {
    ...
    /* Scripting */
    lua_State *lua; /* The Lua interpreter. We use just one for all clients */
    redisClient *lua_client;   /* The "fake client" to query Redis from Lua */
    redisClient *lua_caller;   /* The client running EVAL right now, or NULL */
    dict *lua_scripts;         /* A dictionary of SHA1 -> Lua scripts */
    mstime_t lua_time_limit;  /* Script timeout in milliseconds */
    mstime_t lua_time_start;  /* Start time of script, milliseconds time */
    int lua_write_dirty;  /* True if a write command was called during the
                             execution of the current script. */
    int lua_random_dirty; /* True if a random command was called during the
                             execution of the current script. */
    int lua_timedout;     /* True if we reached the time limit for script
                             execution. */
    int lua_kill;         /* Kill the script if true. */
    ...
};

僞客戶端

redis 命令執行必須有相應的客戶端狀態,redis服務器專門爲 lua 環境創建了一個僞客戶端,server 中的 lua_client 成員就是 lua 的僞客戶端,當初始化 lua 環境時,對僞客戶端初始化如下(scriptingInit()):

   /* Create the (non connected) client that we use to execute Redis commands
     * inside the Lua interpreter.
     * Note: there is no need to create it again when this function is called
     * by scriptingReset(). */
    if (server.lua_client == NULL) {
        server.lua_client = createClient(-1);
        server.lua_client->flags |= REDIS_LUA_CLIENT;
    }

Lua 腳本使用 redis.call 或者 redis.pcall 執行 redis 命令的時候,步驟如下:

  • Lua 環境將 redis.call 或者 redis.pcall 想要執行的命令傳給僞客戶端
  • 僞客戶端將腳本想要執行的命令傳給命令執行器 (call)
  • 命令執行器執行命令後,將結果返回給僞客戶端
  • 僞客戶端接收到結果並將結果返回給 lua 環境
  • lua 環境接收到結果之後,有將結果返回給 redis.call 或者 redis.pcall 函數
  • 接收到結果的 redis.call 或者 redis.pcall 函數,將命令結果作爲函數返回值返回給腳本中的調用者。
    以上步驟摘自 《Redis 設計與實現》 黃健宏著,機械工業出版社,20.1.1節 僞客戶端

使用客戶端執行lua腳本

./redis-cli -h ${IP} -p ${PORT} -a ${PASSWORD} --eval <lua script>

lua_scripts 字典

另一個lua環境協作組件是 lua_scripts 字典。這個字典的值,是 lua 對應的腳本,鍵是 lua 腳本的 SHA1 校驗和。這個可以在 scripting.c/luaCreateFunction() 函數中查看

  {
        int retval = dictAdd(server.lua_scripts,
                             sdsnewlen(funcname+2,40),body);    //dict, key is sha1, value is script
        redisAssertWithInfo(c,NULL,retval == DICT_OK);
        incrRefCount(body);
  }

funcname 就是 lua 腳本的SHA1校驗和,body 就是 lua 對應的腳本

EVAL 命令的實現

第一步:
當客戶端向服務器發送 EVAL 命令執行一段 lua 腳本的時候,服務器首先在 lua 環境中,爲傳入的腳本定義一個 lua 函數,函數名由 f_ 前綴加上腳本的 SHA1 校驗和,而函數體即爲腳本本身。

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

    /* 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;

    /* Get the number of arguments that are keys */
    if (getLongLongFromObjectOrReply(c,c->argv[2],&numkeys,NULL) != REDIS_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;
    }

    /* 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]) == REDIS_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 REDIS_ERR. */
            return;
        }
        /* Now the following is guaranteed to return non nil */
        lua_getglobal(lua, funcname);
        redisAssert(!lua_isnil(lua,-1));
    }
    ...
}

首先解析參數,可以查看 EVAL 命令的語法

EVAL script numkeys key [key ...] arg [arg ...]

在 lua 環境創建對應的 lua 函數,保存在變量 funcname 中,

funcname[0] = 'f';
funcname[1] = '_';

表示函數名以 f_ 作爲前綴,使用 shahex1() 函數獲取腳本的 SHA1 校驗和,保存在 funcname 中,此時的 funcname 將作爲 lua 環境中腳本對應的函數名。使用 luaCreateFunction 函數在 lua 環境中創建該腳本的 lua 函數,同時將腳本即 lua 環境中對應的函數名加入到 lua_scripts 字典中。

/* Define a lua function with the specified function name and body.
 * The function name musts be a 2 characters long string, since all the
 * functions we defined in the Lua context are in the form:
 *
 *   f_<hex sha1 sum>
 *
 * On success REDIS_OK is returned, and nothing is left on the Lua stack.
 * On error REDIS_ERR is returned and an appropriate error is set in the
 * client context. */
int luaCreateFunction(redisClient *c, lua_State *lua, char *funcname, robj *body) {
    sds funcdef = sdsempty();

    funcdef = sdscat(funcdef,"function ");  //create lua function
    funcdef = sdscatlen(funcdef,funcname,42);
    funcdef = sdscatlen(funcdef,"() ",3);
    funcdef = sdscatlen(funcdef,body->ptr,sdslen(body->ptr));
    funcdef = sdscatlen(funcdef,"\nend",4);

    if (luaL_loadbuffer(lua,funcdef,sdslen(funcdef),"@user_script")) {
        addReplyErrorFormat(c,"Error compiling script (new function): %s\n",
            lua_tostring(lua,-1));
        lua_pop(lua,1);
        sdsfree(funcdef);
        return REDIS_ERR;
    }
    sdsfree(funcdef);
    if (lua_pcall(lua,0,0,0)) {     //execute lua function
        addReplyErrorFormat(c,"Error running script (new function): %s\n",
            lua_tostring(lua,-1));
        lua_pop(lua,1);
        return REDIS_ERR;
    }

    /* We also save a SHA1 -> Original script map in a dictionary
     * so that we can replicate / write in the AOF all the
     * EVALSHA commands as EVAL using the original script. */
    {
        int retval = dictAdd(server.lua_scripts,
                             sdsnewlen(funcname+2,40),body);    //dict, key is sha1, value is script
        redisAssertWithInfo(c,NULL,retval == DICT_OK);
        incrRefCount(body);
    }
    return REDIS_OK;
}

第二步:
執行腳本之前,服務器還需要做一些設置鉤子和傳入參數的準備工作

a、 將EVAL命令中傳入的參數和腳本參數分爲保存在 KEYS 和 ARGV 數組中,並作爲全局變量保存在 lua 環境中

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

b、 爲 lua 環境裝載超時吃力鉤子,當腳本運行時間超時時,客戶端通過 SCRIPT KILL 命令可以停止腳本,也可以通過 SHUTDOWN 命令停止服務器。

c、 執行腳本
d、 移除鉤子
e、 將執行腳本函數得到的結果保存到客戶端狀態的輸出緩衝去,等待服務器將結果返回給客戶端

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. */
 }

EVALSHA 命令

每一個被 EVAL 命令執行過的腳本,在 lua 環境中都會有一個與腳本對應的 lua 函數,函數的名字由 f_ 前綴和腳本的 SHA1 校驗和組成。主要這個函數在 lua 環境中定義了,就會在 lua_scripts 中保存,那麼使用 EVALSHA 命令,即使不知道腳本本身,也可以直接使用腳本的校驗和來調用腳本對應的 lua 環境中的函數,這就是 EVALSHA 實現的原理

其他的腳本命令

這裏主要講下命令的作用

SCRIPT FLUSH,用於清楚服務器中和 Lua 腳本有關的信息,這個命令會釋放並重建 lua_scripts 字典,關閉現有的 lua 環境並重新創建一個新的 lua 環境

SCRIPT EXISTS,根據輸入的 SHA1 校驗和,檢查校驗和對應的腳本是否存在於服務器中。(注意:該命令允許一次傳入多個 SHA1 校驗和)

SCRIPT LOAD,首先在 lua 環境中爲腳本創建對應的 lua 函數,然後將腳本和 SHA1 校驗和保存在 lua_scripts 字典中

SCRIPT KILL,當服務器設置了參數 lua-time-limit 時,每次在執行 lua 腳本之前,都會設置一個超時鉤子,腳本運行時,一旦鉤子發現腳本運行時間已經超時,鉤子將定期檢查是否有 SCRIPT KILL 或者 SHUTDOWN 命令到達。如果超時腳本未執行任何寫操作,客戶端可以通過 SCRIPT KILL 命令停止腳本,並向執行腳本的客戶端返回一個錯誤信息。處理完之後,服務器將繼續執行。如果腳本已經執行過寫操作,那麼客戶端只能通過 SHUTDOWN 命令停止服務器,防止不合法的數據寫入服務器中。

/* ---------------------------------------------------------------------------
 * SCRIPT command for script environment introspection and control
 * ------------------------------------------------------------------------- */

void scriptCommand(redisClient *c) {
    if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"flush")) {
        scriptingReset();
        addReply(c,shared.ok);
        replicationScriptCacheFlush();
        server.dirty++; /* Propagating this command is a good idea. */
    } else if (c->argc >= 2 && !strcasecmp(c->argv[1]->ptr,"exists")) {
        int j;

        addReplyMultiBulkLen(c, c->argc-2);
        for (j = 2; j < c->argc; j++) {
            if (dictFind(server.lua_scripts,c->argv[j]->ptr))
                addReply(c,shared.cone);
            else
                addReply(c,shared.czero);
        }
    } 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])
                    == REDIS_ERR) {
                sdsfree(sha);
                return;
            }
        }
        addReplyBulkCBuffer(c,funcname+2,40);
        sdsfree(sha);
        forceCommandPropagation(c,REDIS_PROPAGATE_REPL|REDIS_PROPAGATE_AOF);
    } else if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"kill")) {
        if (server.lua_caller == NULL) {
            addReplySds(c,sdsnew("-NOTBUSY No scripts in execution right now.\r\n"));
        } else if (server.lua_write_dirty) {
            addReplySds(c,sdsnew("-UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.\r\n"));
        } else {
            server.lua_kill = 1;
            addReply(c,shared.ok);
        }
    } else {
        addReplyError(c, "Unknown SCRIPT subcommand or wrong # of args.");
    }
}

參考文獻:
1. Redis 設計與實現,黃健宏著,機械工業出版社
2. redis 3.0.7 版本的源代碼
3. lua manual 5.2

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章