聊聊redis執行lua原理html
從一次面試場景提及
java
「看你簡歷上寫的精通redis」
面試
「額,還能夠啦」redis
「那你說說redis執行lua腳本的原理」
算法
「這個,這個,不就是那麼執行的嗎,eval 一段lua腳本就好了」數據庫
「好的,瞭解了,今天面試先到這個吧,後續有消息會通知你」編程
「好的,祝您生活愉快」
api
面試場景純屬娛樂,但這個面試題確實是筆者真實遇到過的,今天咱們就來看看redis執行lua腳本的原理,但願經過本篇學習能夠解決心中的困惑,更深層次的講能夠了解到兩種不一樣語言溝通的一點思想,我以爲這個是最寶貴的。數組
名詞解釋服務器
redis:一個高性能的k,v數據庫,基於C語言編寫;
lua:一種輕量小巧的腳本語言,用標準C語言編寫並以源代碼形式開放, 其設計目的是爲了嵌入應用程序中,從而爲應用程序提供靈活的擴展和定製功能。
需求緣起
首先說說什麼場景下須要用到lua腳本,當你想一次執行一批redis指令並且又不但願中途被其餘指令打斷的時候,也許有人說pipeline不香嗎?是的,pipeline也是一種提升性能的方法,可是它自身有兩個特色在某些場景下是沒法替代lua腳本的,其一:pipeline的執行是沒法保證原子性的;其二:pipeline多條指令之間是沒法共享上下文的,這個怎麼理解呢,好比pipeline中包括A,B兩條指令,若是B指令須要依賴A指令的執行結果,這時是沒法獲取到的,舉個簡單例子以下:
判斷key1是否等於value1,若是等於就刪除key1,不然什麼都不作。
按正常思惟這個代碼很簡單,兩行代碼搞定
if "value1".equals(jedis.get("key1") { //@1 jedis.del("key1") //@2 }
可是老司機一看就會說這個是有問題的,由於@1和@2之間有可能會插入其餘指令,好比jedis.set("key1","value2"),那怎麼解決呢,很簡單,直接一段lua腳本完事,以下:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
初識eval api
EVAL script numkeys key [key ...] arg [arg ...]
從 Redis 2.6.0 版本開始,經過內置的 Lua 解釋器,可使用 EVAL 命令對 Lua 腳本進行求值。
script 參數是一段 Lua 腳本程序,它會被運行在 Redis 服務器上下文中,這段腳本沒必要(也不該該)定義爲一個 Lua 函數。
numkeys 參數用於指定鍵名參數的個數。
鍵名參數 key [key ...] 從 EVAL 的第三個參數開始算起,表示在腳本中所用到的那些 Redis 鍵(key),這些鍵名參數能夠在 Lua 中經過全局變量 KEYS 數組,用 1 爲基址的形式訪問( KEYS[1] , KEYS[2] ,以此類推)。
在命令的最後,那些不是鍵名參數的附加參數 arg [arg ...] ,能夠在 Lua 中經過全局變量 ARGV 數組訪問,訪問的形式和 KEYS 變量相似( ARGV[1] 、 ARGV[2] ,諸如此類)。
上面這幾段長長的說明能夠用一個簡單的例子來歸納:
eval "return redis.call('set',KEYS[1],'bar')" 1 foo
執行流程初探
上面提到經過eval 指令來執行一段lua腳本,如今就來看看具體的執行流程是什麼樣的,先放一張redis執行指令的總體流程,對執行過程感興趣的能夠參考我另外一篇文章redis工做流程初探。
如今從上圖中6.1開始看起,redis根據命令執行相應的函數,eval對應的函數是evalCommand,看下evalCommand的大致流程。
這裏先放一個最簡化的流程圖,隨着過程深刻慢慢豐富這個流程。
看完這個簡化流程,我這裏先拋幾個問題出來,後面一一解答。
爲何要根據script生成functionName?
如何動態生成function?
具體是如何執行function的?
剖析
上一節拋了三個問題出來,這節將一一解答。
問題1:爲何要根據script生成functionName?
lua雖然能夠直接執行語句,可是Lua開放給C調用的接口是以函數爲單位,因此這裏須要爲script生成一個函數名稱,具體生成邏輯能夠簡單理解爲對script採用sha1算法生成的哈希串。
問題2:如何動態生成function?
首先看下lua中的函數定義格式以下:
function 方法名(參數1,參數2) return 結果 end
假設執行eval "redis.call('get','aaaa')" 0
那麼會根據如下規則生成function的字符串定義:
根據script生成functionName,值爲f_c1e0a03d7d32d0ade6850909efd61f92337847a8;
將script內容做爲函數體;
最終獲得的結果是:
function f_7c6f28e03fe1da50a15a7396fd66d0927ee4f350() redis.call('get','aaa') end
以上只是生成了function的字符串定義,真正要生成lua的函數還須要藉助Lua供的函數lua_load
int lua_load (lua_State *L, lua_Reader reader, void *data, const char *chunkname); Loads a Lua chunk. If there are no errors, lua_load pushes the compiled chunk as a Lua function on top of the stack. Otherwise, it pushes an error message. The return values of lua_load are: lua_load automatically detects whether the chunk is text or binary, and loads it accordingly (see program luac). The lua_load function uses a user-supplied reader function to read the chunk (see lua_Reader). The data argument is an opaque value passed to the reader function. The chunkname argument gives a name to the chunk, which is used for error messages and in debug information (see §3.8).
問題3:具體是如何執行function的?
經過前面的部分redis根據script已經動態生成了function,接下來就能夠調用function了,這塊是最核心的部分了。
整體來講C語言調用Lua函數時須要藉助Lua提供的lua_call接口
void lua_call (lua_State *L, int nargs, int nresults);
這個接口一共須要三個參數,各自的含義以下:
L:lua_State類型變量,用來保存執行過程當中的狀態,包括函數,參數,返回值等;
nargs:本次函數調用所須要的參數個數;
nresults:本地調用結束之後期待的返回值個數;
看到這兒仍是有一些懵逼,C究竟是怎麼調用Lua函數的呢?對於兩個異構系統的相互調用通常須要兩個條件:
存在一層適配層,這一層負責作相關的轉換,對於C和Lua互調來講,這一層由Lua底層實現,好比上面的luc_call,lua_load等等;
須要某種通訊協議來達成共識,這樣才能順暢的交流;
而剛纔提到的某種通訊協議在lua_call的接口說明中也提到了,具體以下:
首先,要調用的函數被壓入棧;而後,該函數的參數按直接順序入棧;也就是說,第一個參數先入棧。最後調用lua_call實現函數調用; nargs是您壓入堆的參數數量。調用函數時,將從棧中彈出全部參數和函數。函數返回時,函數結果將被壓入棧,函數結果以直接順序被推入堆棧(第一個結果被首先推入),所以在調用以後,最後一個結果在堆棧頂部。
經過一組圖描述下調用過程
階段1-加載函數
lua_load
階段2-函數入棧
lua_getglobal(luaState, funcname);
階段3-參數入棧
lua_pushnil、lua_pushnumber、lua_pushstring等
階段4-函數調用
lua_call(luaState, 2, 1);//調用函數,該函數接收兩個參數,最終一個返回值
階段5-獲取返回值
lua_tostring(luaState, -1)//以字符串形式返回棧頂元素,也就是返回值
綜上所述,C調用lua函數以前須要將要調用的函數,函數須要的參數入棧,最終使用lua_call來實現函數調用,調用時須要明確的指出本地調用的參數個數,返回值個數,看到這兒你可能會問,爲何還須要指出參數個數、返回值個數呢?其實這就是所謂的通訊協議,通訊載體是一個棧,棧裏面即放了函數,也放了函數參數,適配層(其實就是lua底層)如何知道函數在什麼位置呢?執行完之後該返回幾個結果呢(lua函數能夠返回多個結果,但調用者可能不須要這麼多)?這些都須要調用者明確的告訴適配層。
這裏放個C語言調用Lua的例子來幫助理解,代碼以下:
#include <lua.h> #include <lauxlib.h> #include <lualib.h> int main(void){ //定義一段lua函數 char lua_func[] = "function hello(v) return v end"; //建立luaState lua_State* L = luaL_newstate(); //加載lua_func中內容爲一個lua函數 if (luaL_loadbuffer(L, lua_func, strlen(lua_func), "@user_script")){ printf(lua_tostring(L, -1)); return -1; } lua_pcall(L, 0, LUA_MULTRET, 0); //hello函數入棧 lua_getglobal(L, "hello"); //hello函數所需參數入棧 lua_pushstring(L, "world"); //使用lua_pcall調用hello函數,告訴它須要一個參數一個返回值 if (lua_pcall(L, 1, 1, 0)){ //若是調用失敗輸出錯誤信息,錯誤信息在棧的頂部,因此用lua_tostring(L,-1) printf(lua_tostring(L, -1)); getchar(); return -1; } //沒有錯誤,輸出hello函數返回值,返回值在棧的頂部 printf(lua_tostring(L, -1)); //這個是爲了讓命令行不要退出 getchar(); return 0; }
能夠看到輸出了hello函數返回的參數值「world」
更進一步
前面的章節僅僅能算一個鋪墊,只是聊了聊C語言調用Lua函數的知識,離「redis中Lua執行原理」真相還差一截,爲何這麼說呢?
咱們依然之前面的那段lua腳本
eval "return redis.call('set',KEYS[1],'bar')" 1 foo
來展開,redis.call從字面理解對應着一次redis操做,這個操做難道是lua完成的?
redis.call('set',KEYS[1],'bar')能夠理解爲調用了redis對象(Lua語言中應該叫table)的call方法,參數分別爲'set',KEYS[1],'bar',當執行redis.call時,其最終會映射到redis源碼中的luaRedisCallCommand方法,這個映射操做是在redis啓動時scriptingInit函數完成的,跟着源碼看下這塊邏輯:
void scriptingInit(void) { //1.初始化luaState lua_State *lua = lua_open(); //2.加載一些lua庫 luaLoadLibraries(lua); luaRemoveUnsupportedFunctions(lua); //3.初始化一個空的lua table,併入棧s,這時table在棧頂,對應的index=-1 lua_newtable(lua); //4.壓字符串"call"入棧,這時"call"在棧頂,index=-1,前一步的table在棧底 //對應的index=-2 lua_pushstring(lua,"call"); //5.壓c函數luaRedisCallCommand入棧,這時"luaRedisCallCommand"在棧頂,index=-1, //前兩步壓入棧的table和"call"在棧中的index分別爲-3,-2 lua_pushcfunction(lua,luaRedisCallCommand); //6.爲table賦值,table處在-3位置,依次從棧中彈出兩個元素做爲table的 //value和key,執行table[key] = value,賦值之後的table相似於這樣的結構 //{"call":luaRedisCallCommand} //lua_settable之後棧中只剩table lua_settable(lua,-3); //7.從棧頂彈出一個元素設置爲全局變量,並命名爲redis,由於目前棧中只剩 //table,因此redis就是table lua_setglobal(lua,"redis"); }
上面這段代碼的主要做用是將redis.call這個Lua調用映射爲luaRedisCallCommand這個C調用,那接下來應該還有兩個點值得咱們關注:
參數如何傳遞給C函數的;
C函數調用完成之後如何返回結果給Lua。
前面在說C調用Lua時說過,對於兩個異構系統的相互調用通常須要兩個條件:
存在一層適配層,這一層負責作相關的轉換,對於C和Lua互調來講,這一層由Lua底層實現,好比上面的luc_call,lua_load等等;
須要某種通訊協議來達成共識,這樣才能順暢的交流;
一樣的,這兩個前提條件一樣適用於Lua調用C,轉換依然由Lua底層實現,通訊載體依然是一個棧,通訊協議雖然有一點變化,可是原理相似,具體以下:
Lua底層對存在C映射關係的lua函數調用時,好比redis.call,Lua底層會將函數參數依次壓棧,當C函數調用時從棧中獲取參數,C函數執行完成之後將返回值壓棧,C函數的返回結果爲返回值的數量,Lua底層根據函數返回值去棧中獲取必定數量的值做爲lua的返回值;
經過一組圖描述下調用過程:
階段1-c函數入棧
void lua_pushcfunction (lua_State *L, lua_CFunction f);
階段2-將C函數設置爲lua全局變量,其實就是lua調用到c調用的映射
void lua_setglobal (lua_State *L, const char *name);
階段3-函數調用
redis.call('set',KEYS[1],'bar')
lua底層會將redis.call這個lua調用的參數依次壓棧,而後觸發對應的C函數,好比luaRedisCallCommand,它會從棧中獲取參數而後執行。
階段4-C函數調用完成
lua_pushxxx(luaState,結果);//結果壓棧 return 1;//返回結果的個數
階段5-獲取C函數執行結果
這一步是由lua底層自動完成的,lua底層根據C函數的返回結果去棧中獲取相應的結果,好比返回值爲1,那就獲取棧頂元素做爲返回值,若是返回值爲2,那就獲取棧頂前兩個元素做爲返回值。
最後一塊兒來看下luaRedisGenericCommand這個C函數的源碼:
int luaRedisGenericCommand(lua_State *lua, int raise_error) { //獲取函數個數 int j, argc = lua_gettop(lua); struct redisCommand *cmd; robj **argv; redisClient *c = server.lua_client; sds reply; /* Build the arguments vector */ argv = zmalloc(sizeof(robj*)*argc); //從棧中依次獲取各參數的值 for (j = 0; j < argc; j++) { if (!lua_isstring(lua,j+1)) break; argv[j] = createStringObject((char*)lua_tostring(lua,j+1), lua_strlen(lua,j+1)); } /* Setup our fake client for command execution */ //將參數個數和參數值告訴redis client,這裏比較有意思,爲何叫 //fake client呢?正常狀況下redis client都是真實的應用程序,可是這裏是 //redis server僞造的一個redis client c->argv = argv; c->argc = argc; /* Command lookup */ //根據redis命令查找對應的c函數,argv[0]就是redis命令 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; } /* There are commands that are not allowed inside scripts. */ if (cmd->flags & REDIS_CMD_NOSCRIPT) { luaPushError(lua, "This Redis command is not allowed from scripts"); goto cleanup; } /* 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 & REDIS_CMD_WRITE) { if (server.lua_random_dirty) { luaPushError(lua, "Write commands not allowed after non deterministic commands"); goto cleanup; } else if (server.masterhost && server.repl_slave_ro && !server.loading && !(server.lua_caller->flags & REDIS_MASTER)) { luaPushError(lua, shared.roslaveerr->ptr); goto cleanup; } else if (server.stop_writes_on_bgsave_err && server.saveparamslen > 0 && server.lastbgsave_status == REDIS_ERR) { luaPushError(lua, shared.bgsaveerr->ptr); goto cleanup; } } if (cmd->flags & REDIS_CMD_RANDOM) server.lua_random_dirty = 1; if (cmd->flags & REDIS_CMD_WRITE) server.lua_write_dirty = 1; /* Run the command */ c->cmd = cmd; //調用具體的C函數 call(c,REDIS_CALL_SLOWLOG | REDIS_CALL_STATS); /* 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. */ //解析響應結果 reply = sdsempty(); if (c->bufpos) { reply = sdscatlen(reply,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); /* Sort the output array if needed, assuming it is a non-null multi bulk * reply as expected. */ if ((cmd->flags & REDIS_CMD_SORT_FOR_SCRIPT) && (reply[0] == '*' && reply[1] != '-')) { luaSortArray(lua); } sdsfree(reply); c->reply_bytes = 0; cleanup: /* Clean up. Command code may have changed argv/argc so we use the * argv/argc of the client instead of the local variables. */ for (j = 0; j < c->argc; j++) decrRefCount(c->argv[j]); zfree(c->argv); if (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. */ lua_pushstring(lua,"err"); lua_gettable(lua,-2); return lua_error(lua); } //返回結果的數量 return 1; }
總結
redis和Lua可以直接通訊得益於底層都是C實現的,關鍵在於LuaState,能夠簡單理解爲一個棧,充當了通訊的載體,其次就是通訊協議的定義,參數的傳遞、返回值的獲取、方法的調用都經過簡單的入棧、出棧操做實現。
推薦閱讀
Lua官方文檔 http://www.lua.org/manual/5.1/manual.html
Lua編程指南 http://www.lua.org/pil/24.html http://www.lua.org/pil/25.html http://www.lua.org/pil/26.html
來個人公衆號與我交流