diff --git a/README.md b/README.md index 4c263c12a..015ea924c 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,11 @@ redis 仓库链接:https://github.com/redis/redis
| [redis-check-rdb.c](https://github.com/CN-annotation-team/redis7.0-chinese-annotated/blob/7.0-cn-annotated/src/redis-check-rdb.c) | Redis RDB 检查工具 | 完成 | | [evict.c](https://github.com/CN-annotation-team/redis7.0-chinese-annotated/blob/7.0-cn-annotated/src/evict.c) | 四种 redis 内存淘汰算法 | 完成 | | [aof.c](https://github.com/CN-annotation-team/redis7.0-chinese-annotated/blob/7.0-cn-annotated/src/aof.c) | redis AOF 功能 | 过半 | +| [redis-check-rdb.c](https://github.com/CN-annotation-team/redis7.0-chinese-annotated/blob/7.0-cn-annotated/src/redis-check-rdb.c) | Redis RDB 检查工具 | 低于一半 | +| [script.c](https://github.com/CN-annotation-team/redis7.0-chinese-annotated/blob/7.0-cn-annotated/src/script.c) | redis功能api的lua方法实现 | 过半 | +| [script_lua.c](https://github.com/CN-annotation-team/redis7.0-chinese-annotated/blob/7.0-cn-annotated/src/script_lua.c) | redis调用lua方法的通用api抽象 | 过半 | +| [eval.c](https://github.com/CN-annotation-team/redis7.0-chinese-annotated/blob/7.0-cn-annotated/src/eval.c) | lua执行实现 | 过半 | +

尚未有中文注释的文件不会出现在表格中。
更新日期:2022/10/23 diff --git a/src/db.c b/src/db.c index 99212bac4..d967438d8 100644 --- a/src/db.c +++ b/src/db.c @@ -1615,6 +1615,10 @@ int keyIsExpired(redisDb *db, robj *key) { * only the first time it is accessed and not in the middle of the * script execution, making propagation to slaves / AOF consistent. * See issue #1525 on Github for more information. */ + + /* Lua űУǼʱ䱻 Lua űʼʱ䡣 + * һkey ֻڵһηʱڣڽűִййڣӶʹ/ AOF Ĵһ¡ + * йظϢ Github ϵĵ1525ڵ issue */ if (server.script_caller) { now = scriptTimeSnapshot(); } diff --git a/src/eval.c b/src/eval.c index e2ee41cb5..16d8c8995 100644 --- a/src/eval.c +++ b/src/eval.c @@ -27,6 +27,12 @@ * POSSIBILITY OF SUCH DAMAGE. */ +/** + * 该文件中涉及到了很多lua的调用接口 + * lua 的接口原理可参照:https://www.runoob.com/manual/lua53doc/manual.html + * 负责对 redis 提供服务的方法 + */ + #include "server.h" #include "sha1.h" #include "rand.h" @@ -59,6 +65,8 @@ static uint64_t dictStrCaseHash(const void *key) { } /* server.lua_scripts sha (as sds string) -> scripts (as luaScript) cache. */ + +/* server.lua_scripts sha(作为sds字符串)-> scripts(作为luaScript)缓存 */ dictType shaScriptObjectDictType = { dictStrCaseHash, /* hash function */ NULL, /* key dup */ @@ -71,13 +79,19 @@ dictType shaScriptObjectDictType = { /* Lua context */ struct luaCtx { + /* lua执行器, 全局唯一 */ lua_State *lua; /* The Lua interpreter. We use just one for all clients */ + /* lua客户端,用于调用redis方法 */ client *lua_client; /* The "fake client" to query Redis from Lua */ + /* sha1和lua脚本的映射 */ dict *lua_scripts; /* A dictionary of SHA1 -> Lua scripts */ + /* 脚本的内存空间 */ unsigned long long lua_scripts_mem; /* Cached scripts' memory + oh */ } lctx; /* Debugger shared state is stored inside this global structure. */ + +/* 调试器共享状态存储在此全局结构中 */ #define LDB_BREAKPOINTS_MAX 64 /* Max number of breakpoints. */ #define LDB_MAX_LEN_DEFAULT 256 /* Default len limit for replies / var dumps. */ struct ldbState { @@ -109,6 +123,10 @@ struct ldbState { * * 'digest' should point to a 41 bytes buffer: 40 for SHA1 converted into an * hexadecimal number, plus 1 byte for null term. */ + +/* 执行输入字符串的 SHA1。我们将其用于散列脚本体以获得 Lua 函数名,并用于 redis.sha1() 的实现。 + * + * “digest” 应该指向一个41字节的缓冲区:40表示 SHA1 转换为十六进制数,加上1字节表示空项 */ void sha1hex(char *digest, char *script, size_t len) { SHA1_CTX ctx; unsigned char hash[20]; @@ -131,6 +149,8 @@ void sha1hex(char *digest, char *script, size_t len) { * Allows to stop execution during a debugging session from within * the Lua code implementation, like if a breakpoint was set in the code * immediately after the function. */ + +/* 允许在调试会话期间从 Lua 代码实现中停止执行,就像在函数之后的代码中设置了断点一样 */ int luaRedisBreakpointCommand(lua_State *lua) { if (ldb.active) { ldb.luabp = 1; @@ -146,6 +166,10 @@ int luaRedisBreakpointCommand(lua_State *lua) { * Log a string message into the output console. * Can take multiple arguments that will be separated by commas. * Nothing is returned to the caller. */ + +/* 将字符串消息记录到输出控制台中。 + * 可以接受多个用逗号分隔的参数。 + * 不会向呼叫者返回任何内容 */ int luaRedisDebugCommand(lua_State *lua) { if (!ldb.active) return 0; int argc = lua_gettop(lua); @@ -165,6 +189,9 @@ int luaRedisDebugCommand(lua_State *lua) { * a write command so far, and returns true. Otherwise if the script * already started to write, returns false and stick to whole scripts * replication, which is our default. */ + +/* 如果脚本到目前为止从未调用过写命令,则启用单命令复制,并返回 true。否则,如果脚本已经开始编写,则返回 false 并坚持整个脚本 + * 现在已经完全通过传播命令来保证lua的正确性,所以全部返回 ture */ int luaRedisReplicateCommandsCommand(lua_State *lua) { lua_pushboolean(lua,1); return 1; @@ -180,9 +207,12 @@ int luaRedisReplicateCommandsCommand(lua_State *lua) { * in order to reset the Lua scripting environment. * * However it is simpler to just call scriptingReset() that does just that. */ + +/* 初始化脚本环境。setup 表示是否为启动时的初始化.说明当前方法是可以多次调用, 同时必须在 scriptingRelease 执行后,因为后者会对相关空间进行释放 */ void scriptingInit(int setup) { lua_State *lua = lua_open(); + /* 如果首次调用,则初始化lctx上下文、server全局数据、luaDebug信息 */ if (setup) { lctx.lua_client = NULL; server.script_caller = NULL; @@ -202,11 +232,15 @@ void scriptingInit(int setup) { lua_getglobal(lua,"redis"); /* redis.breakpoint */ + + /* redis.breakpoint() 在 debug 模式下,主动进入断点模式 */ lua_pushstring(lua,"breakpoint"); lua_pushcfunction(lua,luaRedisBreakpointCommand); lua_settable(lua,-3); /* redis.debug */ + + /* redis.debug(a,b) 在 debug 模式下,打印相关信息 */ lua_pushstring(lua,"debug"); lua_pushcfunction(lua,luaRedisDebugCommand); lua_settable(lua,-3); @@ -222,6 +256,9 @@ void scriptingInit(int setup) { * 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. */ + + /* 添加一个用于pcall错误报告的帮助函数。 + * 请注意,当错误发生在C函数中时,我们希望报告有关调用者的信息,从用户调试脚本的角度来看,这是有意义的 */ { char *errh_func = "local dbg = debug\n" "debug = nil\n" @@ -239,7 +276,9 @@ void scriptingInit(int setup) { " end" " return err\n" "end\n"; + /* 将代码压入栈中 */ luaL_loadbuffer(lua,errh_func,strlen(errh_func),"@err_handler_def"); + /* 尝试运行一下代码, 使其生效 */ lua_pcall(lua,0,0,0); } @@ -247,6 +286,9 @@ void scriptingInit(int setup) { * inside the Lua interpreter. * Note: there is no need to create it again when this function is called * by scriptingReset(). */ + + /* 创建(未连接的)客户端,我们使用该客户端在Lua解释器中执行Redis命令。 + * 注意:当通过scriptingReset() 调用此函数时,无需再次创建它 */ if (lctx.lua_client == NULL) { lctx.lua_client = createClient(NULL); lctx.lua_client->flags |= CLIENT_SCRIPT; @@ -256,9 +298,13 @@ void scriptingInit(int setup) { } /* Lock the global table from any changes */ + + /* 设置全局变量查询错误异常 */ lua_pushvalue(lua, LUA_GLOBALSINDEX); luaSetErrorMetatable(lua); /* Recursively lock all tables that can be reached from the global table */ + + /* 递归锁定可从全局表访问的所有表, 将其都设置为只读状态 */ luaSetTableProtectionRecursively(lua); lua_pop(lua, 1); @@ -285,6 +331,7 @@ void scriptingReset(int async) { * EVAL and SCRIPT commands implementation * ------------------------------------------------------------------------- */ +/* 计算当前的方法名称,即 f_{sha1 code} */ static void evalCalcFunctionName(int evalsha, sds script, char *out_funcname) { /* We obtain the script SHA1, then check if this function is already * defined into the Lua state */ @@ -313,9 +360,18 @@ static void evalCalcFunctionName(int evalsha, sds script, char *out_funcname) { * The err arg is optional, can be used to get a detailed error string. * The out_shebang_len arg is optional, can be used to trim the shebang from the script. * Returns C_OK on success, and C_ERR on error. */ + +/* Helper 函数尝试从脚本正文中提取 shebang 标志。 + * 如果未找到 shebang,则返回成功和 COMPAT 模式标志。 + * err 参数是可选的,可用于获取详细的错误字符串。 + * out_shebang_len 参数是可选的,可用于从脚本中删除 shebang + * 成功时返回 C_OK ,错误时返回 C_ERR + * Shebang 的名字来自于 SHArp 和 bang,或 haSH bang 的缩写,指代 Shebang 中 #! 两个符号的典型 Unix 名称 + * 参考:https://redis.io/docs/manual/programmability/eval-intro/ */ int evalExtractShebangFlags(sds body, uint64_t *out_flags, ssize_t *out_shebang_len, sds *err) { ssize_t shebang_len = 0; uint64_t script_flags = SCRIPT_FLAG_EVAL_COMPAT_MODE; + /* 是否以 #!开头 */ if (!strncmp(body, "#!", 2)) { int numparts,j; char *shebang_end = strchr(body, '\n'); @@ -326,6 +382,7 @@ int evalExtractShebangFlags(sds body, uint64_t *out_flags, ssize_t *out_shebang_ } shebang_len = shebang_end - body; sds shebang = sdsnewlen(body, shebang_len); + /* 以空格为分隔符, 分割为多块 */ sds *parts = sdssplitargs(shebang, &numparts); sdsfree(shebang); if (!parts || numparts == 0) { @@ -341,6 +398,7 @@ int evalExtractShebangFlags(sds body, uint64_t *out_flags, ssize_t *out_shebang_ sdsfreesplitres(parts, numparts); return C_ERR; } + /* 移除特殊的兼容标记 */ script_flags &= ~SCRIPT_FLAG_EVAL_COMPAT_MODE; for (j = 1; j < numparts; j++) { if (!strncmp(parts[j], "flags=", 6)) { @@ -380,6 +438,9 @@ int evalExtractShebangFlags(sds body, uint64_t *out_flags, ssize_t *out_shebang_ /* Try to extract command flags if we can, returns the modified flags. * Note that it does not guarantee the command arguments are right. */ + +/* 如果可以,尝试提取命令标志,返回修改后的标志。请注意,它不能保证命令参数是正确的 + * 主要用于在 processCommand 中判断执行条件 */ uint64_t evalGetCommandFlags(client *c, uint64_t cmd_flags) { char funcname[43]; int evalsha = c->cmd->proc == evalShaCommand || c->cmd->proc == evalShaRoCommand; @@ -392,6 +453,7 @@ uint64_t evalGetCommandFlags(client *c, uint64_t cmd_flags) { if (!de) { if (evalsha) return cmd_flags; + /* 解析当前脚本的shebang属性 */ if (evalExtractShebangFlags(c->argv[1]->ptr, &script_flags, NULL, NULL) == C_ERR) return cmd_flags; } else { @@ -420,6 +482,14 @@ uint64_t evalGetCommandFlags(client *c, uint64_t cmd_flags) { * * If 'c' is not NULL, on error the client is informed with an appropriate * error describing the nature of the problem and the Lua interpreter error. */ + +/* 使用指定的主体定义Lua函数。函数名称将以以下形式生成: + * f_<十六进制sha1和> + * 该函数增加 “body” 对象的引用计数,作为成功调用的副作用。 + * 成功时,将返回一个指向 SDS 字符串的指针,该字符串表示刚刚添加的函数的函数 SHA1(在下一次调用 scriptingReset 函数之前有效),否则返回 NULL + * 该函数处理被已经存在的脚本调用的事实,在这种情况下,它的行为与成功的情况类似。 + * 如果 “c” 不为 NULL,则在出现错误时,会通知客户端一个适当的错误,描述问题的性质和 Lua 解释器错误 + * 方法解析了 shebang,然后将 sha1 值和 script body、script flag 绑定 */ sds luaCreateFunction(client *c, robj *body) { char funcname[43]; dictEntry *de; @@ -442,6 +512,8 @@ sds luaCreateFunction(client *c, robj *body) { } /* Note that in case of a shebang line we skip it but keep the line feed to conserve the user's line numbers */ + + /* 请注意,对于shebang行,我们跳过它,但保留换行以保留用户的行号 */ if (luaL_loadbuffer(lctx.lua,(char*)body->ptr + shebang_len,sdslen(body->ptr) - shebang_len,"@user_script")) { if (c != NULL) { addReplyErrorFormat(c, @@ -454,6 +526,7 @@ sds luaCreateFunction(client *c, robj *body) { serverAssert(lua_isfunction(lctx.lua, -1)); + /* 设置 table 的字段信息 */ lua_setfield(lctx.lua, LUA_REGISTRYINDEX, funcname); /* We also save a SHA1 -> Original script map in a dictionary @@ -486,6 +559,7 @@ void resetLuaClient(void) { lctx.lua_client->flags &= ~CLIENT_MULTI; } +/* 统一的 lua 执行方法, evalsha 表示算法为 sha1 执行方式 */ void evalGenericCommand(client *c, int evalsha) { lua_State *lua = lctx.lua; char funcname[43]; @@ -502,12 +576,17 @@ void evalGenericCommand(client *c, int evalsha) { return; } + /* 通过lua脚本获取到对应的sha1作为funcname */ evalCalcFunctionName(evalsha, c->argv[1]->ptr, funcname); /* Push the pcall error handler function on the stack. */ + + /* 将错误处理方法加载到栈中,用于在调用方法错误时回调 */ lua_getglobal(lua, "__redis__err__handler"); /* Try to lookup the Lua function */ + + /* 加载对应的方法 */ lua_getfield(lua, LUA_REGISTRYINDEX, funcname); if (lua_isnil(lua,-1)) { lua_pop(lua,1); /* remove the nil from the stack */ @@ -535,19 +614,24 @@ void evalGenericCommand(client *c, int evalsha) { luaScript *l = dictGetVal(de); int ro = c->cmd->proc == evalRoCommand || c->cmd->proc == evalShaRoCommand; + /* 运行时上下文 */ scriptRunCtx rctx; + /* 准备上下文信息 */ if (scriptPrepareForRun(&rctx, lctx.lua_client, c, lua_cur_script, l->flags, ro) != C_OK) { lua_pop(lua,2); /* Remove the function and error handler. */ return; } + /* 将当前运行标记为 EVAL(与 FCALL 相反),这样我们将获得适当的错误消息和日志 */ rctx.flags |= SCRIPT_EVAL_MODE; /* mark the current run as EVAL (as opposed to FCALL) so we'll get appropriate error messages and logs */ + /* 整体 debug 模式都在下面的方法中通过 hook 实现 */ luaCallFunction(&rctx, lua, c->argv+3, numkeys, c->argv+3+numkeys, c->argc-3-numkeys, ldb.active); lua_pop(lua,1); /* Remove the error handler. */ scriptResetRun(&rctx); } +/* lua 脚本执行入口函数 */ void evalCommand(client *c) { /* Explicitly feed monitor here so that lua commands appear after their * script command. */ @@ -558,19 +642,26 @@ void evalCommand(client *c) { evalGenericCommandWithDebugging(c,0); } +/* lua脚本只读方式(不进行计数)执行入口函数 */ void evalRoCommand(client *c) { evalCommand(c); } +/* lua 脚本 sha 执行入口函数 */ void evalShaCommand(client *c) { /* Explicitly feed monitor here so that lua commands appear after their * script command. */ + + /* 在此处显式馈送监视器,以便 lua 命令出现在其脚本命令之后 */ replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc); if (sdslen(c->argv[1]->ptr) != 40) { /* We know that a match is not possible if the provided SHA is * not the right length. So we return an error ASAP, this way * evalGenericCommand() can be implemented without string length * sanity check */ + + /* 我们知道,如果提供的 SHA 长度不正确,则不可能匹配。 + * 因此,我们会尽快返回一个错误,这样 evalGenericCommand() 就可以在没有字符串长度健全性检查的情况下实现 */ addReplyErrorObject(c, shared.noscripterr); return; } @@ -586,6 +677,7 @@ void evalShaRoCommand(client *c) { evalShaCommand(c); } +/* script 命令处理,包括 DEBUG 开关、 EXISTS 判断 sha 值、 FLUSH 刷新缓存、KILL 杀死脚本、LOAD 加载脚本 */ void scriptCommand(client *c) { if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) { const char *help[] = { @@ -679,6 +771,8 @@ unsigned long evalScriptsMemory() { * ------------------------------------------------------------------------- */ /* Initialize Lua debugger data structures. */ + +/* 初始化 Lua 调试器数据结构 */ void ldbInit(void) { ldb.conn = NULL; ldb.active = 0; @@ -691,6 +785,8 @@ void ldbInit(void) { } /* Remove all the pending messages in the specified list. */ + +/* 删除指定列表中的所有待处理的消息 */ void ldbFlushLog(list *log) { listNode *ln; @@ -703,6 +799,8 @@ int ldbIsEnabled(){ } /* Enable debug mode of Lua scripts for this client. */ + +/* 为此客户端启用 Lua 脚本的调试模式, 通过 script debug yes/sync 启动, 标记当前 client 为 deubg 模式 */ void ldbEnable(client *c) { c->flags |= CLIENT_LUA_DEBUG; ldbFlushLog(ldb.logs); @@ -719,11 +817,15 @@ void ldbEnable(client *c) { /* Exit debugging mode from the POV of client. This function is not enough * to properly shut down a client debugging session, see ldbEndSession() * for more information. */ + +/* 从客户端的 POV 退出调试模式。此函数不足以正确关闭客户端调试会话,有关详细信息,请参阅 ldbEndSession() */ void ldbDisable(client *c) { c->flags &= ~(CLIENT_LUA_DEBUG|CLIENT_LUA_DEBUG_SYNC); } /* Append a log entry to the specified LDB log. */ + +/* 将日志条目附加到指定的 LDB 日志 */ void ldbLog(sds entry) { listAddNodeTail(ldb.logs,entry); } @@ -732,6 +834,8 @@ void ldbLog(sds entry) { * ldb.maxlen. The first time the limit is reached a hint is generated * to inform the user that reply trimming can be disabled using the * debugger "maxlen" command. */ + +/* ldbLog() 的一个版本,用于防止生成大于 ldb.maxlen 的日志。第一次达到限制时,会生成一个提示,通知用户可以使用调试器 maxlen 命令禁用回复修剪 */ void ldbLogWithMaxLen(sds entry) { int trimmed = 0; if (ldb.maxlen && sdslen(entry) > ldb.maxlen) { @@ -750,6 +854,8 @@ void ldbLogWithMaxLen(sds entry) { /* Send ldb.logs to the debugging client as a multi-bulk reply * consisting of simple strings. Log entries which include newlines have them * replaced with spaces. The entries sent are also consumed. */ + +/* 发送 ldb.logs 作为由简单字符串组成的多批量回复记录到调试客户端。包含换行符的日志条目将用空格替换。发送的条目也会被消耗 */ void ldbSendLogs(void) { sds proto = sdsempty(); proto = sdscatfmt(proto,"*%i\r\n", (int)listLength(ldb.logs)); @@ -765,6 +871,8 @@ void ldbSendLogs(void) { /* Avoid warning. We don't check the return value of write() * since the next read() will catch the I/O error and will * close the debugging session. */ + + /* 避免警告。我们不检查 write() 的返回值,因为下一次 read() 将捕获 I/O 错误并关闭调试会话 */ } sdsfree(proto); } @@ -781,7 +889,15 @@ void ldbSendLogs(void) { * * The caller should call ldbEndSession() only if ldbStartSession() * returned 1. */ + +/* 在调用 EVAL 实现之前启动调试会话。 + * 我们使用的技术是捕获客户端 socket 描述符,以便从 Lua 挂钩中对其执行直接 I/O。这样,我们就不必为了处理 I/O 而重新输入 Redis。 + * + * 如果调用方继续调用 EVAL,则函数返回1;如果调用方中止操作,则返回0(这发生在分叉会话中的父级,因为由子级继续,或者分叉返回错误时)。 + * + * 仅当ldbStartSession() 返回1时,调用方才应调用 ldbEndSession */ int ldbStartSession(client *c) { + /* 确定当前是否需要 fork 一个子进程来运行 */ ldb.forked = (c->flags & CLIENT_LUA_DEBUG_SYNC) == 0; if (ldb.forked) { pid_t cp = redisFork(CHILD_TYPE_LDB); @@ -790,6 +906,8 @@ int ldbStartSession(client *c) { return 0; } else if (cp == 0) { /* Child. Let's ignore important signals handled by the parent. */ + + /* 子进程需要忽略父母处理的重要信号 */ struct sigaction act; sigemptyset(&act.sa_mask); act.sa_flags = 0; @@ -800,9 +918,13 @@ int ldbStartSession(client *c) { /* Log the creation of the child and close the listening * socket to make sure if the parent crashes a reset is sent * to the clients. */ + + /* 记录子级的创建并关闭侦听套接字,以确保如果父级崩溃,则向客户端发送重置 */ serverLog(LL_WARNING,"Redis forked for debugging eval"); } else { /* Parent */ + + /* 父进程将当前的子进程编号写入 list 后退出,交给子进程来通信 */ listAddNodeTail(ldb.children,(void*)(unsigned long)cp); freeClientAsync(c); /* Close the client in the parent side. */ return 0; @@ -813,12 +935,16 @@ int ldbStartSession(client *c) { } /* Setup our debugging session. */ + + /* 设置调试会话,为阻塞模式,因为要么同步,要么子进程模式,再后续会重新设置为非阻塞模式 */ connBlock(ldb.conn); connSendTimeout(ldb.conn,5000); ldb.active = 1; /* First argument of EVAL is the script itself. We split it into different * lines since this is the way the debugger accesses the source code. */ + + /* EVAL 的第一个参数是脚本本身。我们将它分成不同的行,因为这是调试器访问源代码的方式 */ sds srcstring = sdsdup(c->argv[1]->ptr); size_t srclen = sdslen(srcstring); while(srclen && (srcstring[srclen-1] == '\n' || @@ -834,12 +960,18 @@ int ldbStartSession(client *c) { /* End a debugging session after the EVAL call with debugging enabled * returned. */ + +/* 在返回启用调试的EVAL调用后结束调试会话 */ void ldbEndSession(client *c) { /* Emit the remaining logs and an mark. */ + + /* 发出剩余日志和 <endsession> 标记 */ ldbLog(sdsnew("")); ldbSendLogs(); /* If it's a fork()ed session, we just exit. */ + + /* 如果是 fork() 会话,我们就退出 */ if (ldb.forked) { writeToClient(c,0); serverLog(LL_WARNING,"Lua debugging session child exiting"); @@ -850,11 +982,16 @@ void ldbEndSession(client *c) { } /* Otherwise let's restore client's state. */ + + /* 否则,让我们恢复客户端的状态 + * 设置连接相关状态 */ connNonBlock(ldb.conn); connSendTimeout(ldb.conn,0); /* Close the client connection after sending the final EVAL reply * in order to signal the end of the debugging session. */ + + /* 在发送最终 EVAL 回复后关闭客户端连接,以发出调试会话结束的信号 */ c->flags |= CLIENT_CLOSE_AFTER_REPLY; /* Cleanup. */ @@ -866,6 +1003,8 @@ void ldbEndSession(client *c) { /* If the specified pid is among the list of children spawned for * forked debugging sessions, it is removed from the children list. * If the pid was found non-zero is returned. */ + +/* 如果指定的 pid 在为分叉调试会话生成的子级列表中,则会从子级列表中将其删除。如果找到 pid,则返回非零 */ int ldbRemoveChild(pid_t pid) { listNode *ln = listSearchKey(ldb.children,(void*)(unsigned long)pid); if (ln) { @@ -877,6 +1016,8 @@ int ldbRemoveChild(pid_t pid) { /* Return the number of children we still did not receive termination * acknowledge via wait() in the parent process. */ + +/* 返回父进程中仍然没有通过 wait() 收到终止确认的子进程数 */ int ldbPendingChildren(void) { return listLength(ldb.children); } @@ -898,7 +1039,10 @@ void ldbKillForkedSessions(void) { /* Wrapper for EVAL / EVALSHA that enables debugging, and makes sure * that when EVAL returns, whatever happened, the session is ended. */ + +/* EVAL/EVALSHA 的包装器,启用调试,并确保当 EVAL 返回时,无论发生了什么,会话都结束, 如果当前 client 为 lua_debug 模式,则会进入当前逻辑 */ void evalGenericCommandWithDebugging(client *c, int evalsha) { + /* 返回0,说明不需要保持会话,要么就是异步父进程,或者是子进程创建失败 */ if (ldbStartSession(c)) { evalGenericCommand(c,evalsha); ldbEndSession(c); @@ -909,6 +1053,8 @@ void evalGenericCommandWithDebugging(client *c, int evalsha) { /* Return a pointer to ldb.src source code line, considering line to be * one-based, and returning a special string for out of range lines. */ + +/* 返回指向 ldb 的指针。src 源代码行,将该行视为一行,并为超出范围的行返回一个特殊字符串 */ char *ldbGetSourceLine(int line) { int idx = line-1; if (idx < 0 || idx >= ldb.lines) return ""; @@ -927,6 +1073,9 @@ int ldbIsBreakpoint(int line) { /* Add the specified breakpoint. Ignore it if we already reached the max. * Returns 1 if the breakpoint was added (or was already set). 0 if there is * no space for the breakpoint or if the line is invalid. */ + +/* 添加指定的断点。如果我们已经达到最大值,请忽略它。 + * 如果断点已添加(或已设置),则返回1。如果断点没有空格或该行无效,则为0 */ int ldbAddBreakpoint(int line) { if (line <= 0 || line > ldb.lines) return 0; if (!ldbIsBreakpoint(line) && ldb.bpcount != LDB_BREAKPOINTS_MAX) { @@ -938,6 +1087,8 @@ int ldbAddBreakpoint(int line) { /* Remove the specified breakpoint, returning 1 if the operation was * performed or 0 if there was no such breakpoint. */ + +/* 删除指定的断点,如果执行了操作,则返回1;如果没有此类断点,则返回0 */ int ldbDelBreakpoint(int line) { int j; @@ -954,6 +1105,9 @@ int ldbDelBreakpoint(int line) { /* Expect a valid multi-bulk command in the debugging client query buffer. * On success the command is parsed and returned as an array of SDS strings, * otherwise NULL is returned and there is to read more buffer. */ + +/* 调试客户端查询缓冲区中应包含有效的多批量命令。 + * 成功后,命令被解析并作为 SDS 字符串数组返回,否则返回 NULL,需要读取更多缓冲区*/ sds *ldbReplParseCommand(int *argcp, char** err) { static char* protocol_error = "protocol error"; sds *argv = NULL; @@ -962,6 +1116,8 @@ sds *ldbReplParseCommand(int *argcp, char** err) { /* Working on a copy is simpler in this case. We can modify it freely * for the sake of simpler parsing. */ + + /* 在这种情况下,处理副本更简单。为了更简单的解析,我们可以自由地修改它 */ sds copy = sdsdup(ldb.cbuf); char *p = copy; @@ -969,6 +1125,8 @@ sds *ldbReplParseCommand(int *argcp, char** err) { * works in this context. It is also very forgiving regarding broken * protocol. */ + /* 这个 Redis 协议解析器是一个笑话……在这个上下文中工作的最简单的东西。对于违反协议,这也是非常宽容的 */ + /* Seek and parse *\r\n. */ p = strchr(p,'*'); if (!p) goto protoerr; char *plen = p+1; /* Multi bulk len pointer. */ @@ -1008,6 +1166,8 @@ sds *ldbReplParseCommand(int *argcp, char** err) { } /* Log the specified line in the Lua debugger output. */ + +/* 在 Lua 调试器输出中记录指定的行 */ void ldbLogSourceLine(int lnum) { char *line = ldbGetSourceLine(lnum); char *prefix; @@ -1031,6 +1191,9 @@ void ldbLogSourceLine(int lnum) { * around the specified line is shown. When a line number is specified * the amount of context (lines before/after) is specified via the * 'context' argument. */ + +/* 实现 Lua 调试器的 “list” 命令。如果 around 为0,则会列出整个文件,否则只显示指定行周围文件的一小部分。 + * 当指定行号时,通过 “context” 参数指定上下文量(前后行)*/ void ldbList(int around, int context) { int j; @@ -1047,6 +1210,12 @@ void ldbList(int around, int context) { * * The element is not automatically removed from the stack, nor it is * converted to a different type. */ + +/* 将 “Lua” 状态堆栈上位置 “idx” 处的 Lua 值的可读表示形式附加到作为参数传递的 SDS 字符串。 + * 返回附加了表示值的新 SDS 字符串。 + * 用于实现 ldbLogStackValue() + * + * 元素不会自动从堆栈中移除,也不会转换为其他类型 */ #define LDB_MAX_VALUES_DEPTH (LUA_MINSTACK/2) sds ldbCatStackValueRec(sds s, lua_State *lua, int idx, int level) { int t = lua_type(lua,idx); @@ -1078,6 +1247,8 @@ sds ldbCatStackValueRec(sds s, lua_State *lua, int idx, int level) { /* Note: we create two representations at the same time, one * assuming the table is an array, one assuming it is not. At the * end we know what is true and select the right one. */ + + /* 注意:我们同时创建了两个表示,一个假设表是数组,另一个假设它不是数组。最后,我们知道什么是正确的,并选择正确的 */ sds repr1 = sdsempty(); sds repr2 = sdsempty(); lua_pushnil(lua); /* The first key to start the iteration is nil. */ @@ -1133,6 +1304,8 @@ sds ldbCatStackValueRec(sds s, lua_State *lua, int idx, int level) { /* Higher level wrapper for ldbCatStackValueRec() that just uses an initial * recursion level of '0'. */ + +/* ldbCatStackValueRec() 的高级包装,它只使用初始递归级别 “0” */ sds ldbCatStackValue(sds s, lua_State *lua, int idx) { return ldbCatStackValueRec(s,lua,idx,0); } @@ -1140,6 +1313,9 @@ sds ldbCatStackValue(sds s, lua_State *lua, int idx) { /* Produce a debugger log entry representing the value of the Lua object * currently on the top of the stack. The element is not popped nor modified. * Check ldbCatStackValue() for the actual implementation. */ + +/* 生成一个调试器日志条目,表示当前位于堆栈顶部的Lua对象的值。不会弹出或修改元素。 + * 检查 ldbCatStackValue() 以了解实际实现 */ void ldbLogStackValue(lua_State *lua, char *prefix) { sds s = sdsnew(prefix); s = ldbCatStackValue(s,lua,-1); @@ -1161,6 +1337,10 @@ char *ldbRedisProtocolToHuman_Double(sds *o, char *reply); * * Note that the SDS string is passed by reference (pointer of pointer to * char*) so that we can return a modified pointer, as for SDS semantics. */ + +/* 从 “reply” 中获取 Redis 协议,并将其以人类可读的形式附加到传递的 SDS 字符串 “o” 中。 + * + * 请注意, SDS 字符串是通过引用(指向 char* 的指针)传递的,因此我们可以返回一个修改过的指针,就像 SDS 语义一样*/ char *ldbRedisProtocolToHuman(sds *o, char *reply) { char *p = reply; switch(*p) { @@ -1181,6 +1361,7 @@ char *ldbRedisProtocolToHuman(sds *o, char *reply) { /* The following functions are helpers for ldbRedisProtocolToHuman(), each * take care of a given Redis return type. */ +/* 以下函数是 ldbRedisProtocolToHuman() 的助手,每个函数负责给定的 Redis 返回类型 */ char *ldbRedisProtocolToHuman_Int(sds *o, char *reply) { char *p = strchr(reply+1,'\r'); *o = sdscatlen(*o,reply+1,p-reply-1); @@ -1287,6 +1468,9 @@ char *ldbRedisProtocolToHuman_Double(sds *o, char *reply) { /* Log a Redis reply as debugger output, in a human readable format. * If the resulting string is longer than 'len' plus a few more chars * used as prefix, it gets truncated. */ + +/* 以人类可读的格式将 Redis 回复记录为调试器输出。 + * 如果生成的字符串比 “len” 长,再加上几个用作前缀的字符,它将被截断 */ void ldbLogRedisReply(char *reply) { sds log = sdsnew(" "); ldbRedisProtocolToHuman(&log,reply); @@ -1296,6 +1480,8 @@ void ldbLogRedisReply(char *reply) { /* Implements the "print " command of the Lua debugger. It scans for Lua * var "varname" starting from the current stack frame up to the top stack * frame. The first matching variable is printed. */ + +/* 实现 Lua 调试器的 “print<var>” 命令。它从当前堆栈帧开始扫描 Lua-var“varname” ,直到堆栈顶部帧。打印第一个匹配变量 */ void ldbPrint(lua_State *lua, char *varname) { lua_Debug ar; @@ -1328,6 +1514,9 @@ void ldbPrint(lua_State *lua, char *varname) { /* Implements the "print" command (without arguments) of the Lua debugger. * Prints all the variables in the current stack frame. */ + +/* 实现Lua调试器的 “print” 命令(无参数)。 + * 打印当前堆栈帧中的所有变量 */ void ldbPrintAll(lua_State *lua) { lua_Debug ar; int vars = 0; @@ -1353,6 +1542,8 @@ void ldbPrintAll(lua_State *lua) { } /* Implements the break command to list, add and remove breakpoints. */ + +/* 实现 break 命令以列出、添加和删除断点 */ void ldbBreak(sds *argv, int argc) { if (argc == 1) { if (ldb.bpcount == 0) { @@ -1397,12 +1588,18 @@ void ldbBreak(sds *argv, int argc) { /* Implements the Lua debugger "eval" command. It just compiles the user * passed fragment of code and executes it, showing the result left on * the stack. */ + +/* 实现 Lua 调试器 “eval” 命令。它只编译用户传递的代码片段并执行它,显示堆栈上留下的结果 */ void ldbEval(lua_State *lua, sds *argv, int argc) { /* Glue the script together if it is composed of multiple arguments. */ + + /* 如果脚本由多个参数组成,请将其粘合在一起 */ sds code = sdsjoinsds(argv+1,argc-1," ",1); sds expr = sdscatsds(sdsnew("return "),code); /* Try to compile it as an expression, prepending "return ". */ + + /* 尝试将其编译为表达式,并在 “return” 之前 */ if (luaL_loadbuffer(lua,expr,sdslen(expr),"@ldb_eval")) { lua_pop(lua,1); /* Failed? Try as a statement. */ @@ -1431,6 +1628,9 @@ void ldbEval(lua_State *lua, sds *argv, int argc) { * the implementation very simple: we just call the Lua redis.call() command * implementation, with ldb.step enabled, so as a side effect the Redis command * and its reply are logged. */ + +/* 执行调试器 “redis” 命令。为了使实现变得非常简单,我们使用了一个技巧: + * 我们只需调用 Lua redis.call() 命令实现,使用 ldb.step 已启用,因此作为副作用,Redis 命令及其回复将被记录 */ void ldbRedis(lua_State *lua, sds *argv, int argc) { int j; @@ -1458,6 +1658,8 @@ void ldbRedis(lua_State *lua, sds *argv, int argc) { /* Implements "trace" command of the Lua debugger. It just prints a backtrace * querying Lua starting from the current callframe back to the outer one. */ + +/* 实现 Lua 调试器的 “trace” 命令。它只打印从当前调用帧开始到外部调用帧的回溯查询 Lua */ void ldbTrace(lua_State *lua) { lua_Debug ar; int level = 0; @@ -1479,6 +1681,8 @@ void ldbTrace(lua_State *lua) { /* Implements the debugger "maxlen" command. It just queries or sets the * ldb.maxlen variable. */ + +/* 实现调试器 “maxlen” 命令。它只是查询或设置 ldb.maxlen 变量 */ void ldbMaxlen(sds *argv, int argc) { if (argc == 2) { int newval = atoi(argv[1]); @@ -1496,6 +1700,9 @@ void ldbMaxlen(sds *argv, int argc) { /* Read debugging commands from client. * Return C_OK if the debugging session is continuing, otherwise * C_ERR if the client closed the connection or is timing out. */ + +/* 从客户端读取调试命令。 + * 如果调试会话正在继续,则返回 C_OK ;否则,如果客户端关闭连接或超时,返回 C_ERR */ int ldbRepl(lua_State *lua) { sds *argv; int argc; @@ -1503,6 +1710,8 @@ int ldbRepl(lua_State *lua) { /* We continue processing commands until a command that should return * to the Lua interpreter is found. */ + + /* 我们继续处理命令,直到找到应该返回到 Lua 解释器的命令 */ while(1) { while((argv = ldbReplParseCommand(&argc, &err)) == NULL) { char buf[1024]; @@ -1627,19 +1836,26 @@ ldbLog(sdsnew(" next line of code.")); /* This is the core of our Lua debugger, called each time Lua is about * to start executing a new line. */ + +/* 这是我们的 Lua 调试器的核心,每次 Lua 即将开始执行新行时都会调用它,主要通过 lua 的 hook 功能来调用 */ void luaLdbLineHook(lua_State *lua, lua_Debug *ar) { scriptRunCtx* rctx = luaGetFromRegistry(lua, REGISTRY_RUN_CTX_NAME); lua_getstack(lua,0,ar); lua_getinfo(lua,"Sl",ar); ldb.currentline = ar->currentline; + /* 判断当前代码行是否为断点 */ int bp = ldbIsBreakpoint(ldb.currentline) || ldb.luabp; int timeout = 0; /* Events outside our script are not interesting. */ + + /* 我们脚本之外的事件并不有趣, 参考 @user_script */ if(strstr(ar->short_src,"user_script") == NULL) return; /* Check if a timeout occurred. */ + + /* 检测当前是否超过了 server 设定的最大耗时 */ if (ar->event == LUA_HOOKCOUNT && ldb.step == 0 && bp == 0) { mstime_t elapsed = elapsedMs(rctx->start_time); mstime_t timelimit = server.busy_reply_threshold ? @@ -1659,18 +1875,26 @@ void luaLdbLineHook(lua_State *lua, lua_Debug *ar) { else if (timeout) reason = "timeout reached, infinite loop?"; ldb.step = 0; ldb.luabp = 0; + + /* 输出 stop 日志和当前的代码行信息 */ ldbLog(sdscatprintf(sdsempty(), "* Stopped at %d, stop reason = %s", ldb.currentline, reason)); ldbLogSourceLine(ldb.currentline); ldbSendLogs(); + + /* 读取客户端的 debug 命令 */ if (ldbRepl(lua) == C_ERR && timeout) { /* If the client closed the connection and we have a timeout * connection, let's kill the script otherwise the process * will remain blocked indefinitely. */ + + /* 如果客户端关闭了连接,我们有一个超时连接,让我们终止脚本,否则进程将无限期地被阻塞 */ luaPushError(lua, "timeout during Lua debugging with client closing connection"); luaError(lua); } + + /* 重置当前的上下文时间 */ rctx->start_time = getMonotonicUs(); rctx->snapshot_time = mstime(); } diff --git a/src/script.c b/src/script.c index a6d2e1a0c..34564561f 100644 --- a/src/script.c +++ b/src/script.c @@ -31,6 +31,7 @@ #include "script.h" #include "cluster.h" +/* 支持的标记 */ scriptFlag scripts_flags_def[] = { {.flag = SCRIPT_FLAG_NO_WRITES, .str = "no-writes"}, {.flag = SCRIPT_FLAG_ALLOW_OOM, .str = "allow-oom"}, @@ -43,6 +44,7 @@ scriptFlag scripts_flags_def[] = { /* On script invocation, holding the current run context */ static scriptRunCtx *curr_run_ctx = NULL; +/* 移除脚本的超时模式 */ static void exitScriptTimedoutMode(scriptRunCtx *run_ctx) { serverAssert(run_ctx == curr_run_ctx); serverAssert(scriptIsTimedout()); @@ -52,6 +54,7 @@ static void exitScriptTimedoutMode(scriptRunCtx *run_ctx) { if (server.masterhost && server.master) queueClientForReprocessing(server.master); } +/* 确认脚本进入了超时模式 */ static void enterScriptTimedoutMode(scriptRunCtx *run_ctx) { serverAssert(run_ctx == curr_run_ctx); serverAssert(!scriptIsTimedout()); @@ -77,10 +80,14 @@ client* scriptGetCaller() { /* interrupt function for scripts, should be call * from time to time to reply some special command (like ping) * and also check if the run should be terminated. */ + +/* 脚本的中断函数,应不时调用以回复某些特殊命令(如ping),并检查运行是否应终止 */ int scriptInterrupt(scriptRunCtx *run_ctx) { if (run_ctx->flags & SCRIPT_TIMEDOUT) { /* script already timedout we just need to precess some events and return */ + + /* 脚本已经超时,我们只需要处理一些事件并返回 */ processEventsWhileBlocked(); return (run_ctx->flags & SCRIPT_KILLED) ? SCRIPT_KILL : SCRIPT_CONTINUE; } @@ -101,13 +108,18 @@ int scriptInterrupt(scriptRunCtx *run_ctx) { * we need to mask the client executing the script from the event loop. * If we don't do that the client may disconnect and could no longer be * here when the EVAL command will return. */ + + /* 一旦脚本超时,我们就重新进入事件循环以允许其他人执行一些命令。因此,我们需要从事件循环中屏蔽执行脚本的客户端。 + * 如果我们不这样做,当 EVAL 命令返回时,客户端可能会断开连接,并且不再在这里*/ protectClient(run_ctx->original_client); + /* 尝试去处理 eventLoop */ processEventsWhileBlocked(); return (run_ctx->flags & SCRIPT_KILLED) ? SCRIPT_KILL : SCRIPT_CONTINUE; } +/* 把脚本的 flag 转换为 cmd 的 flag,包括类似禁止 OOM,可写命令 */ uint64_t scriptFlagsToCmdFlags(uint64_t cmd_flags, uint64_t script_flags) { /* If the script declared flags, clear the ones from the command and use the ones it declared.*/ cmd_flags &= ~(CMD_STALE | CMD_DENYOOM | CMD_WRITE); @@ -122,6 +134,8 @@ uint64_t scriptFlagsToCmdFlags(uint64_t cmd_flags, uint64_t script_flags) { /* In addition the MAY_REPLICATE flag is set for these commands, but * if we have flags we know if it's gonna do any writes or not. */ + + /* 此外,为这些命令设置了 MAY_REPLICATE 标志,但如果我们有标志,我们就知道它是否会进行任何写入 */ cmd_flags &= ~CMD_MAY_REPLICATE; return cmd_flags; @@ -131,17 +145,22 @@ uint64_t scriptFlagsToCmdFlags(uint64_t cmd_flags, uint64_t script_flags) { int scriptPrepareForRun(scriptRunCtx *run_ctx, client *engine_client, client *caller, const char *funcname, uint64_t script_flags, int ro) { serverAssert(!curr_run_ctx); + /* 从节点、且当前主从复制是不正常的 */ int running_stale = server.masterhost && server.repl_state != REPL_STATE_CONNECTED && server.repl_serve_stale_data == 0; + /* 是否是一个 master client 或者是一个 aof client */ int obey_client = mustObeyClient(caller); + /* 存在特殊的标记 */ if (!(script_flags & SCRIPT_FLAG_EVAL_COMPAT_MODE)) { + /* 不允许 cluster 模式下执行 */ if ((script_flags & SCRIPT_FLAG_NO_CLUSTER) && server.cluster_enabled) { addReplyError(caller, "Can not run script on cluster, 'no-cluster' flag is set."); return C_ERR; } + /* 判断是否允许故障模式下执行 */ if (running_stale && !(script_flags & SCRIPT_FLAG_ALLOW_STALE)) { addReplyError(caller, "-MASTERDOWN Link with MASTER is down, " "replica-serve-stale-data is set to 'no' " @@ -149,17 +168,24 @@ int scriptPrepareForRun(scriptRunCtx *run_ctx, client *engine_client, client *ca return C_ERR; } + /* 判断是否允许写入操作 */ if (!(script_flags & SCRIPT_FLAG_NO_WRITES)) { /* Script may perform writes we need to verify: * 1. we are not a readonly replica * 2. no disk error detected * 3. command is not `fcall_ro`/`eval[sha]_ro` */ + + /* 脚本可能会执行我们需要验证的写入: + * 1.我们不是只读副本 + * 2.未检测到磁盘错误 + * 3.命令不是 `fcall_ro`/`eval[sha]_ro` */ if (server.masterhost && server.repl_slave_ro && !obey_client) { addReplyError(caller, "-READONLY Can not run script with write flag on readonly replica"); return C_ERR; } /* Deny writes if we're unale to persist. */ + /* 当前写磁盘错误 */ int deny_write_type = writeCommandsDeniedByDiskError(); if (deny_write_type != DISK_ERROR_TYPE_NONE && !obey_client) { if (deny_write_type == DISK_ERROR_TYPE_RDB) @@ -174,6 +200,7 @@ int scriptPrepareForRun(scriptRunCtx *run_ctx, client *engine_client, client *ca return C_ERR; } + /* 当前属于只读模式 */ if (ro) { addReplyError(caller, "Can not execute a script with write flag using *_ro command."); return C_ERR; @@ -181,6 +208,8 @@ int scriptPrepareForRun(scriptRunCtx *run_ctx, client *engine_client, client *ca /* Don't accept write commands if there are not enough good slaves and * user configured the min-slaves-to-write option. */ + + /* 当前的主节点是否可用 */ if (server.masterhost == NULL && server.repl_min_slaves_max_lag && server.repl_min_slaves_to_write && @@ -193,6 +222,8 @@ int scriptPrepareForRun(scriptRunCtx *run_ctx, client *engine_client, client *ca /* Check OOM state. the no-writes flag imply allow-oom. we tested it * after the no-write error, so no need to mention it in the error reply. */ + + /* 检查 OOM 状态。no writes 标志表示允许 oom 。我们在无写错误之后测试了它,因此无需在错误回复中提及它 */ if (server.pre_command_oom_state && server.maxmemory && !(script_flags & (SCRIPT_FLAG_ALLOW_OOM|SCRIPT_FLAG_NO_WRITES))) { @@ -203,6 +234,8 @@ int scriptPrepareForRun(scriptRunCtx *run_ctx, client *engine_client, client *ca } else { /* Special handling for backwards compatibility (no shebang eval[sha]) mode */ + + /* 向后兼容(无 shebang eval[sha])模式的特殊处理, 如果当前服务故障中,则返回错误 */ if (running_stale) { addReplyErrorObject(caller, shared.masterdownerr); return C_ERR; @@ -213,36 +246,44 @@ int scriptPrepareForRun(scriptRunCtx *run_ctx, client *engine_client, client *ca run_ctx->original_client = caller; run_ctx->funcname = funcname; + /* 绑定 client 和上下文的关系 */ client *script_client = run_ctx->c; client *curr_client = run_ctx->original_client; server.script_caller = curr_client; /* Select the right DB in the context of the Lua client */ selectDb(script_client, curr_client->db->id); + /* 设置协议为2 */ script_client->resp = 2; /* Default is RESP2, scripts can change it. */ /* If we are in MULTI context, flag Lua client as CLIENT_MULTI. */ + /* 透传事务标识 */ if (curr_client->flags & CLIENT_MULTI) { script_client->flags |= CLIENT_MULTI; } + /* 初始化相关时间信息, 前者用来判断执行时间, 后者用于 key 的过期判断 */ run_ctx->start_time = getMonotonicUs(); run_ctx->snapshot_time = mstime(); run_ctx->flags = 0; + /* 默认设置 aof 和 repl 标记 */ run_ctx->repl_flags = PROPAGATE_AOF | PROPAGATE_REPL; + /* 设置只读标记 */ if (ro || (!(script_flags & SCRIPT_FLAG_EVAL_COMPAT_MODE) && (script_flags & SCRIPT_FLAG_NO_WRITES))) { /* On fcall_ro or on functions that do not have the 'write' * flag, we will not allow write commands. */ run_ctx->flags |= SCRIPT_READ_ONLY; } + /* 设置 OOM 标记 */ if (!(script_flags & SCRIPT_FLAG_EVAL_COMPAT_MODE) && (script_flags & SCRIPT_FLAG_ALLOW_OOM)) { /* Note: we don't need to test the no-writes flag here and set this run_ctx flag, * since only write commands can are deny-oom. */ run_ctx->flags |= SCRIPT_ALLOW_OOM; } + /* 设置允许跨分片标记 */ if ((script_flags & SCRIPT_FLAG_EVAL_COMPAT_MODE) || (script_flags & SCRIPT_FLAG_ALLOW_CROSS_SLOT)) { run_ctx->flags |= SCRIPT_ALLOW_CROSS_SLOT; } @@ -254,10 +295,14 @@ int scriptPrepareForRun(scriptRunCtx *run_ctx, client *engine_client, client *ca } /* Reset the given run ctx after execution */ + +/* 执行后重置给定的运行 ctx */ void scriptResetRun(scriptRunCtx *run_ctx) { serverAssert(curr_run_ctx); /* After the script done, remove the MULTI state. */ + + /* 脚本完成后,删除 MULTI 状态 */ run_ctx->c->flags &= ~CLIENT_MULTI; server.script_caller = NULL; @@ -291,6 +336,8 @@ int scriptIsEval() { } /* Kill the current running script */ + +/* 终止当前运行的脚本 */ void scriptKill(client *c, int is_eval) { if (!curr_run_ctx) { addReplyError(c, "-NOTBUSY No scripts in execution right now."); @@ -300,6 +347,7 @@ void scriptKill(client *c, int is_eval) { addReplyError(c, "-UNKILLABLE The busy script was sent by a master instance in the context of replication and cannot be killed."); } + /* 如果数据已修改,则禁止终止 */ if (curr_run_ctx->flags & SCRIPT_WRITE_DIRTY) { addReplyError(c, "-UNKILLABLE Sorry the script already executed write " @@ -345,12 +393,16 @@ static int scriptVerifyACL(client *c, sds *err) { return C_OK; } +/* 校验当前写命令是否能执行成功 */ static int scriptVerifyWriteCommandAllow(scriptRunCtx *run_ctx, char **err) { /* A write command, on an RO command or an RO script is rejected ASAP. * Note: For scripts, we consider may-replicate commands as write commands. * This also makes it possible to allow read-only scripts to be run during * CLIENT PAUSE WRITE. */ + + /* RO 命令或 RO 脚本上的写入命令将被尽快拒绝。 + * 注意:对于脚本,我们认为可以将命令复制为写命令。 */ if (run_ctx->flags & SCRIPT_READ_ONLY && (run_ctx->c->cmd->flags & (CMD_WRITE|CMD_MAY_REPLICATE))) { @@ -360,17 +412,23 @@ static int scriptVerifyWriteCommandAllow(scriptRunCtx *run_ctx, char **err) { /* The other checks below are on the server state and are only relevant for * write commands, return if this is not a write command. */ + + /* 下面的其他检查针对服务器状态,仅与写命令相关,如果这不是写命令,则返回 */ if (!(run_ctx->c->cmd->flags & CMD_WRITE)) return C_OK; /* If the script already made a modification to the dataset, we can't * fail it on unpredictable error state. */ + + /* 如果脚本已经对数据集进行了修改,我们不能在不可预测的错误状态下使其失败 */ if ((run_ctx->flags & SCRIPT_WRITE_DIRTY)) return C_OK; /* 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. */ + + /* 禁止对只读从属设备执行写入命令,或者如果在脚本上下文中已调用了标记为非确定性的命令,则禁止执行写入命令 */ int deny_write_type = writeCommandsDeniedByDiskError(); if (server.masterhost && server.repl_slave_ro && @@ -389,6 +447,9 @@ static int scriptVerifyWriteCommandAllow(scriptRunCtx *run_ctx, char **err) { * user configured the min-slaves-to-write option. Note this only reachable * for Eval scripts that didn't declare flags, see the other check in * scriptPrepareForRun */ + + /* 如果没有足够好的从机,并且用户配置了 min-slavesto-write 选项,则不要接受 write 命令。 + * 请注意,这只适用于未声明标志的 Eval 脚本,请参阅 scriptPrepareForRun 中的其他检查 */ if (!checkGoodReplicasStatus()) { *err = sdsdup(shared.noreplicaserr->ptr); return C_ERR; @@ -408,6 +469,7 @@ static int scriptVerifyOOM(scriptRunCtx *run_ctx, char **err) { * first write in the context of this script, otherwise we can't stop * in the middle. */ + /* 如果我们达到了通过 maxmemory 配置的内存限制,则不允许使用可能扩大内存使用量的命令,但前提是这是该脚本上下文中的第一次写入,否则我们不能中途停止 */ if (server.maxmemory && /* Maxmemory is actually enabled. */ !mustObeyClient(run_ctx->original_client) && /* Don't care about mem for replicas or AOF. */ !(run_ctx->flags & SCRIPT_WRITE_DIRTY) && /* Script had no side effects so far. */ @@ -428,6 +490,8 @@ static int scriptVerifyClusterState(scriptRunCtx *run_ctx, client *c, client *or /* If this is a Redis Cluster node, we need to make sure the script 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. */ + + /* 如果这是一个 Redis Cluster 节点,我们需要确保脚本没有试图访问非本地密钥,除了从我们的主机接收到的命令或将 AOF 加载回内存时 */ int error_code; /* Duplicate relevant flags in the script client. */ c->flags &= ~(CLIENT_READONLY | CLIENT_ASKING); @@ -451,6 +515,8 @@ static int scriptVerifyClusterState(scriptRunCtx *run_ctx, client *c, client *or /* If the script declared keys in advanced, the cross slot error would have * already been thrown. This is only checking for cross slot keys being accessed * that weren't pre-declared. */ + + /* 如果脚本以高级方式声明键,那么交叉槽错误可能已经被抛出。这只是检查正在访问的未预先声明的交叉槽密钥 */ if (hashslot != -1 && !(run_ctx->flags & SCRIPT_ALLOW_CROSS_SLOT)) { if (original_c->slot == -1) { original_c->slot = hashslot; @@ -475,6 +541,8 @@ int scriptSetResp(scriptRunCtx *run_ctx, int resp) { /* set Repl for a given run_ctx * either: PROPAGATE_AOF | PROPAGATE_REPL*/ + +/* 设置当前的行为 */ int scriptSetRepl(scriptRunCtx *run_ctx, int repl) { if ((repl & ~(PROPAGATE_AOF | PROPAGATE_REPL)) != 0) { return C_ERR; @@ -483,6 +551,7 @@ int scriptSetRepl(scriptRunCtx *run_ctx, int repl) { return C_OK; } +/* 是否禁止故障从节点执行 */ static int scriptVerifyAllowStale(client *c, sds *err) { if (!server.masterhost) { /* Not a replica, stale is irrelevant */ @@ -514,6 +583,11 @@ static int scriptVerifyAllowStale(client *c, sds *err) { * up to the engine to take and parse. * The err out variable is set only if error occurs and describe the error. * If err is set on reply is written to the run_ctx client. */ + +/* 调用 Redis 命令。 + * 回复被写入 run_ctx 客户端,由引擎获取和解析。 + * 仅当发生错误并描述错误时,才会设置 err-out 变量。 + * 如果设置了 err,则会将回复写入 run_ctx 客户端 */ void scriptCall(scriptRunCtx *run_ctx, robj* *argv, int argc, sds *err) { client *c = run_ctx->c; @@ -527,6 +601,7 @@ void scriptCall(scriptRunCtx *run_ctx, robj* *argv, int argc, sds *err) { argv = c->argv; argc = c->argc; + /* 查询对应的命令对象 */ struct redisCommand *cmd = lookupCommand(argv, argc); c->cmd = c->lastcmd = c->realcmd = cmd; if (scriptVerifyCommandArity(cmd, argc, err) != C_OK) { @@ -534,29 +609,37 @@ void scriptCall(scriptRunCtx *run_ctx, robj* *argv, int argc, sds *err) { } /* There are commands that are not allowed inside scripts. */ + + /* 是否禁止调用脚本不可执行的命令 */ if (!server.script_disable_deny_script && (cmd->flags & CMD_NOSCRIPT)) { *err = sdsnew("This Redis command is not allowed from script"); goto error; } + /* 是否禁止故障从节点执行 */ if (scriptVerifyAllowStale(c, err) != C_OK) { goto error; } + /* 校验权限状态 */ if (scriptVerifyACL(c, err) != C_OK) { goto error; } + /* 校验写命令是否可执行 */ if (scriptVerifyWriteCommandAllow(run_ctx, err) != C_OK) { goto error; } + /* 校验 oom */ if (scriptVerifyOOM(run_ctx, err) != C_OK) { goto error; } if (cmd->flags & CMD_WRITE) { /* signify that we already change the data in this execution */ + + /* 表示我们已经更改了此执行中的数据 */ run_ctx->flags |= SCRIPT_WRITE_DIRTY; } @@ -581,6 +664,8 @@ void scriptCall(scriptRunCtx *run_ctx, robj* *argv, int argc, sds *err) { } /* Returns the time when the script invocation started */ + +/* 返回脚本调用开始的时间 */ mstime_t scriptTimeSnapshot() { serverAssert(curr_run_ctx); return curr_run_ctx->snapshot_time; diff --git a/src/script.h b/src/script.h index b601342c5..769790e49 100644 --- a/src/script.h +++ b/src/script.h @@ -48,6 +48,18 @@ * to clients and perform script kill */ +/* script.c单元为函数和eval提供了与Redis交互的API。 + * 交互主要包括执行命令,但也包括调用Redis返回长脚本或检查脚本是否已被删除。 + * 交互是使用scriptRunCtx对象完成的 + * 需要由用户创建并使用scriptPrepareForRun初始化。 + * + * 该单元公开的功能的详细列表: + * 1.调用命令(包括所有验证检查,如acl、cluster、read-only run等) + * 2.设置响应 + * 3.设置复制方法(AOF/Replication/NONE) + * 4.在长时间运行的脚本上调用Redis,以允许Redis回复客户端并执行脚本终止 + * 主要定位在于是c语言实现的lua方法 */ + /* * scriptInterrupt function will return one of those value, * @@ -67,6 +79,7 @@ #define SCRIPT_ALLOW_CROSS_SLOT (1ULL<<8) /* Indicate that the current script may access keys from multiple slots */ typedef struct scriptRunCtx scriptRunCtx; +/* 运行时上下文 */ struct scriptRunCtx { const char *funcname; client *c; diff --git a/src/script_lua.c b/src/script_lua.c index 22eb58102..dfdd7ef6e 100644 --- a/src/script_lua.c +++ b/src/script_lua.c @@ -112,6 +112,11 @@ static char *lua_builtins_removed_after_initialization_allow_list[] = { * * Also notice that the allow list is only checked on start time, * after that the global table is locked so not need to check anything.*/ + +/* 这些允许列表是从允许列表首次引入时用户可用的全局变量创建的。 + * 因为我们不想破坏向后兼容性,所以我们保留了所有的全局变量。允许列表将防止我们在将来意外创建不需要的全局变量。 + * + * 还要注意,只在开始时检查允许列表,之后全局表被锁定,因此无需检查任何内容 */ static char **allow_lists[] = { libraries_allow_list, redis_api_allow_list, @@ -125,6 +130,10 @@ static char **allow_lists[] = { * and there is no need to print a warning message form them. We will print a * log message only if an element was added to the globals and the element is * not on the allow list nor on the back list. */ + +/* 拒绝列表包含我们知道不想添加到全局变量中的元素, + * 并且不需要从中打印警告消息。 + * 只有当元素被添加到全局变量中并且该元素不在允许列表或后备列表中时,我们才会打印日志消息 */ static char *deny_list[] = { "dofile", "loadfile", @@ -155,6 +164,8 @@ static void luaReplyToRedisReply(client *c, client* script_client, lua_State *lu * Save the give pointer on Lua registry, used to save the Lua context and * function context so we can retrieve them from lua_State. */ + +/* 保存Lua注册表上的给定指针,用于保存Lua上下文和函数上下文,以便我们可以从Lua_State中检索它们。*/ void luaSaveOnRegistry(lua_State* lua, const char* name, void* ptr) { lua_pushstring(lua, name); if (ptr) { @@ -168,6 +179,8 @@ void luaSaveOnRegistry(lua_State* lua, const char* name, void* ptr) { /* * Get a saved pointer from registry */ + + /* 获取一个共享对象从寄存器中,主要用于获取上下文 */ void* luaGetFromRegistry(lua_State* lua, const char* name) { lua_pushstring(lua, name); lua_gettable(lua, LUA_REGISTRYINDEX); @@ -230,6 +243,7 @@ static const ReplyParserCallbacks DefaultLuaTypeParserCallbacks = { static void redisProtocolToLuaType(lua_State *lua, char* reply) { ReplyParser parser = {.curr_location = reply, .callbacks = DefaultLuaTypeParserCallbacks}; + /* 格式化数据 */ parseReply(&parser, lua); } @@ -527,6 +541,10 @@ static void redisProtocolToLuaType_Double(void *ctx, double d, const char *proto * since the returned tables are otherwise always indexed by integers, never by strings. * * The function takes ownership on the given err_buffer. */ + +/* 此函数用于以 redis 使用的格式在Lua堆栈上推送错误。 + * pcall 返回错误,这是一个 lua 表,其中 “err” 字段设置为包含错误代码的错误字符串。 + * 请注意,这个表从来不是正确命令的有效回复,因为返回的表总是用整数索引,而不是用字符串索引。*/ void luaPushErrorBuff(lua_State *lua, sds err_buffer) { sds msg; sds error_code; @@ -543,6 +561,13 @@ void luaPushErrorBuff(lua_State *lua, sds err_buffer) { * We support format (1) so this function can reuse the error messages used in other places in redis. * We support format (2) so it'll be easy to pass descriptive errors to this function without worrying about format. */ + + /* 收到的“错误”字符串有两种可能的格式: + * 1. “-CODE msg”:在这种情况下,我们删除前导'-',因为我们不将其存储为lua错误格式的一部分。 + * 2. “msg”:在这种情况下,由于所有错误状态都需要一些错误代码,所以我们在前面加上一个通用的 “ERR” 代码。 + * 我们支持格式(1),因此此函数可以重用 redis 中其他地方使用的错误消息。 + * 我们支持格式(2),所以很容易将描述性错误传递给这个函数,而不用担心格式。 + */ if (err_buffer[0] == '-') { /* derive error code from the message */ char *err_msg = strstr(err_buffer, " "); @@ -561,6 +586,8 @@ void luaPushErrorBuff(lua_State *lua, sds err_buffer) { } /* Trim newline at end of string. If we reuse the ready-made Redis error objects (case 1 above) then we might * have a newline that needs to be trimmed. In any case the lua Redis error table shouldn't end with a newline. */ + + /* 修剪字符串末尾的换行符。如果我们重用现成的 Redis 错误对象(上面的例子1),那么我们可能会有一个需要修剪的换行符。无论如何,lua Redis 错误表不应该以换行符结尾 */ msg = sdstrim(msg, "\r\n"); sds final_msg = sdscatfmt(error_code, " %s", msg); @@ -592,6 +619,8 @@ int luaError(lua_State *lua) { /* Reply to client 'c' converting the top element in the Lua stack to a * Redis reply. As a side effect the element is consumed from the stack. */ + +/* 将 Lua 堆栈中的顶部元素转换为 Redis 回复。作为副作用,元素从堆栈中消耗 */ static void luaReplyToRedisReply(client *c, client* script_client, lua_State *lua) { int t = lua_type(lua,-1); @@ -605,6 +634,7 @@ static void luaReplyToRedisReply(client *c, client* script_client, lua_State *lu return; } + /* 主要针对 string、boolean、number、table 四种类型的特殊处理 */ switch(t) { case LUA_TSTRING: addReplyBulkCBuffer(c,(char*)lua_tostring(lua,-1),lua_strlen(lua,-1)); @@ -625,8 +655,15 @@ static void luaReplyToRedisReply(client *c, client* script_client, lua_State *lu * Status replies are returned as single element table with 'ok' * field. */ + /* 我们需要检查它是数组、错误还是状态回复。 + * 错误作为带有 “err” 字段的单个元素表返回。 + * 状态回复作为带有 “ok” 字段的单元素表返回 */ + /* Handle error reply. */ /* we took care of the stack size on function start */ + + /* 处理错误回复*/ + /* 我们在函数启动时处理了堆栈大小 */ lua_pushstring(lua,"err"); lua_gettable(lua,-2); t = lua_type(lua,-1); @@ -710,6 +747,7 @@ static void luaReplyToRedisReply(client *c, client* script_client, lua_State *lu lua_pop(lua,1); /* Discard field name pushed before. */ /* Handle map reply. */ + /* 如果存在的是一个map字段 */ lua_pushstring(lua,"map"); lua_gettable(lua,-2); t = lua_type(lua,-1); @@ -721,7 +759,9 @@ static void luaReplyToRedisReply(client *c, client* script_client, lua_State *lu while (lua_next(lua,-2)) { /* Stack now: table, key, value */ lua_pushvalue(lua,-2); /* Dup key before consuming. */ + /* 递归分析 */ luaReplyToRedisReply(c, script_client, lua); /* Return key. */ + /* 递归分析 */ luaReplyToRedisReply(c, script_client, lua); /* Return value. */ /* Stack now: table, key. */ maplen++; @@ -733,6 +773,7 @@ static void luaReplyToRedisReply(client *c, client* script_client, lua_State *lu lua_pop(lua,1); /* Discard field name pushed before. */ /* Handle set reply. */ + /* 如果存在的是一个set字段 */ lua_pushstring(lua,"set"); lua_gettable(lua,-2); t = lua_type(lua,-1); @@ -745,6 +786,7 @@ static void luaReplyToRedisReply(client *c, client* script_client, lua_State *lu /* Stack now: table, key, true */ lua_pop(lua,1); /* Discard the boolean value. */ lua_pushvalue(lua,-1); /* Dup key before consuming. */ + /* 递归分析 */ luaReplyToRedisReply(c, script_client, lua); /* Return key. */ /* Stack now: table, key. */ setlen++; @@ -756,6 +798,8 @@ static void luaReplyToRedisReply(client *c, client* script_client, lua_State *lu lua_pop(lua,1); /* Discard field name pushed before. */ /* Handle the array reply. */ + + /* 如果存在的是一个 array 字段 */ void *replylen = addReplyDeferredLen(c); int j = 1, mbulklen = 0; while(1) { @@ -763,10 +807,12 @@ static void luaReplyToRedisReply(client *c, client* script_client, lua_State *lu lua_pushnumber(lua,j++); lua_gettable(lua,-2); t = lua_type(lua,-1); + /* 未知类型 */ if (t == LUA_TNIL) { lua_pop(lua,1); break; } + /* 递归分析 */ luaReplyToRedisReply(c, script_client, lua); mbulklen++; } @@ -782,6 +828,7 @@ static void luaReplyToRedisReply(client *c, client* script_client, lua_State *lu * Lua redis.* functions implementations. * ------------------------------------------------------------------------- */ +/* 将 lua 参数转换为 redis 参数数组 */ static robj **luaArgsToRedisArgv(lua_State *lua, int *argc) { int j; /* Require at least one argument */ @@ -799,6 +846,7 @@ static robj **luaArgsToRedisArgv(lua_State *lua, int *argc) { 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. */ @@ -821,6 +869,8 @@ static robj **luaArgsToRedisArgv(lua_State *lua, int *argc) { /* 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). */ + + /* 检查 Lua 脚本传递的参数之一是否不是字符串或整数 ( Lua_isstring 对于整数也返回 true )*/ if (j != *argc) { j--; while (j >= 0) { @@ -835,8 +885,10 @@ static robj **luaArgsToRedisArgv(lua_State *lua, int *argc) { return argv; } +/* lua 调用 redis 方法, raise_error 表示是否会引发错误导致程序暂停 */ static int luaRedisGenericCommand(lua_State *lua, int raise_error) { int j; + /* 获取上下文信息 */ scriptRunCtx* rctx = luaGetFromRegistry(lua, REGISTRY_RUN_CTX_NAME); if (!rctx) { luaPushError(lua, "redis.call/pcall can only be called inside a script invocation"); @@ -846,6 +898,7 @@ static int luaRedisGenericCommand(lua_State *lua, int raise_error) { client* c = rctx->c; sds reply; + /* 拿到 redis 能用的参数信息 */ int argc; robj **argv = luaArgsToRedisArgv(lua, &argc); if (argv == NULL) { @@ -858,6 +911,9 @@ static int luaRedisGenericCommand(lua_State *lua, int raise_error) { * 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. */ + + /* 通过使用 Lua 调试挂钩,可以触发对 luaRedisGenericCommand() 的递归调用,这通常不应该发生。 + * 使此函数可重入是徒劳的,而且会使它变慢,但我们至少应该检测到这种误用,并终止*/ if (inuse) { char *recursion_warning = "luaRedisGenericCommand() recursive call detected. " @@ -869,6 +925,7 @@ static int luaRedisGenericCommand(lua_State *lua, int raise_error) { inuse++; /* Log the command if debugging is active. */ + /* 如果调试处于活动状态,则记录命令 */ if (ldbIsEnabled()) { sds cmdlog = sdsnew(""); for (j = 0; j < c->argc; j++) { @@ -890,6 +947,8 @@ static int luaRedisGenericCommand(lua_State *lua, int raise_error) { sdsfree(err); /* push a field indicate to ignore updating the stats on this error * because it was already updated when executing the command. */ + + /* push 字段表示忽略更新此错误的统计信息,因为它在执行命令时已更新 */ lua_pushstring(lua,"ignore_error_stats_update"); lua_pushboolean(lua, 1); lua_settable(lua,-3); @@ -899,10 +958,15 @@ static int luaRedisGenericCommand(lua_State *lua, int raise_error) { /* 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. */ + + /* 将 Redis 命令的结果转换为合适的Lua类型。 + * 我们需要的第一件事是从客户端输出缓冲区创建一个字符串*/ if (listLength(c->reply) == 0 && (size_t)c->bufpos < c->buf_usable_size) { /* 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. */ + + /* 对于客户端静态缓冲区内的常见回复情况,这是一条快速路径。不要创建 SDS 字符串,而是直接使用客户端缓冲区 */ c->buf[c->bufpos] = '\0'; reply = c->buf; c->bufpos = 0; @@ -916,6 +980,7 @@ static int luaRedisGenericCommand(lua_State *lua, int raise_error) { listDelNode(c->reply,listFirst(c->reply)); } } + /* 如果需要错误,但返回值不为错误,则修改错误状态 */ if (raise_error && reply[0] != '-') raise_error = 0; redisProtocolToLuaType(lua,reply); @@ -929,6 +994,8 @@ static int luaRedisGenericCommand(lua_State *lua, int raise_error) { cleanup: /* Clean up. Command code may have changed argv/argc so we use the * argv/argc of the client instead of the local variables. */ + + /* 清理干净。命令代码可能已更改 argv/argc ,因此我们使用客户端的 argv/argc 而不是本地变量*/ freeClientArgv(c); c->user = NULL; inuse--; @@ -937,6 +1004,8 @@ static int luaRedisGenericCommand(lua_State *lua, int raise_error) { /* If we are here we should have an error in the stack, in the * form of a table with an "err" field. Extract the string to * return the plain error. */ + + /* 如果我们在这里,堆栈中应该有一个错误,以带有 “err” 字段的表的形式。提取字符串以返回纯错误 */ return luaError(lua); } return 1; @@ -951,6 +1020,11 @@ static int luaRedisGenericCommand(lua_State *lua, int raise_error) { * object is a string. To keep backward * comparability we catch the table object * and just return the error message. */ + +/* 我们对 lua-pcall 的实现。 + * 我们需要这个实现来向后比较旧的 Redis 版本。 + * + * 在Redis 7上,错误对象是一个表,与旧版本的错误对象是字符串相比。为了保持向后可比性,我们捕获表对象并返回错误消息 */ static int luaRedisPcall(lua_State *lua) { int argc = lua_gettop(lua); lua_pushboolean(lua, 1); /* result place holder */ @@ -983,6 +1057,8 @@ static int luaRedisPCallCommand(lua_State *lua) { /* This adds redis.sha1hex(string) to Lua scripts using the same hashing * function used for sha1ing lua scripts. */ + +/* 这增加了 redis.sha1hex(string) 转换为 Lua 脚本,使用与转换 Lua 脚本相同的哈希函数 */ static int luaRedisSha1hexCommand(lua_State *lua) { int argc = lua_gettop(lua); char digest[41]; @@ -1007,6 +1083,9 @@ static int luaRedisSha1hexCommand(lua_State *lua) { * return redis.error_reply("ERR Some Error") * return redis.status_reply("ERR Some Error") */ + +/* 返回一个表,其中单个字段 “field” 设置为作为参数传递的字符串值。 + * 当从 Lua 返回 Redis Protocol 错误或状态回复时会非常方便 */ static int luaRedisReturnSingleFieldTable(lua_State *lua, char *field) { if (lua_gettop(lua) != 1 || lua_type(lua,-1) != LUA_TSTRING) { luaPushError(lua, "wrong number or type of arguments"); @@ -1048,6 +1127,8 @@ static int luaRedisStatusReplyCommand(lua_State *lua) { * * Set the propagation of write commands executed in the context of the * script to on/off for AOF and slaves. */ + +/* 将在脚本上下文中执行的写命令的传播设置为 AOF 和 slave 的开/关 */ static int luaRedisSetReplCommand(lua_State *lua) { int flags, argc = lua_gettop(lua); @@ -1150,6 +1231,8 @@ static int luaLogCommand(lua_State *lua) { } /* redis.setresp() */ + +/* 设置当前 RESP 的协议版本 */ static int luaSetResp(lua_State *lua) { scriptRunCtx* rctx = luaGetFromRegistry(lua, REGISTRY_RUN_CTX_NAME); if (!rctx) { @@ -1176,6 +1259,7 @@ static int luaSetResp(lua_State *lua) { * Lua engine initialization and reset. * ------------------------------------------------------------------------- */ +/* 加载 lib,通过调用对于的方法实现 */ static void luaLoadLib(lua_State *lua, const char *libname, lua_CFunction luafunc) { lua_pushcfunction(lua, luafunc); lua_pushstring(lua, libname); @@ -1238,6 +1322,11 @@ static int luaProtectedTableError(lua_State *lua) { * The function assumes the Lua stack have a least enough * space to push 2 element, its up to the caller to verify * this before calling this function. */ + +/* 在堆栈顶部的表上设置一个特殊的元表。 + * 如果用户试图获取不存在的值,元表将引发错误。 + * + * 该函数假设 Lua 堆栈至少有足够的空间来推送2元素,这取决于调用方在调用该函数之前验证这一点 */ void luaSetErrorMetatable(lua_State *lua) { lua_newtable(lua); /* push metatable */ lua_pushcfunction(lua, luaProtectedTableError); /* push get error handler */ @@ -1245,8 +1334,10 @@ void luaSetErrorMetatable(lua_State *lua) { lua_setmetatable(lua, -2); } +/* 确定新增数据是否被允许 */ static int luaNewIndexAllowList(lua_State *lua) { int argc = lua_gettop(lua); + /* 回调方法必须有3个数据:table、varname、value */ if (argc != 3) { serverLog(LL_WARNING, "malicious code trying to call luaProtectedTableError with wrong arguments"); luaL_error(lua, "Wrong number of arguments to luaNewIndexAllowList"); @@ -1272,6 +1363,8 @@ static int luaNewIndexAllowList(lua_State *lua) { break; } } + + /* allow_l 为 NULL 时,说明当前不存在满足条件的数据 */ if (!*allow_l) { /* Search the value on the back list, if its there we know that it was removed * on purpose and there is no need to print a warning. */ @@ -1281,10 +1374,12 @@ static int luaNewIndexAllowList(lua_State *lua) { break; } } + /* 如果存在在 deny_list 中,则打印日志,并告知是专门被禁止的字段 */ if (!*c) { serverLog(LL_WARNING, "A key '%s' was added to Lua globals which is not on the globals allow list nor listed on the deny list.", variable_name); } } else { + /* 如果在满足条件中的字段,则绕过 __new_index 直接赋值 */ lua_rawset(lua, -3); } return 0; @@ -1296,6 +1391,9 @@ static int luaNewIndexAllowList(lua_State *lua) { * The metatable is set on the table which located on the top * of the stack. */ + +/* __nexindex 字段是在对表中不存在的值进行赋值(set)时候执行的操作 + * 以验证新索引是否出现在全局 while 列表中 */ void luaSetAllowListProtection(lua_State *lua) { lua_newtable(lua); /* push metatable */ lua_pushcfunction(lua, luaNewIndexAllowList); /* push get error handler */ @@ -1306,9 +1404,14 @@ void luaSetAllowListProtection(lua_State *lua) { /* Set the readonly flag on the table located on the top of the stack * and recursively call this function on each table located on the original * table. Also, recursively call this function on the metatables.*/ + +/* 在位于堆栈顶部的表上设置只读标志,并对位于原始表上的每个表递归调用此函数。此外,在元表上递归调用此函数 */ void luaSetTableProtectionRecursively(lua_State *lua) { /* This protect us from a loop in case we already visited the table * For example, globals has '_G' key which is pointing back to globals. */ + + /* 这样可以保护我们不受循环的影响,以防我们已经访问了表 + * 例如,全局变量具有“_G”键,该键指向全局变量*/ if (lua_isreadonlytable(lua, -1)) { return; } @@ -1366,31 +1469,57 @@ void luaRegisterLogFunction(lua_State* lua) { lua_settable(lua,-3); } +/* 注册相关的 RedisAPI */ void luaRegisterRedisAPI(lua_State* lua) { + /* 设置全局变量的增加限制 */ lua_pushvalue(lua, LUA_GLOBALSINDEX); luaSetAllowListProtection(lua); lua_pop(lua, 1); + /* 加载 lib 库 */ luaLoadLibraries(lua); + /* 将pcall绑定至luaRedisPcall方法上 */ lua_pushcfunction(lua,luaRedisPcall); lua_setglobal(lua, "pcall"); /* Register the redis commands table and fields */ lua_newtable(lua); + /* + * 参考官方:https://redis.io/docs/manual/programmability/lua-api/,已支持的方法如下: + * redis.call(command [,arg...]) + * redis.pcall(command [,arg...]) + * redis.error_reply(x) + * redis.status_reply(x) + * redis.sha1hex(x) + * redis.log(level, message) + * redis.setresp(x) + * redis.set_repl(x) + * redis.replicate_commands() + * redis.breakpoint() + * redis.debug(x) + * redis.acl_check_cmd(command [,arg...]) + * redis.register_function + * redis.REDIS_VERSION + * redis.REDIS_VERSION_NUM + */ /* redis.call */ lua_pushstring(lua,"call"); lua_pushcfunction(lua,luaRedisCallCommand); + /* 赋值call->luaRedisCallCommand */ lua_settable(lua,-3); /* redis.pcall */ lua_pushstring(lua,"pcall"); lua_pushcfunction(lua,luaRedisPCallCommand); + /* 赋值 pcall->luaRedisPCallCommand */ lua_settable(lua,-3); + /* 注册日志方法 */ luaRegisterLogFunction(lua); + /* 注册版本信息 */ luaRegisterVersion(lua); /* redis.setresp */ @@ -1442,9 +1571,13 @@ void luaRegisterRedisAPI(lua_State* lua) { lua_settable(lua,-3); /* Finally set the table as 'redis' global var. */ + + /* 将整个 table 设置给 "redis",用于后续的 redis.xxx 的调用 */ lua_setglobal(lua,REDIS_API_NAME); /* Replace math.random and math.randomseed with our implementations. */ + + /* 在注册math全局变量 */ lua_getglobal(lua,"math"); lua_pushstring(lua,"random"); @@ -1523,9 +1656,12 @@ static int redis_math_randomseed (lua_State *L) { } /* This is the Lua script "count" hook that we use to detect scripts timeout. */ + +/* 这是我们用来检测脚本超时的 Lua 脚本 “count” 挂钩 */ static void luaMaskCountHook(lua_State *lua, lua_Debug *ar) { UNUSED(ar); scriptRunCtx* rctx = luaGetFromRegistry(lua, REGISTRY_RUN_CTX_NAME); + /* 如果当前脚本已经终止了,则返回错误 */ if (scriptInterrupt(rctx) == SCRIPT_KILL) { serverLog(LL_WARNING,"Lua script killed by user with SCRIPT KILL."); @@ -1534,6 +1670,8 @@ static void luaMaskCountHook(lua_State *lua, lua_Debug *ar) {          * will not be able to catch the error with pcall and invoke          * pcall again which will prevent the script from ever been killed */ + + /* 将钩子设置为始终调用,这样用户将无法用 pcall 捕获错误并再次调用 pcall,这将防止脚本被终止 */ lua_sethook(lua, luaMaskCountHook, LUA_MASKLINE, 0); luaPushError(lua,"Script killed by user with SCRIPT KILL..."); @@ -1541,12 +1679,14 @@ static void luaMaskCountHook(lua_State *lua, lua_Debug *ar) { } } +/* 释放错误信息的空间 */ void luaErrorInformationDiscard(errorInfo *err_info) { if (err_info->msg) sdsfree(err_info->msg); if (err_info->source) sdsfree(err_info->source); if (err_info->line) sdsfree(err_info->line); } +/* 解析当前堆栈中的错误信息 */ void luaExtractErrorInformation(lua_State *lua, errorInfo *err_info) { if (lua_isstring(lua, -1)) { err_info->msg = sdscatfmt(sdsempty(), "ERR %s", lua_tostring(lua, -1)); @@ -1580,6 +1720,7 @@ void luaExtractErrorInformation(lua_State *lua, errorInfo *err_info) { lua_pop(lua, 1); } +/* 允许lua脚本方法 */ void luaCallFunction(scriptRunCtx* run_ctx, lua_State *lua, robj** keys, size_t nkeys, robj** args, size_t nargs, int debug_enabled) { client* c = run_ctx->original_client; int delhook = 0; @@ -1588,12 +1729,22 @@ void luaCallFunction(scriptRunCtx* run_ctx, lua_State *lua, robj** keys, size_t * Lua hook might be called wheneven we run any Lua instruction * such as 'luaSetGlobalArray' and we want the run_ctx to be available * each time the Lua hook is invoked. */ + + /* 我们必须在设置 Lua 钩子之前设置它,理论上,当我们运行任何 Lua 指令(如 “luaSetGlobalArray” )时, + * 都可能会调用 Lua 钩子,并且我们希望每次调用Lua挂钩时,run_ctx 都可用 */ luaSaveOnRegistry(lua, REGISTRY_RUN_CTX_NAME, run_ctx); + /* 当前的 hook 信息 + * LUA_MASKCALL:调用一个函数时,就调用一次钩子函数。 + * LUA_MASKRET:从一个函数中返回时,就调用一次钩子函数。 + * LUA_MASKLINE:执行一行指令时,就回调一次钩子函数。 + * LUA_MASKCOUNT:执行指定数量的指令时,就回调一次钩子函数。 */ if (server.busy_reply_threshold > 0 && !debug_enabled) { + /* 如果存在忙碌限制,则增加 hook 方法 */ lua_sethook(lua,luaMaskCountHook,LUA_MASKCOUNT,100000); delhook = 1; } else if (debug_enabled) { + /* 如果存在 debug 限制,则增加 hook 方法 */ lua_sethook(lua,luaLdbLineHook,LUA_MASKLINE|LUA_MASKCOUNT,100000); delhook = 1; } @@ -1602,6 +1753,8 @@ void luaCallFunction(scriptRunCtx* run_ctx, lua_State *lua, robj** keys, size_t * EVAL received. */ luaCreateArray(lua,keys,nkeys); /* On eval, keys and arguments are globals. */ + + /* 如果是 eval 命令,则会把 key 和 arg 都做为全局变量,若是 function,则不需要设置 */ if (run_ctx->flags & SCRIPT_EVAL_MODE){ /* open global protection to set KEYS */ lua_enablereadonlytable(lua, LUA_GLOBALSINDEX, 0); @@ -1622,7 +1775,12 @@ void luaCallFunction(scriptRunCtx* run_ctx, lua_State *lua, robj** keys, size_t * In addition the error handler is located on position -2 on the Lua stack. * On function mode, we pass 2 arguments (the keys and args tables), * and the error handler is located on position -4 (stack: error_handler, callback, keys, args) */ + + /* 在这一点上,无论这个脚本以前是否从未见过,或者它是否已经定义,我们都可以调用它。 + * 在 eval 模式下,我们有零个参数,并且期望一个返回值。 此外,错误处理程序位于 Lua 堆栈的位置-2。 + * 在函数模式下,我们传递2个参数(键和参数表),错误处理程序位于位置-4(堆栈:error_handler,callback,keys,args)*/ int err; + /* 参考: int lua_pcall (lua_State *L, int nargs, int nresults, int errfunc) */ if (run_ctx->flags & SCRIPT_EVAL_MODE) { err = lua_pcall(lua,0,1,-2); } else { @@ -1635,6 +1793,8 @@ void luaCallFunction(scriptRunCtx* run_ctx, lua_State *lua, robj** keys, size_t * The call is performed every LUA_GC_CYCLE_PERIOD executed commands * (and for LUA_GC_CYCLE_PERIOD collection steps) because calling it * for every command uses too much CPU. */ + + /* 触发整个的 lua 的 gc 能力 */ #define LUA_GC_CYCLE_PERIOD 50 { static long gc_count = 0; @@ -1646,10 +1806,14 @@ void luaCallFunction(scriptRunCtx* run_ctx, lua_State *lua, robj** keys, size_t } } + /* 如果出现了错误 */ if (err) { /* Error object is a table of the following format: * {err='', source='', line=} * We can construct the error message from this information */ + + /* 错误对象是以下格式的表:{err=“<Error msg>”,source=“c, lua); /* Convert and consume the reply. */ } /* Perform some cleanup that we need to do both on error and success. */ + + /* 移除 hook 信息 */ if (delhook) lua_sethook(lua,NULL,0,0); /* Disable hook */ /* remove run_ctx from registry, its only applicable for the current script. */ + + /* 移除上下文信息 */ luaSaveOnRegistry(lua, REGISTRY_RUN_CTX_NAME, NULL); } diff --git a/src/script_lua.h b/src/script_lua.h index 4c2b34804..d34185e24 100644 --- a/src/script_lua.h +++ b/src/script_lua.h @@ -48,6 +48,17 @@ * Uses script.c for interaction back with Redis. */ +/* + * script_lua.c单元提供eval和function_lua之间的共享功能, 提供的功能: + * + * 1. 执行Lua代码,假设代码位于Lua堆栈的顶部。此外,解析执行结果并将其转换为resp并回复客户端。 + * 2. 从Lua代码中运行Redis命令(包括解析回复并从中创建Lua对象)。 + * 3. 向Lua解释器注册Redis API。仅注册共享API(仅与eval.c相关的API(如调试)在eval.cc上注册)。 + * + * 使用script.c用于与Redis交互。 + * 主要定位是在完全和lua引擎交互。 + */ + #include "server.h" #include "script.h" #include