Redis Lua腳本大學教程

前面咱們已經把Redis Lua相關的基礎都介紹過了,若是你能夠編寫一些簡單的Lua腳本,恭喜你已經能夠從Lua中學畢業了。git

在大學課程中,咱們主要學習Lua腳本調試和Redis中Lua執行原理兩部份內容兩部分。github

Lua腳本調試

Redis從3.2版本開始支持Lua腳本調試,調試器的名字叫作LDB。它有一些重要的特性:redis

  • 它使用的是服務器-客戶端模式,因此是遠程調試。Redis服務器就是調試服務器,默認的客戶端是redis-cli。也能夠開發遵循服務器協議的其餘客戶端。
  • 默認狀況下,每一個debugging session都是一個新的session。也就是說在調試的過程當中,服務器不會被阻塞。仍然能夠被其餘客戶端使用或開啓新的session。同時也意味着在調試過程當中全部的修改在結束時都會回滾。
  • 若是須要,能夠把debugging模式調成同步,這樣就能夠保留對數據集的更改。在這種模式下,調試時服務器會處於阻塞狀態。
  • 支持步進式執行
  • 支持靜態和動態斷點
  • 支持從腳本中向調試控制檯打印調試日誌
  • 檢查Lua變量
  • 追蹤Redis命令的執行
  • 很好的支持打印Redis和Lua的值
  • 無限循環和長執行檢測,模擬斷點
Lua腳本調試實戰

在開始調試以前,首先編寫一個簡單的Lua腳本script.lua:數據庫

local src = KEYS[1]
local dst = KEYS[2]
local count = tonumber(ARGV[1])
while count > 0 do
    local item = redis.call('rpop',src)
    if item ~= false then
        redis.call('lpush',dst,item)
    end
    count = count - 1
end
return redis.call('llen',dst)  
複製代碼

這個腳本是把src中的元素依次插入到dst元素的頭部。緩存

有了這個腳本以後咱們就能夠開始調試工做了。bash

咱們可使用redis-cli —eval命令來運行這個腳本,而要調試的話,能夠加上—ldb參數,所以咱們先執行下面的命令:服務器

redis-cli --ldb --eval script.lua foo bar , 10
複製代碼

頁面會出現一些幫助信息,並進入到調試模式session

lua_debug_help

能夠看到幫助頁告訴咱們異步

  • 執行quit能夠退出調試模式
  • 執行restart能夠從新調試
  • 執行help能夠查看更多幫助信息

這裏咱們執行help命令,查看一下幫助信息,打印出不少能夠在調試模式下執行的命令,中括號"[]"內到內容表示命令的簡寫。函數

其中經常使用的有:

  • step/next:執行一行
  • continue:執行到西一個斷點
  • list:展現源碼
  • print:打印一些值
  • break:打斷點

另外在腳本中還可使用redis.breakpoint()添加動態斷點。

下面來簡單演示一下

lua_debug_display

如今我把代碼中count = count - 1這一行刪除,使程序死循環,再來調試一下

lua_debug_dead_loop

能夠看到咱們並無打斷點,可是程序仍然會中止,這是由於執行超時,調試器模擬了一個斷點使程序中止。從源碼中能夠看出,這裏的超時時間是5s。

/* Check if a timeout occurred. */
if (ar->event == LUA_HOOKCOUNT && ldb.step == 0 && bp == 0) {
  mstime_t elapsed = mstime() - server.lua_time_start;
  mstime_t timelimit = server.lua_time_limit ?
    server.lua_time_limit : 5000;
  if (elapsed >= timelimit) {
    timeout = 1;
    ldb.step = 1;
  } else {
    return; /* No timeout, ignore the COUNT event. */
  }
}
複製代碼

因爲Redis默認的debug模式是異步的,因此在調試結束後不會改變redis中的數據。

lua_debug_asyn

