前面我們已經把Redis Lua相關的基礎都介紹過了,如果你可以編寫一些簡單的Lua腳本,恭喜你已經可以從Lua中學畢業了。

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

Lua腳本調試

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

  • 它使用的是伺服器-客戶端模式,所以是遠程調試。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元素的頭部。

有了這個腳本之後我們就可以開始調試工作了。

我們可以使用redis-cli —eval命令來運行這個腳本,而要調試的話,可以加上—ldb參數,因此我們先執行下面的命令:

redis-cli --ldb --eval script.lua foo bar , 10

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

可以看到幫助頁告訴我們

  • 執行quit可以退出調試模式
  • 執行restart可以重新調試
  • 執行help可以查看更多幫助信息

這裡我們執行help命令,查看一下幫助信息,列印出很多可以在調試模式下執行的命令,中括弧"[]"內到內容表示命令的簡寫。

其中常用的有:

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

另外在腳本中還可以使用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命令

前文我們已經詳細介紹過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 cant be greater than number of args");
return;
} else if (numkeys < 0) {
addReplyError(c,"Number of keys cant 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 dont 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] =

相關文章