淺析 redis lua 實現
關於 redis lua 的使用大家都不陌生,應用場景需要把複雜邏輯的原子性,比如計數器,分佈式鎖。見過沒用 lua 實現的鎖,不出 bug 也算是神奇
好奇實現的細節,閱讀了幾個版本,本文源碼展示爲 3.2 版本, 7.0 重構比較多,看着乾淨一些
一致性
redis> EVAL "return { KEYS[1], KEYS[2], ARGV[1], ARGV[2], ARGV[3] }" 2 key1 key2 arg1 arg2 arg3
1) "key1"
2) "key2"
3) "arg1"
4) "arg2"
5) "arg3"
上面是簡單的測試用例,其中 2 表示緊隨其後的兩個參數是 key, 我們通過 KEYS[i]
來獲取,後面的是參數,通過 ARGV[i]
獲取。我司歷史上遇到過一次 redis 主從數據不一致的情況,原因比較簡單:
Lua 腳本需要 hgetall
拿到所有數據,但是依賴順序,恰好此時底層結構 master 己經變成了 hashtable
, 但是 slave 還是 ziplist
, 獲取到的第一個數據當成 key 去做其它邏輯,導致主從不一致發生
引出使用 redis lua 最佳實踐之一:無論單機還是集羣模式,對於 key 的操作必須通過參數列表,顯示的傳進去,而不能依賴腳本或是隨機邏輯
結論其實顯而易見,會有數據不一致的風險,同時對於 cluster 模式,要求所有 keys 所在的 slot 必須在同一個 shard 內,這個檢測是在 smart client 或者是 cluster proxy 端
導致問題的原因在於,redis 舊版本同步時,本質上還是直接執行的 lua 腳本,這種模式叫做 verbatim replication
. 如果只同步 lua 腳本修改的內容可以避免這類 issue, 類似於 mysql binlog 的 SQL 模式和 ROW 模式的區別 (也不完全一樣)
實際上 redis 也是這麼做的,3.2 版本引入 redis.replicate_commands()
, 只同步變更的內容,稱爲 effects replication
模式。5.0 lua 默認爲該模式,在 7.0 中移除了舊版本的 verbatim replication
的支持
int luaRedisGenericCommand(lua_State *lua, int raise_error) {
......
/* If we are using single commands replication, we need to wrap what
* we propagate into a MULTI/EXEC block, so that it will be atomic like
* a Lua script in the context of AOF and slaves. */
if (server.lua_replicate_commands &&
!server.lua_multi_emitted &&
server.lua_write_dirty &&
server.lua_repl != PROPAGATE_NONE)
{
execCommandPropagateMulti(server.lua_caller);
server.lua_multi_emitted = 1;
}
/* Run the command */
int call_flags = CMD_CALL_SLOWLOG | CMD_CALL_STATS;
if (server.lua_replicate_commands) {
/* Set flags according to redis.set_repl() settings. */
if (server.lua_repl & PROPAGATE_AOF)
call_flags |= CMD_CALL_PROPAGATE_AOF;
if (server.lua_repl & PROPAGATE_REPL)
call_flags |= CMD_CALL_PROPAGATE_REPL;
}
call(c, call_flags);
......
}
3.2 版本中,當 lua 虛擬機執行 redis.call 或者 redis.pcall 時調用 luaRedisGenericCommand
, 如果開啓了 lua_replicate_commands
選項,那麼生成一個 multi
事務命令用於複製
同時 call
去真正執行命令時,call_flags 打上 CMD_CALL_PROPAGATE_AOF
與 CMD_CALL_PROPAGATE_REPL
標籤,執行命令時生成同步命令
void evalGenericCommand(client *c, int evalsha) {
......
/* If we are using single commands replication, emit EXEC if there
* was at least a write. */
if (server.lua_replicate_commands) {
preventCommandPropagation(c);
if (server.lua_multi_emitted) {
robj *propargv[1];
propargv[0] = createStringObject("EXEC",4);
alsoPropagate(server.execCommand,c->db->id,propargv,1,
PROPAGATE_AOF|PROPAGATE_REPL);
decrRefCount(propargv[0]);
}
}
......
}
略去無關代碼,evalGenericCommand
函數最後判斷,如果處於 effects replication
模式,那麼只通過事務去執行產生的命令,而不是同步 lua 腳本,生成一個 exec
命令
另外爲了保證 deterministic
確定性,redis lua 做了以下事情:
-
redis lua 不允許獲取系統時間或者外部狀態
-
修改了僞隨機函數
math.random
, 使用同一個種子,使得每次獲取得到隨機序列是一樣的 (除非指定了 math.randomseed) -
有些命令返回的結果是沒有排序的,比如
SMEMBERS
, 4.0 版本 redids lua 會額外的做一次排序再返回。但是 5.0 後去掉了這個排序,因爲前面提到的effects replication
避免了這個問題,但是使用時不要假設有任何排序,是否排序要看普通命令的文檔說明 -
當用戶的腳本調用
RANDOMKEY
,SRANDMEMBER
,TIME
隨機命令後,嘗試去修改數據庫,會報錯。但是隻讀的 lua 腳本可以調用這些 non-deterinistic 命令
緩存
一般我們用 eval
命令執行 lua 腳本內容,但是對於高頻執行的腳本,每次都要從文本中解析生成 function 開銷會很高,所以引入了 evalsha
命令
> script load "redis.call('incr', KEYS[1])"
"072f8f1f3cac2bbf3017c4af7c73e742aa085ac5"
先調用 script load
生成對應腳本的 hash 值,每次執行時只需要傳入 hash 值即可
> EVALSHA da0bf4095ef4b6f337f03ba9dcd326dbc5fc8ace 1 testkey
(nil)
對於 failover, 或第一次執行時 redis 不存在該 lua 函數則報錯
> EVALSHA da0bf4095ef4b6f337f03ba9dcd326dbc5fc8aca 1 testkey
(error) NOSCRIPT No matching script. Please use EVAL.
所以,我們在封裝 redis client 時要處理異常情況
-
Client 初始化計算 sha 值後,直接
evalsha
調用腳本 -
如果失敗,返回錯誤是
NOSCRIPT
, 再調用SCRIPT LOAD
創建 lua 函數,Client 再正常調用evalsha
void scriptCommand(client *c) {
......
} 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])
== C_ERR) {
sdsfree(sha);
return;
}
}
addReplyBulkCBuffer(c,funcname+2,40);
sdsfree(sha);
forceCommandPropagation(c,PROPAGATE_REPL|PROPAGATE_AOF);
......
}
命令入口函數 scriptCommand
,LOAD
名字很不直觀,以爲是個只讀命令,但實際上做了很多事情:
-
計算 sha 值
-
luaCreateFunction
創建運行時函數 -
forceCommandPropagation
設置 flag 參數用於複製到從庫或者 AOF
Lua 源碼走讀
初始化
初始化只需看 scriptingInit
函數,主要功能是加載 lua 庫 (cjson, table, string, math ...), 移除不支除的函數 (loadfile, dofile), 註冊我們常用的命令表到 lua table 中 (call, pcall, log, math, random ...), 最後創建虛擬的 redisClient 用於執行命令
void scriptingInit(int setup) {
lua_State *lua = lua_open();
if (setup) {
server.lua_client = NULL;
server.lua_caller = NULL;
server.lua_timedout = 0;
server.lua_always_replicate_commands = 0; /* Only DEBUG can change it.*/
ldbInit();
}
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);
......
}
這裏面涉及 c 如何與 lua 語言交互,如何互相調用的問題,不用深究用到了再學即可
lua_newtable(lua);
創建 lua table 併入棧,此時位置是 -1
lua_pushstring(lua,"call");
入棧字符串 call
lua_pushcfunction(lua,luaRedisCallCommand);
入棧函數 luaRedisCallCommand
lua_settable(lua,-3);
生成命令表,此時 table 位置是 -3,然後一次從棧中彈出,即僞代碼爲 table["call"] = luaRedisCallCommand
eval "redis.call('incr', KEYS[1])" 1 testkey
這也就是爲什麼我們的 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");
這裏也看到同時修改了 random 函數行爲
執行命令
eval
函數總入口是 evalCommand
, 這裏參考 3.2 源碼,非 debug 模式下執行調用 evalGenericCommand
, 函數比較長,主要分三大塊
void evalGenericCommand(client *c, int evalsha) {
lua_State *lua = server.lua;
char funcname[43];
long long numkeys;
int delhook = 0, err;
/* When we replicate whole scripts, 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;
server.lua_replicate_commands = server.lua_always_replicate_commands;
server.lua_multi_emitted = 0;
server.lua_repl = PROPAGATE_AOF|PROPAGATE_REPL;
/* 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;
}
命令執行前的檢查階段,設置隨機種子,設置一些 flag, 並檢查 keys 個數是否正確
/* 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]) == C_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 C_ERR. */
return;
}
/* Now the following is guaranteed to return non nil */
lua_getglobal(lua, funcname);
serverAssert(!lua_isnil(lua,-1));
}
lua 中保存腳本 funcname 格式是 f_{evalsha hash}
, 如果每一次執行,調用 luaCreateFunction
讓 lua 虛擬機加載 user_script 腳本
/* 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);
/* 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 && server.masterhost == NULL &&
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;
}
/* At this point whether this script was never seen before or if it was
* already defined, we can call it. We have zero arguments and expect
* a single return value. */
err = lua_pcall(lua,0,1,-2);
/* Perform some cleanup that we need to do both on error and success. */
if (delhook) lua_sethook(lua,NULL,0,0); /* Disable hook */
if (server.lua_timedout) {
server.lua_timedout = 0;
/* Restore the readable handler that was unregistered when the
* script timeout was detected. */
aeCreateFileEvent(server.el,c->fd,AE_READABLE,
readQueryFromClient,c);
}
server.lua_caller = NULL;
luaSetGlobalArray
將 KEYS
, ARGS
以參數形式入棧,設置一堆 debug/slow call 相關的參數,最後 lua_pcall
執行用戶腳本,lua 虛擬機執行腳本時,如果遇到 redis.call
就會回調 redis 函數 luaRedisCallCommand
, 對應的 redis.pcall
執行 luaRedisPCallCommand
函數
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. */
}
/* If we are using single commands replication, emit EXEC if there
* was at least a write. */
if (server.lua_replicate_commands) {
preventCommandPropagation(c);
if (server.lua_multi_emitted) {
robj *propargv[1];
propargv[0] = createStringObject("EXEC",4);
alsoPropagate(server.execCommand,c->db->id,propargv,1,
PROPAGATE_AOF|PROPAGATE_REPL);
decrRefCount(propargv[0]);
}
}
......
if (evalsha && !server.lua_replicate_commands) {
if (!replicationScriptCacheExists(c->argv[1]->ptr)) {
/* This script is not in our script cache, replicate it as
* EVAL, then add it into the script cache, as from now on
* slaves and AOF know about it. */
robj *script = dictFetchValue(server.lua_scripts,c->argv[1]->ptr);
replicationScriptCacheAdd(c->argv[1]->ptr);
serverAssertWithInfo(c,NULL,script != NULL);
rewriteClientCommandArgument(c,0,
resetRefCount(createStringObject("EVAL",4)));
rewriteClientCommandArgument(c,1,script);
forceCommandPropagation(c,PROPAGATE_REPL|PROPAGATE_AOF);
}
}
}
代碼有點長,總體就是執行超時處理,生成 exec
用於複製,最後如果 replication 從庫沒有執行過這個 evlsha
腳本,並且當前模式不是 lua_always_replicate_commands 要把腳本真實內容也先同步到 replication
這裏還有最重要的是 luaReplyToRedisReply(c,lua);
將 lua 返回值,轉換成 redis RESP 格式
再來看一下 luaRedisGenericCommand
是如何調用 redis 函數
int luaRedisGenericCommand(lua_State *lua, int raise_error) {
int j, argc = lua_gettop(lua);
struct redisCommand *cmd;
client *c = server.lua_client;
sds reply;
/* Cached across calls. */
static robj **argv = NULL;
static int argv_size = 0;
static robj *cached_objects[LUA_CMD_OBJCACHE_SIZE];
static size_t cached_objects_len[LUA_CMD_OBJCACHE_SIZE];
static int inuse = 0; /* Recursive calls detection. */
/* By using Lua debug hooks it is possible to trigger a recursive call
* 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. */
if (inuse) {
char *recursion_warning =
"luaRedisGenericCommand() recursive call detected. "
"Are you doing funny stuff with Lua debug hooks?";
serverLog(LL_WARNING,"%s",recursion_warning);
luaPushError(lua,recursion_warning);
return 1;
}
inuse++;
/* Require at least one argument */
if (argc == 0) {
luaPushError(lua,
"Please specify at least one argument for redis.call()");
inuse--;
return raise_error ? luaRaiseError(lua) : 1;
}
/* Build the arguments vector */
if (argv_size < argc) {
argv = zrealloc(argv,sizeof(robj*)*argc);
argv_size = argc;
}
for (j = 0; j < argc; j++) {
char *obj_s;
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. */
lua_Number num = lua_tonumber(lua,j+1);
obj_len = snprintf(dbuf,sizeof(dbuf),"%.17g",(double)num);
obj_s = dbuf;
} else {
obj_s = (char*)lua_tolstring(lua,j+1,&obj_len);
if (obj_s == NULL) break; /* Not a string. */
}
/* Try to use a cached object. */
if (j < LUA_CMD_OBJCACHE_SIZE && cached_objects[j] &&
cached_objects_len[j] >= obj_len)
{
sds s = cached_objects[j]->ptr;
argv[j] = cached_objects[j];
cached_objects[j] = NULL;
memcpy(s,obj_s,obj_len+1);
sdssetlen(s, obj_len);
} else {
argv[j] = createStringObject(obj_s, obj_len);
}
}
/* 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). */
if (j != argc) {
j--;
while (j >= 0) {
decrRefCount(argv[j]);
j--;
}
luaPushError(lua,
"Lua redis() command arguments must be strings or integers");
inuse--;
return raise_error ? luaRaiseError(lua) : 1;
}
/* Setup our fake client for command execution */
c->argv = argv;
c->argc = argc;
/* Log the command if debugging is active. */
if (ldb.active && ldb.step) {
sds cmdlog = sdsnew("<redis>");
for (j = 0; j < c->argc; j++) {
if (j == 10) {
cmdlog = sdscatprintf(cmdlog," ... (%d more)",
c->argc-j-1);
} else {
cmdlog = sdscatlen(cmdlog," ",1);
cmdlog = sdscatsds(cmdlog,c->argv[j]->ptr);
}
}
ldbLog(cmdlog);
}
這裏的功能,主要是從 lua 虛擬機中獲取 eval 腳本的參數,賦值給 redisClient, 爲以後執行命令做準備
/* Command lookup */
cmd = lookupCommand(argv[0]->ptr);
if (!cmd || ((cmd->arity > 0 && cmd->arity != argc) ||
(argc < -cmd->arity)))
{
if (cmd)
luaPushError(lua,
"Wrong number of args calling Redis command From Lua script");
else
luaPushError(lua,"Unknown Redis command called from Lua script");
goto cleanup;
}
c->cmd = c->lastcmd = cmd;
lookupCommand
查表,找到要執行的 redis 命令
/* There are commands that are not allowed inside scripts. */
if (cmd->flags & CMD_NOSCRIPT) {
luaPushError(lua, "This Redis command is not allowed from scripts");
goto cleanup;
}
如果是不允許在 lua 中執行的命令,報錯退出
/* 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. */
if (cmd->flags & CMD_WRITE) {
if (server.lua_random_dirty && !server.lua_replicate_commands) {
luaPushError(lua,
"Write commands not allowed after non deterministic commands. Call redis.replicate_commands() at the start of your script in order to switch to single commands replication mode.");
goto cleanup;
} else if (server.masterhost && server.repl_slave_ro &&
!server.loading &&
!(server.lua_caller->flags & CLIENT_MASTER))
{
luaPushError(lua, shared.roslaveerr->ptr);
goto cleanup;
} else if (server.stop_writes_on_bgsave_err &&
server.saveparamslen > 0 &&
server.lastbgsave_status == C_ERR)
{
luaPushError(lua, shared.bgsaveerr->ptr);
goto cleanup;
}
}
/* If we reached the memory limit configured via maxmemory, commands that
* could enlarge the memory usage are not allowed, but only if this is the
* first write in the context of this script, otherwise we can't stop
* in the middle. */
if (server.maxmemory && server.lua_write_dirty == 0 &&
(cmd->flags & CMD_DENYOOM))
{
if (freeMemoryIfNeeded() == C_ERR) {
luaPushError(lua, shared.oomerr->ptr);
goto cleanup;
}
}
if (cmd->flags & CMD_RANDOM) server.lua_random_dirty = 1;
if (cmd->flags & CMD_WRITE) server.lua_write_dirty = 1;
設置 cmd->flags
/* If this is a Redis Cluster node, we need to make sure Lua 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. */
if (server.cluster_enabled && !server.loading &&
!(server.lua_caller->flags & CLIENT_MASTER))
{
/* Duplicate relevant flags in the lua client. */
c->flags &= ~(CLIENT_READONLY|CLIENT_ASKING);
c->flags |= server.lua_caller->flags & (CLIENT_READONLY|CLIENT_ASKING);
if (getNodeByQuery(c,c->cmd,c->argv,c->argc,NULL,NULL) !=
server.cluster->myself)
{
luaPushError(lua,
"Lua script attempted to access a non local key in a "
"cluster node");
goto cleanup;
}
}
如果是 cluster 模式,要保證 lua 的 keys 所在的 slots 必須在本地 shard
/* If we are using single commands replication, we need to wrap what
* we propagate into a MULTI/EXEC block, so that it will be atomic like
* a Lua script in the context of AOF and slaves. */
if (server.lua_replicate_commands &&
!server.lua_multi_emitted &&
server.lua_write_dirty &&
server.lua_repl != PROPAGATE_NONE)
{
execCommandPropagateMulti(server.lua_caller);
server.lua_multi_emitted = 1;
}
如果是 effect replication
模式,生成 multi
事務命令用於複製
/* Run the command */
int call_flags = CMD_CALL_SLOWLOG | CMD_CALL_STATS;
if (server.lua_replicate_commands) {
/* Set flags according to redis.set_repl() settings. */
if (server.lua_repl & PROPAGATE_AOF)
call_flags |= CMD_CALL_PROPAGATE_AOF;
if (server.lua_repl & PROPAGATE_REPL)
call_flags |= CMD_CALL_PROPAGATE_REPL;
}
call(c,call_flags);
這裏纔去真正的執行命令,call_flags
參數用於控制是否複製,是否生成 AOF 等等
/* 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. */
if (listLength(c->reply) == 0 && c->bufpos < PROTO_REPLY_CHUNK_BYTES) {
/* 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. */
c->buf[c->bufpos] = '\0';
reply = c->buf;
c->bufpos = 0;
} else {
reply = sdsnewlen(c->buf,c->bufpos);
c->bufpos = 0;
while(listLength(c->reply)) {
robj *o = listNodeValue(listFirst(c->reply));
reply = sdscatlen(reply,o->ptr,sdslen(o->ptr));
listDelNode(c->reply,listFirst(c->reply));
}
}
if (raise_error && reply[0] != '-') raise_error = 0;
redisProtocolToLuaType(lua,reply);
......
}
redisProtocolToLuaType
把 redis 結果轉換成 lua 類型返回給 lua 虛擬機
小結
感慨一下,redis 僅有的幾個數據結構就能滿足 90% 的業務需求,最近幾個版本優化非常明顯,大家趕緊升級吧,享受新版的福利
從生產環境上看,大版本穩定半年到一年,大膽升級準沒錯,還在抱殘守缺的用 redis 3.X 4.X 的活該遇到各種問題
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Qx5qzB2lVnOo546WheL9EQ