【第22期】观点:IT 行业加班,到底有没有价值?

Redis 与 Lua 脚本

转载 2016年08月29日 16:23:28

Lua 简介

Lua 以可嵌入,轻量,高效,提升静态语言的灵活性,有了 Lua,方便对程序进行改动或拓展,减少编译的次数,在游戏开发中特别常见。举一个在 C 语言中调用 Lua 脚本的例子:

//这是 Lua 所需的三个头文件
//当然,你需要链接到正确的 lib
extern "C"
{
    #include "lua.h"
    #include "lauxlib.h"
    #include "lualib.h"
}
int main(int argc, char *argv[])
{
    lua_State *L = lua_open();
    // 此处记住,当你使用的是 5.1 版本以上的 Lua 时,请修改以下两句为
    // luaL_openlibs(L);
    luaopen_base(L);
    luaopen_io(L);
    // 记住, 当你使用的是 5.1 版本以上的 Lua 时请使用 luaL_dostring(L,buf);
    lua_dofile("script.lua");
    lua_close(L);
    return 0;
}

lua_dofile(”script.lua”); 这一句能为我们提供无限的遐想,开发人员可以在 script.lua 脚本文件中实现程序逻辑,而不需要重新编译 main.cpp 文件。在上面给出的例子中,c 语言执行了 lua 脚本。不仅如此,我们也可以将c 函数注册到 lua 解释器中,从而在 lua 脚本中,调用 c 函数。

Redis 为什么添加 Lua 支持

从上所说,lua 为静态语言提供更多的灵活性,redis lua 脚本出现之前 Redis 是没有服务器端运算能力的,主要是用来存储,用做缓存,运算是在客户端进行,这里有两个缺点:一、如此会破坏数据的一致性,试想如果两个客户端先后获取(get)一个值,它们分别对键值做不同的修改,然后先后提交结果,最终 Redis 服务器中的结果肯定不是某一方客户端所预期的。二、浪费了数据传输的网络带宽。

lua 出现之后这一问题得到了充分的解决,非常棒!有了 Lua 的支持,客户端可以定义对键值的运算。总之,可以让 Redis 更为灵活。

Lua 环境的初始化

在 Redis 服务器初始化函数 scriptingInit() 中,初始化了 Lua 的环境。

  • 加载了常用的 Lua 库,方便在 Lua 脚本中调用
  • 创建 SHA1->lua_script 哈希表,可见 Redis 会保存客户端执行过的 Lua 脚本

SHA1 是安全散列算法产生的一个固定长度的序列,你可以把它理解为一个键值。可见 Redis 服务器会保存客户端执行过的 Lua 脚本。这在一个 Lua 脚本需要被经常执行的时候是非常有用的。试想,客户端只需要给定一个 SHA1 序列就可以执行相应的 Lua 脚本了。事实上,EVLASHA 命令就是这么工作的。

  • 注册 Redis 的一些处理函数,譬如命令处理函数,日志函数。注册过的函数,可以在 lua 脚本中调用
  • 替换已经加载的某些库的函数
  • 创建虚拟客户端(fake client)。和 AOF,RDB 数据恢复的做法一样,是为了复用命令处理函数

重点展开第三、五点。

Lua 脚本执行 Redis 命令

要在lua 脚本中调用c 函数,会有以下几个步骤:

  1. 定义下面的函数:typedef int (*lua_CFunction) (lua_State *L);
  2. 为函数取一个名字,并入栈
  3. 调用 lua_pushcfunction() 将函数指针入栈
  4. 关联步骤 2 中的函数名和步骤 3 的函数指针

在 Redis 初始化的时候,会将 luaRedisPCallCommand(), luaRedisPCallCommand() 两个函数入栈:

void scriptingInit(void) {
    ......
    // 向lua 解释器注册redis 的数据或者变量
    /* Register the redis commands table and fields */
    lua_newtable(lua);
    // 注册redis.call 函数,命令处理函数
    /* redis.call */
    // 将"call" 入栈,作为key
    lua_pushstring(lua,"call");
    // 将luaRedisPCallCommand() 函数指针入栈,作为value
    lua_pushcfunction(lua,luaRedisCallCommand);
    // 弹出"call",luaRedisPCallCommand() 函数指针,即key-value,
    // 并在table 中设置key-values
    lua_settable(lua,-3);
    // 注册redis.pall 函数,命令处理函数
    /* redis.pcall */
    // 将"pcall" 入栈,作为key
    lua_pushstring(lua,"pcall");
    // 将luaRedisPCallCommand() 函数指针入栈,作为value
    lua_pushcfunction(lua,luaRedisPCallCommand);
    // 弹出"pcall",luaRedisPCallCommand() 函数指针,即key-value,
    // 并在table 中设置key-values
    lua_settable(lua,-3);
    ......
}

