###Lua模块热更新原理 能很好的支持代码热更新机制,是大部分选择要嵌入脚本语言的原因之一。好处很简单,脚本代码可以热更新的话,调试和线上解决问题都可以不用重启程序了,对开发效率有很大的帮助。
下面就来谈谈Lua代码如何实现热更新。
先简单回顾之前提过的模块和require机制。Lua内部提供了一个require函数,来实现模块的加载,它做的事情主要是以下几个:
- 在registry["_LOADED"]表中判断该模块是否已经加载过了,如果是则返回,避免重复加载某个模块代码。
- 依次调用注册的loader来加载模块
- 将加载过的模块赋值给registry["_LOADED"]表。
而如果要实现Lua的代码热更新,其实也就是需要重新加载某个模块,因此就要想办法让Lua认为它之前没有加载过。查看Lua代码发现,registry["_LOADED"]表,实际上对应的是package.loaded表,这在以下函数中有体现:
(loadlib.c)
627 LUALIB_API int luaopen_package (lua_State *L) {
655 /* set field `loaded' */
656 luaL_findtable(L, LUA_REGISTRYINDEX, "_LOADED", 2);
657 lua_setfield(L, -2, "loaded");
因此事情就很简单了,需要提供一个require_ex函数,可以理解为require的增强版,使用这个函数可以动态更新某个模块的代码:
function require_ex( _mname )
print( string.format("require_ex = %s", _mname) )
if package.loaded[_mname] then
print( string.format("require_ex module[%s] reload", _mname))
end
package.loaded[_mname] = nil
require( _mname )
end
这个函数做的事情一目了然。首先判断是否曾经加载过这个模块,如果有则打印一条日志表示需要重新加载某个模块,然后将该模块原来在表中注册的值赋空,然后再次调用require进行模块的加载和注册。
以上了解了Lua代码热更新的原理,但是还有一些细节需要提醒一下。
第一点,如何组织你的项目中的Lua代码?我在qnode中使用的方式是,单独使用一个叫main.lua的文件调用require_ex函数来加载需要用到的lua模块,而Lua虚拟机创建之后执行的是这个文件,这样的话,当你需要热更新项目中的Lua代码时,只需要重新执行这个main.lua就行了。如何通知热更新代码呢?我在qnode中使用的信号机制,当服务器收到USR1信号时,通知所有工作进程,由工作进程来重新对main.lua进行重新加载,这样就完成了lua代码的热更新,为此我写了一个简单的脚本reload.sh,就是根据当前qnode的服务器进程ID来对其发送USR1信号量的。
第二点,一般热更新的都是函数的实现,所以需要对全局变量做一些保护。比如当前某全局变量为100,表示某个操作已经进行了100次,它不能因为热更新重新置0,所以要对这些不能改变的全局变量做一个保护,最简单的方式就是这样:
a = a or 0
很简单的原理,只有当前a这个变量没有初始值的时候才会赋值为0,而后面不管这个Lua文件被加载多少次,a都不会因为重新加载了Lua代码发生改变了。