Redis 与 Lua 脚本

2023-11-02

这篇文章,主要是讲 Redis 和 Lua 是如何协同工作的以及 Redis 如何管理 Lua 脚本。

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 服务器的性能。


FROM: http://wiki.jikexueyuan.com/project/redis/lua.html

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Redis 与 Lua 脚本 的相关文章

随机推荐

  • SRTM数据介绍与下载

    本文主要介绍SRTM DEM 数据及下载过程 文章目录 一 SRTM数据介绍 二 数据下载 三 数据处理 一 SRTM数据介绍 DEM是对遥感影像进行处理获得的栅格数字高程模型 只有一个波段 且是遥感影像中合成的波段 SRTM 航天飞机雷达
  • 外包项目服务器被黑了,蓝盟经验谈,安全经验谈之网络服务器如何防止被黑

    1 对数据库进行安全配置 例如你的程序连接数据库所使用的帐户 口令 权限 如果是浏览新闻的 用只读权限即可可以对不同的模块使用不同的帐户 权限 另外 数据库的哪些存储过程可以调用 也要进行严格地配置 用不到的全部禁用 特别是cmd这种 防止
  • @Autowired一个接口有多个实现类

    Autowired是spring的注解 默认使用的是byType的方式向Bean里面注入相应的Bean 例如 Autowired private UserService userService 这段代码会在初始化的时候 在spring容器中
  • 第四季新星计划即将开启,博客之星取消拉票你怎么看?

    catalogue 写在前面 线下创机遇 新星计划 做导师可以得到什么 新星计划跟原力计划有何不同 博客之星新玩法你怎么看 写在前面 哈喽 大家好 我是几何心凉 这是一份全新的专栏 得到CSDN王总的授权 来对于我们每周四的绿萝时间 直达C
  • JAVA的HttpClient问题:The server failed to respond with a valid HTTP response

    Caused by org apache http ProtocolException The server failed to respond with a valid HTTP response 昨天帐前 卒使用java 的HttpCl
  • 微信js-sdk 分享配置报错config:invalid signature?

    缘起 最近做H5关于微信分享的需求的时候 遇到了大坑 由于没使用过js sdk做微信分享 故查阅了很多资料 大多数博主的代码偏差都不大 于是我借鉴了核心写法 我的代码 wechatApi wechatConfigInit 是封装的微信js
  • 【Linux】shell编程之循环语句

    提示 文章写完后 目录可以自动生成 如何生成可参考右边的帮助文档 文章目录 一 循环语句 二 for循环语句 1 for 语句的结构 2 for语句应用示例 三 while 循环语句 1 while 循环语句结构 2 while语句应用示例
  • Hadoop的Nodes of the cluster界面MemoryTotal为0B

    1 hadoop集群安装部署结束 一键启动 start all sh 2 查看各主从机器上jps进程 jps master ResourceManager NameNode slave1 第二namenode DataNode Second
  • 2021-11-17 尤破金11.19国际黄金今日走势分析及行情预测,黄金原油操作建议

    黄金最新行情分析 黄金消息面解析 周四 11月18日 国际金价冲高回落 因美元指数探底回升 但人们对通货膨胀加剧的担忧不断升温 各国央行最终需要加息以遏制通胀上升的前景 限制了金价下行空间 但金价距离本周创下的6月14日以来高点1877 1
  • SPI、I2C、I2S、UART、GPIO、SDIO、CAN、JTAG的区别及使用方法。

    SPI 全称及由来 SPI接口的全称是 Serial Peripheral Interface 意为串行外围接口 是Motorola首先在其MC68HCXX系列处理器上定义的 使用方法 SPI接口主要应用在EEPROM FLASH 实时时钟
  • 2019大厂Android高级工程师面试题整理+进阶资料

    金三银四 很多同学心里大概都准备着年后找工作或者跳槽 最近有很多同学都在交流群里求大厂面试题 正好我电脑里面有这方面的整理 于是就发上来分享给大家 这些题目是网友去百度 小米 乐视 美团 58 猎豹 360 新浪 搜狐等一线互联网公司面试被
  • 计算机系统 实验四(课程实验LAB四)

    实验中需要的几个控制语句 u userid 使用这个语句是要确保不同的人使用不同的 ID 做题 并攻击不同的地址 h 用于打印这几个操作的内容 n 用于 Level4 关卡 s 用于提交你的解决方案到服务器中 1 根据makecookie生
  • mysql ERROR 1045 (28000): Access denied for user ‘ODBC‘@‘localhost‘ (using password: YES)

    遇到这个问题搞了很久 自己记下来 方法是百度的 亲测有效 ERROR 1045 28000 Access denied for user ODBC localhost using password NO ERROR 1045 28000 A
  • 按照lockattribute来划分MESH

    网格模型基础 网格模型 一 定义 二 子集和属性缓存 2 0 子集 2 1 属性 2 2 操作 三 邻接信息 四 属性表 五 优化 六 网格的创建与绘制 6 1 创建 6 2 绘制 一 定义 网格模型是一种将物体的顶点数据 纹理 材质等信息
  • Linux内核scripts/Makefile.build文件结构

    1 默认目标 build 2 初始化obj y obj m等变量 3 include include config auto conf 内含CONFIG RING BUFFER y等变量列表 4 include scripts Kbuild
  • vue+element动态设置el-menu导航,刷新页面保持当前菜单选中项及路由

    今天闲来无事整理了一套后台管理系统的侧边栏菜单 实现了页面刷新路由保持不变和菜单也是当前点击的高亮状态 来一起看看吧 首先 菜单数据是动态的 注意的是 id 和 路由的 name保持一致 页面刷新要用到 一级菜单不用name 因为没用到路由
  • Android开机自启动添加

    1 添加需要自启动的可以执行文件 1 可执行C文件 system core init start needInitStartService c 例如 include
  • 基于大数据的python爬虫的菜谱美食食物推荐系统

    众所周知 现阶段我们正处于一个 大数据 时代 从互联网上大量的数据中找到自己想要的信息变得越来困难 搜索引擎的商业化给市场带来了百度和谷歌这样的商业公司 网络爬虫便是搜索引擎的重要组成部分 本课题是基于Python设计的面向下厨房网站的网络
  • edge浏览器打开多个网页卡顿解决办法

    edge有时候打开了十几个页面就大量占据内存了 卡的不行 上网汇总了解决方法 具体参考以下两篇文章 一个是通过edge浏览器自身的设置修改 一个是关闭gpu相关的图形加速插件 按照以下两篇文章的方法基本就不会卡了 1 解决win10系统ed
  • Redis 与 Lua 脚本

    这篇文章 主要是讲 Redis 和 Lua 是如何协同工作的以及 Redis 如何管理 Lua 脚本 Lua 简介 Lua 以可嵌入 轻量 高效 提升静态语言的灵活性 有了 Lua 方便对程序进行改动或拓展 减少编译的次数 在游戏开发中特别