经注册后,开发人员可在 Lua 脚本中调用这两个函数,从而在 Lua 脚本也可以执行 Redis 命令,譬如在脚本删除某个键值对。以 luaRedisCallCommand() 为例,当它被回调的时候会完成:

  1. 检测参数的有效性,并通过 lua api 提取参数
  2. 向虚拟客户端 server.lua_client 填充参数
  3. 查找命令
  4. 执行命令
  5. 处理命令处理结果

fake client 的好处又一次体现出来了,这和 AOF 的恢复数据过程如出一辙。在 lua 脚本处理期间,Redis 服务器只服务于 fake client。

Redis Lua 脚本的执行过程

我们依旧从客户端发送一个 lua 相关命令开始。假定用户发送了 EVAL 命令如下:

eval 1 "set KEY[1] ARGV[1]" views 18000

此命令的意图是,将 views 的值设置为 18000。Redis 服务器收到此命令后,会调用对应的命令处理函数evalCommand() 如下:

void evalCommand(redisClient *c) {
    evalGenericCommand(c,0);
}
void evalGenericCommand(redisClient *c, int evalsha) {
    lua_State *lua = server.lua;
    char funcname[43];
    long long numkeys;
    int delhook = 0, err;
    // 随机数的种子,在产生哈希值的时候会用到
    redisSrand48(0);
    // 关于脏命令的标记
    server.lua_random_dirty = 0;
    server.lua_write_dirty = 0;
    // 检查参数的有效性
    if (getLongLongFromObjectOrReply(c,c->argv[2],&numkeys,NULL) != REDIS_OK)
        return;
    if (numkeys > (c->argc - 3)) {
        addReplyError(c,"Number of keys can't be greater than number of args");
        return;
    }
    // 函数名以f_ 开头
    funcname[0] = 'f';
    funcname[1] = '_';
    // 如果没有哈希值,需要计算lua 脚本的哈希值
    if (!evalsha) {
        // 计算哈希值,会放入到SHA1 -> lua_script 哈希表中
        // c->argv[1]->ptr 是用户指定的lua 脚本
        // sha1hex() 产生的哈希值存在funcname 中
        sha1hex(funcname+2,c->argv[1]->ptr,sdslen(c->argv[1]->ptr));
    } else {
        // 用户自己指定了哈希值
        int j;
        char *sha = c->argv[1]->ptr;
    for (j = 0; j < 40; j++)
        funcname[j+2] = tolower(sha[j]);
        funcname[42] = '\0';
    }
    // 将错误处理函数入栈
    // lua_getglobal() 会将读取指定的全局变量,且将其入栈
    lua_getglobal(lua, "__redis__err__handler");
    /* Try to lookup the Lua function */
    // 在lua 中查找是否注册了此函数。这一句尝试将funcname 入栈
    lua_getglobal(lua, funcname);
    if (lua_isnil(lua,-1)) { // funcname 在lua 中不存在
        // 将nil 出栈
        lua_pop(lua,1); /* remove the nil from the stack */
        // 已经确定funcname 在lua 中没有定义,需要创建
    if (evalsha) {
        lua_pop(lua,1); /* remove the error handler from the stack. */
        addReply(c, shared.noscripterr);
        return;
    }
    // 创建lua 函数funcname
    // c->argv[1] 指向用户指定的lua 脚本
    if (luaCreateFunction(c,lua,funcname,c->argv[1]) == REDIS_ERR) {
        lua_pop(lua,1);
        return;
    }
    // 现在lua 中已经有funcname 这个全局变量了,将其读取并入栈,
    // 准备调用
    lua_getglobal(lua, funcname);
    redisAssert(!lua_isnil(lua,-1));
    }
    // 设置参数,包括键和值
    luaSetGlobalArray(lua,"KEYS",c->argv+3,numkeys);
    luaSetGlobalArray(lua,"ARGV",c->argv+3+numkeys,c->argc-3-numkeys);
    // 选择数据集,lua_client 有专用的数据集
    /* Select the right DB in the context of the Lua client */
    selectDb(server.lua_client,c->db->id);
    // 设置超时回调函数,以在lua 脚本执行过长时间的时候停止脚本的运行
    server.lua_caller = c;
    server.lua_time_start = ustime()/1000;
    server.lua_kill = 0;
    if (server.lua_time_limit > 0 && server.masterhost == NULL) {
        // 当lua 解释器执行了100000,luaMaskCountHook() 会被调用
        lua_sethook(lua,luaMaskCountHook,LUA_MASKCOUNT,100000);
        delhook = 1;
    }
    // 现在,我们确定函数已经注册成功了. 可以直接调用lua 脚本
    err = lua_pcall(lua,0,1,-2);
    // 删除超时回调函数
    if (delhook) lua_sethook(lua,luaMaskCountHook,0,0); /* Disable hook */
        // 如果已经超时了,说明lua 脚本已在超时后背SCRPIT KILL 终结了
        // 恢复监听发送lua 脚本命令的客户端
    if (server.lua_timedout) {
        server.lua_timedout = 0;
        aeCreateFileEvent(server.el,c->fd,AE_READABLE,
        readQueryFromClient,c);
    }
    // lua_caller 置空
    server.lua_caller = NULL;
    // 执行lua 脚本用的是lua 脚本执行专用的数据集。现在恢复原有的数据集
    selectDb(c,server.lua_client->db->id); /* set DB ID from Lua client */
    // Garbage collection 垃圾回收
    lua_gc(lua,LUA_GCSTEP,1);
    // 处理执行lua 脚本的错误
    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. */
    }
    // 将lua 脚本发布到主从复制上,并写入AOF 文件
    ......
}

