这是一个非常棒的实战问题。理解 Lua 脚本在 OpenResty 中的加载机制、生命周期和变量作用域,是避免内存泄漏和数据污染(比如 A 用户看到 B 用户的数据)的关键。
1. 脚本是如何寻找的?(require 机制)
在 OpenResty 中,require 函数的行为和标准 Lua 是一样的,它依赖于 package.path 来寻找文件。但是,在 Nginx 环境下,我们需要通过 Nginx 配置文件来设置这个路径。
配置指令:lua_package_path
你需要在 nginx.conf 的 http {} 块中配置它:
http {
# ?.lua 表示用 require 的模块名替换问号
# ;; 表示保留 OpenResty 默认的搜索路径(强烈建议加上)
lua_package_path "/path/to/your/project/lib/?.lua;/path/to/your/project/app/?.lua;;";
server {
...
}
}
寻找流程:
假设你写了 local tool = require("utils.common"),且配置如上:
- Lua 会尝试查找:
/path/to/your/project/lib/utils/common.lua - 如果没找到,尝试:
/path/to/your/project/app/utils/common.lua - 如果还没找到,去 OpenResty 默认路径(如
/usr/local/openresty/lualib/...)查找。
项目组织建议:
通常我们会这样组织目录:
my_project/
├── conf/
│ └── nginx.conf
├── logs/
└── src/
├── access.lua (入口脚本)
├── content.lua
└── modules/ (放可以用 require 复用的模块)
├── redis_tool.lua
└── user_logic.lua
在 nginx.conf 里指向 src/modules/?.lua 即可。
2. 脚本是一次性加载的吗?(代码缓存)
答案取决于 lua_code_cache 开关。
生产环境:lua_code_cache on; (默认是开启的)
- 机制:当 Nginx Worker 启动或第一次处理请求时,它会把
.lua文件加载到内存,并编译成 LuaJIT Bytecode(字节码)。 - 结果:脚本只加载一次。
- require 的行为:Lua 有一个全局表
package.loaded。当第一次require("module")时,模块代码执行,返回值被存在package.loaded["module"]中。第二次 require 时,直接返回表里的缓存,不会再次读取磁盘文件,也不会再次运行模块里的代码。 - 性能:极高。
- 坑:如果你修改了 Lua 文件,必须
reload或重启 Nginx 才会生效。
开发环境:lua_code_cache off;
- 机制:完全禁用缓存。
- 结果:每一个请求进来,OpenResty 都会重新创建一个全新的 Lua VM(或者清理当前 VM),并重新读取磁盘上的文件。
- 性能:很差(不能用于生产)。
- 优点:改了代码刷新浏览器立刻生效,调试方便。
3. 变量的生命周期与可见性(核心考点)
这是新手最容易踩坑的地方(导致数据串号)。我们需要区分三种变量:全局变量、模块级变量、函数级局部变量。
前提:Nginx 是多进程模型,每个 Worker 进程有一个独立的 Lua VM。Worker 之间的变量是隔离的,但在同一个 Worker 内部处理的所有请求是共享这个 Lua VM 的。
A. 全局变量(千万别用!)
如果你在脚本里这样写(没有 local):
-- 这是一个全局变量,挂载在 _G 表下
current_user_id = 1001
- 后果:因为
lua_code_cache on,Lua VM 是常驻内存的。 - 场景:
- 请求 A 进来,执行
current_user_id = 1001。 - 请求 B 进来(同一个 Worker 处理),由于代码逻辑分支没走到赋值语句,直接读取
current_user_id。 - 恐怖的事情发生了:请求 B 读到了 1001!
- 请求 A 进来,执行
- 结论:严禁使用全局变量。OpenResty 默认开启了
lua-releng工具或编译选项来检测并报错全局变量的使用。
B. 模块级变量(Upvalue)
这是指在模块文件中,写在函数外面的 local 变量。
-- src/modules/counter.lua
local _M = {}
-- 这是一个模块级局部变量
local count = 0
function _M.add()
count = count + 1
return count
end
return _M
- 生命周期:Worker 级别。因为
require只执行一次,这个count变量会一直存在于内存中,直到 Nginx 重启。 - 可见性:同一个 Worker 处理的所有请求,都会共享这个
count。 - 效果:
- 请求 A 调
add()-> count 变成 1 - 请求 B 调
add()-> count 变成 2
- 请求 A 调
- 用途:适合做只读配置缓存、连接池对象等。不适合存请求相关的数据(如用户 ID),否则也是数据污染。
C. 函数级局部变量(推荐)
-- content_by_lua
local function handle()
-- 这是一个函数级局部变量
local user_id = ngx.var.arg_uid
ngx.say(user_id)
end
handle()
- 生命周期:Request 级别。函数执行完,变量就会离开作用域被 GC(垃圾回收)。
- 可见性:只有当前请求可见。
- 结论:这是保存业务数据唯一正确的地方。
总结与最佳实践
- 加载机制:生产环境开启
lua_code_cache on,Lua 脚本被编译并常驻内存,require的模块只初始化一次。 - 变量作用域原则:
- 所有变量必须加
local。 - 请求级数据(User ID, 参数):必须定义在函数内部(
local var)。 - 跨请求共享数据(Worker 级):可以用模块顶层的
local var(慎用,注意并发竞争,虽然 Lua 是单线程执行,但逻辑上可能混淆)。 - 跨 Worker 共享数据:必须使用
lua_shared_dict(共享内存)或 Redis。
- 所有变量必须加
- 如何在请求的不同阶段传值?
- 比如在
access阶段解析了 Token,想传给content阶段。 - 不要用模块变量。
- 使用
ngx.ctx:这是 OpenResty 专门提供的表,生命周期等同于当前请求。
-- access_by_lua ngx.ctx.user_id = 123 -- content_by_lua ngx.say(ngx.ctx.user_id) -- 输出 123 - 比如在
理解了**"Worker 进程长久存活,Lua VM 不销毁"**这一点,你就理解了 OpenResty 变量管理的精髓。