redis源码学习之lua执行原理
聊聊redis执行lua原理
从一次面试场景说起
“看你简历上写的精通redis”
“额,还可以啦”
“那你说说redis执行lua脚本的原理”
“这个,这个,不就是那么执行的吗,eval 一段lua脚本就行了”
“好的,了解了,今天面试先到这个吧,后续有消息会通知你”
“好的,祝您生活愉快”
面试场景纯属娱乐,但这个面试题确实是笔者真实遇到过的,今天我们就来看看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
redis源码学习之lua执行原理的更多相关文章
- Redis源码学习:Lua脚本
Redis源码学习:Lua脚本 1.Sublime Text配置 我是在Win7下,用Sublime Text + Cygwin开发的,配置方法请参考<Sublime Text 3下C/C++开 ...
- redis源码学习之slowlog
目录 背景 环境说明 redis执行命令流程 记录slowlog源码分析 制造一条slowlog slowlog分析 1.slowlog如何开启 2.slowlog数量限制 3.slowlog中的耗时 ...
- Redis源码学习:字符串
Redis源码学习:字符串 1.初识SDS 1.1 SDS定义 Redis定义了一个叫做sdshdr(SDS or simple dynamic string)的数据结构.SDS不仅用于 保存字符串, ...
- 柔性数组(Redis源码学习)
柔性数组(Redis源码学习) 1. 问题背景 在阅读Redis源码中的字符串有如下结构,在sizeof(struct sdshdr)得到结果为8,在后续内存申请和计算中也用到.其实在工作中有遇到过这 ...
- __sync_fetch_and_add函数(Redis源码学习)
__sync_fetch_and_add函数(Redis源码学习) 在学习redis-3.0源码中的sds文件时,看到里面有如下的C代码,之前从未接触过,所以为了全面学习redis源码,追根溯源,学习 ...
- redis源码学习之工作流程初探
目录 背景 环境准备 下载redis源码 下载Visual Studio Visual Studio打开redis源码 启动过程分析 调用关系图 事件循环分析 工作模型 代码分析 动画演示 网络模块 ...
- 【Vue源码学习】响应式原理探秘
最近准备开启Vue的源码学习,并且每一个Vue的重要知识点都会记录下来.我们知道Vue的核心理念是数据驱动视图,所有操作都只需要在数据层做处理,不必关心视图层的操作.这里先来学习Vue的响应式原理,V ...
- [Java多线程]-线程池的基本使用和部分源码解析(创建,执行原理)
前面的文章:多线程爬坑之路-学习多线程需要来了解哪些东西?(concurrent并发包的数据结构和线程池,Locks锁,Atomic原子类) 多线程爬坑之路-Thread和Runable源码解析 多线 ...
- SpringBoot源码学习系列之启动原理简介
本博客通过debug方式简单跟一下Springboot application启动的源码,Springboot的启动源码是比较复杂的,本博客只是简单梳理一下源码,浅析其原理 为了方便跟源码,先找个Ap ...
随机推荐
- 【PYTEST】第四章Fixture
知识点: 利用fixture共享数据 conftest.py共享fixture 使用多个fixture fixture作用范围 usefixture 重命名 1. 利用fixture共享数据 test ...
- npm,pm2等相关知识的学习
现在开始接手node端测试,有好多知识点,比如启动进程的命令,查看进程的命令都不是很清晰,现在具体来学习下- npm由来 前端最大的社区是GitHub,大家在这里分享代码,讨论问题,收集学习资源.大家 ...
- Java基础教程——二维数组
二维数组 Java里的二维数组其实是数组的数组,即每个数组元素都是一个数组. 每个数组的长度不要求一致,但最好一致. // 同样有两种风格的定义方法 int[][] _arr21_推荐 = { { 1 ...
- 以注解的方式实现api和provider
1.provider import com.alibaba.dubbo.config.annotation.Service; import facade.EchoService; import com ...
- MySQL中的事务原理和锁机制
本文主要总结 MySQL 事务几种隔离级别的实现和其中锁的使用情况. 在开始前先简单回顾事务几种隔离级别以及带来的问题. 四种隔离级别:读未提交.读已提交.可重复读.可串行化. 带来的问题:脏读.不可 ...
- NTML
NTLM: 1.客户端向服务器发送一个请求,请求中包含明文的登陆用户名.在服务器中已经存储了登陆用户名和对应的密码hash 2.服务器接收到请求后,NTLMv2协议下 ...
- 基于java实现的简单网页日历功能,有兴趣得可以把它转换到前端实现
之前做项目的时候,因为要用到不同日期显示不同的内容,就自己做了一个日期的显示和选择功能,今天抽空把以前的代码理了一下,顺便就把之前做的日期功能给拿出来回顾一下,大家可以提点意见,帮忙完善下设计.先上一 ...
- celery使用-win10和linux
win10启动方式 celery -A celery_tasks.main worker -l debug -P eventlet linux启动方式 /usr/local/bin/celery ce ...
- 《Machine Learning in Action》—— Taoye给你讲讲Logistic回归是咋回事
在手撕机器学习系列文章的上一篇,我们详细讲解了线性回归的问题,并且最后通过梯度下降算法拟合了一条直线,从而使得这条直线尽可能的切合数据样本集,已到达模型损失值最小的目的. 在本篇文章中,我们主要是手撕 ...
- 【面试题】GC Root都有哪些?
那天去面试,面试官问我JVM垃圾回收,我是有备而来,上来就是一个可达性分析算法,然后就是一个复制算法,标记-清理,标记-整理,以及几个常见的垃圾回收器 详情见:https://www.cnblogs. ...