对应 lua 脚本的执行流程图:

脏命令

在解释脏命令之前,先交代一点。

Redis 服务器执行的 Lua 脚本和普通的命令一样,都是会写入 AOF 文件和发布至主从复制连接上的。以主从复制为例,将 Lua 脚本中发生的数据变更发布到从机上,有两种方法。一,和普通的命令一样,只要涉及写的操作,都发布到从机上;二、直接将 Lua 脚本发送给从机。实际上,两种方法都可以的,数据变更都能得到传播,但首先,第一种方法中普通命令会被转化为 Redis 通信协议的格式,和 Lua 脚本文本大小比较起来,会浪费更多的带宽;其次,第一种方法也会浪费较多的 CPU 的资源,因为从机收到了 Redis 通信协议的格式的命令后,还需要转换为普通的命令,然后才是执行,这比纯粹的执行 lua 脚本,会浪费更多的 CPU 资源。明显,第二种方法是更好的。这一点 Redis 做的比较细致。

上面的结果是,直接将 Lua 脚本发送给从机。但这会产生一个问题。举例一个 Lua 脚本:

-- lua scrpit
local some_key
some_key = redis.call('RANDOMKEY') -- <--- TODO nil
redis.call('set',some_key,'123')

上面脚本想要做的是,从 Redis 服务器中随机选取一个键,将其值设置为 123。从 RANDOMKEY 命令的命令处理函数来看,其调用了 random() 函数,如此一来问题就来了:当 lua 脚本被发布到不同的从机上时,random() 调用返回的结果是不同的,因此主从机的数据就不一致了。

因此在 Redis 服务器配置选项目设置了两个变量来解决这个问题:

// 在lua 脚本中发生了写操作
int lua_write_dirty; /* True if a write command was called during the
execution of the current script. */
// 在lua 脚本发生了未决的操作,譬如RANDOMKEY 命令操作
int lua_random_dirty; /* True if a random command was called during the
execution of the current script. */

在执行 Lua 脚本之前,这两个参数会被置零。在执行 Lua 脚本中,执行命令操作之前,Redis 会检测写操作之前是否执行了 RANDOMKEY 命令,是则会禁止接下来的写操作,因为未决的操作会被传播到从机上;否则会尝试更新上面两个变量,如果发现写操作 lua_write_dirty = 1;如果发现未决操作,lua_random_dirty = 1。对于这段话的表述,有下面的流程图,大家也可以翻阅 luaRedisGenericCommand() 这个函数:

Lua 脚本的传播

如上所说,需要传播 Lua 脚本中的数据变更,Redis 的做法是直接将 lua 脚本发送给从机和写入 AOF 文件的。

