table是Lua里唯一的数据结构类型,数组、字典、对象、模块统统靠它来承载。在OpenResty开发中,table的使用频率更是高得惊人:解析请求参数、组装响应体、共享配置、缓存数据,处处都是table的身影。但很多初学者带着其他语言的思维来用table,经常踩到#table取长度不准、table.insert性能低下、table无法被GC回收之类的坑。这篇文章系统梳理OpenResty中Lua Table库的基础知识与操作要点,并集中解答几个最常见的疑问,帮你把地基打牢。

一、先弄懂table的双重身份:数组与哈希
table在底层由两部分组成:数组部分和哈希部分。当key是从1开始连续的整数时,数据存放在数组部分,访问效率最高;一旦key是字符串、或者整数key不连续(比如只有1和3),数据就会落到哈希部分,查找需要经过哈希计算,开销略高。理解这一点非常重要,因为许多性能问题都源于无意识地把本该是数组的数据塞进了哈希部分。
举例来说,t[1]=10; t[2]=20; t[3]=30是标准的数组用法;而t[1]=10; t[3]=30虽然看起来也是数组,实际上1和3都会进入哈希部分。因此写代码时应尽量保持整数下标从1连续递增,不要中途留空洞。Lua的下标从1开始而不是0,这也是从其他语言转过来的开发者最容易犯的低级错误,遍历时务必注意。
当table作为字典使用时,key可以是字符串、数字、布尔值甚至table和函数。需要注意NaN和nil不能作为key。在OpenResty中,我们常把ngx.req.get_uri_args返回的结果当作字典来处理,此时的key是字符串形式,取值时记得类型转换,不要拿数字和字符串key混淆。
二、Table库常用API与操作要点
1. 插入与删除:insert和remove
table.insert(t, value)在尾部追加元素,table.insert(t, pos, value)在指定位置插入,table.remove(t, pos)删除并返回指定位置元素。要特别注意,insert和remove内部都会涉及元素搬移,在中间位置操作的复杂度是O(n)。如果在热路径代码里频繁在中间插入删除,性能会明显下降。
local t = {1, 2, 3}
table.insert(t, 4) -- 尾部追加,得到 {1,2,3,4}
table.insert(t, 1, 0) -- 头部插入,得到 {0,1,2,3,4},后面元素全部后移
local v = table.remove(t) -- 移除并返回最后一个元素
2. 拼接:concat
table.concat(t, sep)把数组形式的table按分隔符拼成字符串。在OpenResty里拼接响应内容时,强烈建议用concat而不是循环里做字符串连接。因为Lua的字符串是不可变的,循环中用s = s .. x会产生大量临时字符串对象,而concat一次完成拼接,效率差距在高并发场景下非常可观。
local parts = {}
for i = 1, 100 do
parts[i] = "item" .. i
end
local result = table.concat(parts, ",") -- 推荐做法
3. 解包:unpack
table.unpack(t)把数组展开成多个返回值,常用于函数调用转发参数。注意unpack只处理数组部分,遇到nil会停止展开。在Lua 5.1中它写作unpack,Lua 5.2之后移到了table.unpack,OpenResty使用的LuaJIT兼容两种写法,但建议统一用table.unpack以保持清晰。
4. 长度操作符的坑
#t只对数组部分有意义,且当数组存在nil空洞时结果不确定。比如t = {1, nil, 3},#t可能是1也可能是3,取决于底层实现。因此如果业务需要记录元素个数,最稳妥的方式是自己维护一个计数器,或者在遍历时统计,绝不依赖#t去操作含有nil的table。
三、OpenResty场景下的常见疑问解答
疑问1:为什么我的table没有被垃圾回收?
最常见的原因是循环引用配合了__gc的误用,或者table被长期存活的模块级变量持有。在OpenResty中,每个worker进程内的module级别table在进程生命周期内一直存在,如果把请求相关的数据缓存到模块级table里而不清理,数据会越积越多,最终造成内存上涨甚至OOM。正确的做法是:请求级数据放在ngx.ctx或函数局部变量中,随请求结束自动释放;跨请求共享的数据放到lua_shared_dict或使用lua-resty-lrucache,它们都有容量控制机制。
local _M = {}
local cache = {} -- 模块级table,进程存活期间不释放
function _M.handle()
-- 错误示范:请求数据塞进模块级table
-- cache[ngx.worker.pid()] = ngx.req.get_uri_args()
-- 正确示范:请求级数据放局部变量
local args = ngx.req.get_uri_args()
return args
end
return _M
疑问2:要不要复用table来提升性能?
答案是有条件地复用。LuaJIT中新建table本身并不昂贵,但如果某段热路径代码每秒创建成千上万的中型table,复用确实能减少GC压力。做法是在请求入口处创建table,请求内各阶段复用,请求结束后直接丢弃引用让GC回收。注意不要跨请求复用table,也不要自己手写对象池放到模块级变量里,除非你非常清楚生命周期管理,否则极易引发数据串用的并发问题。
疑问3:pairs和ipairs该怎么选?
ipairs从下标1开始顺序遍历,遇到nil即停止,只适合纯数组;pairs遍历所有非nil的key,包括哈希部分,顺序不保证。在OpenResty中遍历请求参数这类字典结构时必须用pairs;遍历有序列表时用ipairs。还有一点性能提示:LuaJIT下pairs遍历数组部分是可以被JIT加速的,但如果table结构在遍历中被修改,会触发NYI回退到解释执行,所以遍历过程中不要修改table结构。
疑问4:如何安全地清空一个table?
如果table会被复用,常见写法是循环赋nil或直接t = {}替换引用。对于数组形式的table,更高效的做法是记录长度后批量清理。要注意Lua没有内置的clear函数,LuaJIT环境下可以使用table.clear(位于require "table.clear"),它只清空内容不释放桶数组,是复用场景下的首选方案。
local clear = require "table.clear"
local t = {1, 2, 3, name = "demo"}
clear(t) -- 内容清空,table本身保留,适合复用
四、编码实践清单:少走弯路的几条守则
第一,数组下标从1开始且保持连续,不留nil空洞,需要占位时用false或0代替nil。第二,拼接字符串一律用table.concat,避免循环内的..连接。第三,请求级数据不要放进模块级table,防止内存泄漏。第四,跨请求数据用lua_shared_dict或lua-resty-lrucache,不要自造缓存结构。第五,遍历时不要增删table元素,需要修改就先收集待处理项,遍历完再统一处理。第六,涉及key是URL参数、header名称时统一做小写或规范化处理,避免大小写不同的key造成数据分裂。
把这些守则落实到日常编码中,配合对table底层结构的理解,绝大多数OpenResty Lua的table相关问题都能提前规避。建议在项目里做code review时专门检查table的生命周期和复用方式,这两个点是最容易埋雷的地方,也是线上问题的高发区。
OpenRestyLua Tablelua-resty库修改时间:2026-09-05 16:09:02