固然,你也能夠選擇以同步模式執行,只須要把執行命令中的**—ldb參數改爲--ldb-sync-mode**就能夠了。

解讀EVAL命令

前文咱們已經詳細介紹過EVAL命令了,不瞭解的同窗能夠再回顧一下Redis Lua腳本中學教程(上)。今天咱們結合源碼繼續探究EVAL命令。

在server.c文件中,咱們知道了eval命令執行的是evalCommand函數。這個函數的實如今scripting.c文件中。

函數調用棧是

evalCommand
	(evalGenericCommandWithDebugging)
    evalGenericCommand
      lua_pcall  //Lua函數
複製代碼

evalCommand函數很簡單,只是簡單的判斷是不是調試模式,若是是調試模式,調用evalGenericCommandWithDebugging函數,若是不是,直接調用evalGenericCommand函數。

在evalGenericCommand函數中,先判斷了key的數量是否正確

/* 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;
}
複製代碼

接着查看腳本是否已經在緩存中,若是沒有,計算腳本的SHA1校驗和,若是已經存在,將SHA1校驗和轉換爲小寫

/* 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';
}
複製代碼

這裏funcname變量存儲的是f_ +SHA1校驗和,Redis會將腳本定義爲一個Lua函數,funcname是函數名。函數體是腳本自己。

sds luaCreateFunction(client *c, lua_State *lua, robj *body) {
    char funcname[43];
    dictEntry *de;

    funcname[0] = 'f';
    funcname[1] = '_';
    sha1hex(funcname+2,body->ptr,sdslen(body->ptr));

    sds sha = sdsnewlen(funcname+2,40);
    if ((de = dictFind(server.lua_scripts,sha)) != NULL) {
        sdsfree(sha);
        return dictGetKey(de);
    }

    sds funcdef = sdsempty();
    funcdef = sdscat(funcdef,"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")) {
        if (c != NULL) {
            addReplyErrorFormat(c,
                "Error compiling script (new function): %s\n",
                lua_tostring(lua,-1));
        }
        lua_pop(lua,1);
        sdsfree(sha);
        sdsfree(funcdef);
        return NULL;
    }
    sdsfree(funcdef);

    if (lua_pcall(lua,0,0,0)) {
        if (c != NULL) {
            addReplyErrorFormat(c,"Error running script (new function): %s\n",
                lua_tostring(lua,-1));
        }
        lua_pop(lua,1);
        sdsfree(sha);
        return NULL;
    }

    /* 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,sha,body);
    serverAssertWithInfo(c ? c : server.lua_client,NULL,retval == DICT_OK);
    server.lua_scripts_mem += sdsZmallocSize(sha) + getStringObjectSdsUsedMemory(body);
    incrRefCount(body);
    return sha;
}
複製代碼

在執行腳本以前,還要保存傳入的參數,選擇正確的數據庫。

/* 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);
複製代碼

而後還須要設置鉤子,咱們以前提過的腳本執行超時自動打斷點以及能夠執行SCRPIT KILL命令中止腳本和經過SHUTDOWN命令中止服務器,都是經過鉤子來實現的。

/* 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 && 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;
}
複製代碼

到這裏已經萬事俱備了,就能夠直接調用lua_pcall函數來執行腳本了。執行完以後,還要刪除鉤子並把結果保存到緩衝中。

上面就是腳本執行的整個過程,這個過程以後,Redis還會處理一些腳本同步的問題。這個前文咱們也介紹過了《Redis Lua腳本中學教程(上)

總結

到這裏,Redis Lua腳本系列就所有結束了。文章雖然結束了,可是學習還遠遠沒有結束。你們有問題的話歡迎和我一塊兒探討。共同窗習,共同進步~

對Lua感興趣的同窗能夠讀一下《Programming in Lua》,有條件的儘可能支持正版,想先看看質量的能夠在我公衆號後臺回覆Lua獲取電子書。

相關文章
相關標籤/搜索