Redis 的做法是,修改执行 Lua 脚本客户端的参数为“EVAL”和相应的lua 脚本文本,至于发送到从机和写入 AOF 文件,交由主从复制机制和 AOF 持久化机制来完成。下面摘一段代码:

void evalGenericCommand(redisClient *c, int evalsha) {
    ......
    if (evalsha) {
    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. */
    // 从server.lua_scripts 获取lua 脚本
    // c->argv[1]->ptr 是SHA1
    robj *script = dictFetchValue(server.lua_scripts,c->argv[1]->ptr);
    // 添加到主从复制专用的脚本缓存中
    replicationScriptCacheAdd(c->argv[1]->ptr);
    redisAssertWithInfo(c,NULL,script != NULL);
    // 重写命令
    // 参数1 为:EVAL
    // 参数2 为:lua_script
    // 如此一来在执行AOF 持久化和主从复制的时候,lua 脚本就能得到传播
    rewriteClientCommandArgument(c,0,
        resetRefCount(createStringObject("EVAL",4)));
    rewriteClientCommandArgument(c,1,script);
    }
  }
}

总结

Redis 服务器的工作模式是单进程单线程,因为开发人员在写 Lua 脚本的时候应该特别注意时间复杂度的问题,不要让 Lua 脚本影响整个 Redis 服务器的性能。

举报

相关文章推荐

Lua语言模型 与 Redis应用

从 2.6版本 起, Redis 开始支持 Lua 脚本 让开发者自己扩展 Redis. 本篇博客主要介绍了 Lua 语言不一样的设计模型(相比于Java/C/C++、JS、PHP), 以及 Red...

Lua 脚本 Lua 脚本功能是 Reids 2.6 版本的最大亮点, 通过内嵌对 Lua 环境的支持, Redis 解决了长久以来不能高效地处理 CAS (check-and-set)命令的缺点,

Lua 脚本 Lua 脚本功能是 Reids 2.6 版本的最大亮点, 通过内嵌对 Lua 环境的支持, Redis 解决了长久以来不能高效地处理 CAS (check-and-set)命令的缺点,...

欢迎关注CSDN程序人生公众号

关注程序员生活,汇聚开发轶事。

调用Lua 脚本

<a id="引言" style="color: #333333;" name="%E5%BC%9

Redis系列学习笔记13 Lua 脚本

Lua 脚本在服务器端执行复杂的操作尽管使用流水线可以一次发送多个命令,但是对于一个由多个命令组成的复杂操作来说,为了执行该操作而不断地重复发送相同的命令,这并不是最高效的做法,会对网络资源造成浪费。...

vision引擎中 为图形添加 Lua 脚本 介绍

引言 <div style="color: #444444; font-family: Tahoma,

PHP使用Redis+Lua脚本操作的注意事项

以前只是简单的用下 Reids 存点数据而已,最近尝试优化性能,做了些测试才发现很多以前完全忽略的问题,总结在下面:一、Redis的一般新手注意事项: 连接本地Reids时,host 要填写 127....

REDIS LUA脚本使用经验分享

redis lua脚本出现之前redis是没有服务器端运算能力的,主要是用来存储,用做缓存用,运算是在客户端进行,这样带来了很大的带宽流量。lua出现之后这一问题得到了充分的解决,非常棒! redis lua脚本api介绍 eval 在redis服务器端执行lur脚本 evalsha 在redis 以脚本的sha1签名值在服务器端执行lua 脚本 script exists 判断脚本是否存在 script flush 释放lur脚本的缓存 script load 以sha1签名值做为key保存脚本 script kill 杀死当前执行的肢本 参考地址htt

为什么在 Redis 实现 Lua 脚本事务?

在刚过去的几个月中,我一直在构思并尝试在 redis 中实现 lua 脚本的事务功能。没有多少人理解我的想法,所以我将通过一些历史为大家做下解释。 MySQL 与 Postgres 在 1...

Havork 为图形添加 Lua 脚本

<table class="vwtb" style="border-collapse: collapse; table-layout: fixed; width: 619px; height: 300px; color: #444444; font-family: Tahoma, 'Microsoft Yahei', Simsun; font-size: 12px; line-height: 18px;" cellspacing="0" cellpadding="

Redis 2.6 Lua脚本功能实现分析

通过对 Redis 源码中的 scripting.c 文件进行分析,解释 Lua 脚本功能的实现机制。   预备知识   因为脚本功能的实现源码和命令关系密切,最好在阅读这篇文章之前先了解 Red...
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)