前面咱們已經把Redis Lua相關的基礎都介紹過了,若是你能夠編寫一些簡單的Lua腳本,恭喜你已經能夠從Lua中學畢業了。git
在大學課程中,咱們主要學習Lua腳本調試和Redis中Lua執行原理兩部份內容兩部分。github
Redis從3.2版本開始支持Lua腳本調試,調試器的名字叫作LDB。它有一些重要的特性:redis
在開始調試以前,首先編寫一個簡單的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
能夠看到幫助頁告訴咱們異步
這裏咱們執行help命令,查看一下幫助信息,打印出不少能夠在調試模式下執行的命令,中括號"[]"內到內容表示命令的簡寫。函數
其中經常使用的有:
另外在腳本中還可使用redis.breakpoint()
添加動態斷點。
下面來簡單演示一下
如今我把代碼中count = count - 1
這一行刪除,使程序死循環,再來調試一下
能夠看到咱們並無打斷點,可是程序仍然會中止,這是由於執行超時,調試器模擬了一個斷點使程序中止。從源碼中能夠看出,這裏的超時時間是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中的數據。
固然,你也能夠選擇以同步模式執行,只須要把執行命令中的**—ldb參數改爲--ldb-sync-mode**就能夠了。
前文咱們已經詳細介紹過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獲取電子書。