2020-12-23

名词解释

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],诸如此类)。

上面这几段长长的说明可以用一个简单的例子来概括:

1eval "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内容作为函数体;

最终得到的结果是:

1function 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接口

1void 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-加载函数

1lua_load

阶段2-函数入栈

1lua_getglobal(luaState, funcname);

阶段3-参数入栈

1lua_pushnil、lua_pushnumber、lua_pushstring等

阶段4-函数调用

1lua_call(luaState, 2, 1);//调用函数,该函数接收两个参数,最终一个返回值

阶段5-获取返回值

1lua_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脚本

1eval "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函数入栈

1void lua_pushcfunction (lua_State *L, lua_CFunction f);

阶段2-将C函数设置为lua全局变量,其实就是lua调用到c调用的映射

1void lua_setglobal (lua_State *L, const char *name);

阶段3-函数调用

1redis.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;

}

深圳网站建设www.sz886.com

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容