lua 8小时时差
公司的棋牌游戏菜单计划采用 Lua 从 MySQL 数据库中提取数据,依据一系列规则生成 JSON 并返回给客户端。客户端只需解析该 JSON 即可生成菜单树,菜单的排序、节点属性、显示等逻辑均记录在 JSON 字符串中。由于数据库中的菜单数据较为原始,Lua 需承担繁重的逻辑处理和节点重新组合任务,同时还需支持缓存。
选择 Lua 生成 JSON 字符串的原因
性能优势
LuaJIT 是目前最快的脚本语言之一,配合 Nginx 的高效性能,我们可以无状态地横向部署多台 Nginx 进行负载均衡。对于拥有数万人在线的公司棋牌游戏来说,菜单获取的压力较大,使用 Lua 能有效应对这一挑战。
经验积累
此前公司的许多对外 API 前端逻辑和下载统计等都使用 Lua 处理,其性能和稳定性表现出色。基于以往的成功经验,此次菜单功能自然也选择使用 Lua 来实现。
开发过程中遇到的问题及解决方案
1. Lua 字符串与时间的相互转换
在编写脚本时,首先遇到的问题是 Lua 对时间和字符串的相互转换。以下是一个将时间戳格式化为当前时间的代码示例:
local now = os.date("%Y-%m-%d %H:%M:%S", os.time())
将时间戳转换为时间字符串相对顺利,但当需要将从数据库查询出的时间字符串转换为 Lua 的时间类型时,却发现没有现成的方法,需要自行实现。最终,在 GitHub 上找到了一个外国朋友编写的 date.lua 类,非常实用,分享给大家:
项目地址:https://github.com/Tieske/date
将文件夹下的 date.lua 放入 OpenResty 定义的 lib 目录中,通过 require 即可使用。该类拥有丰富的 API 功能,具体使用方法请参阅 GitHub 项目帮助文档。
local dateLib = require "date"
-- ...
updatetime = dateLib(tonumber(updatetime)):tolocal() -- 此处需转换为 local 时间,否则会有 8 小时时差
-- ..
local writetime = dateLib(v['writetime'])
通过上述方法,我们可以将时间戳或时间字符串转换为 Lua 的 date 类型,并且 date 类型之间可以进行 > 或 < 等比较操作。
2. 缺少类似 Go 语言的 defer 功能
由于 MySQL 连接可能出现各种问题,如数据查询不到、连接错误等,因此在代码中需要频繁处理错误并关闭归还数据库连接。示例代码如下:
local err,db = self:connect() -- 连接 MySQL 数据库
if err then
self:close_conn()
return err,nil
end
每次进行 SQL 操作都需要执行上述代码,Lua 语言本身并没有提供类似 Go 语言的 defer 功能,这给代码编写带来了一定的不便。
3. 缺少 table 的高级功能
在 Lua 中,数组和对象都属于 table 类型。然而,Lua 缺少对 table 类型的一些常用操作,如 indexOf、forEach、filter 等,导致需要编写大量的 for 语句和临时 table 来保存处理数据,甚至会出现多层 for 循环嵌套的情况。
例如,在循环中修改循环 table 的内容是非常危险的操作,因此需要将修改的位置 pos 记录在临时 table 中,处理结束后再循环处理临时 table 保存的 pos 数据。大致代码如下:
local removeIds = {}
for i,v1 in ipairs(objtable) do -- 第一步,循环结果,查找空 channel
if v1 and v1['ItemType'] == typestr then
local countParent = 0
local curId = v1['Id']
for j,v2 in ipairs(objtable) do
if v2 and v2['parentId'] == curId then
countParent = countParent + 1
end
end
if countParent == 0 then
table.insert(removeIds, curId)
end
end
end
return table.getn(removeIds), removeIds
大量雷同的代码给代码维护带来了困难,若不添加详细注释,可能在短时间后就难以理解代码逻辑。
4. 调试困难
调试是开发过程中最影响效率的环节。当 Lua 脚本出错时,Nginx 会返回 500 错误,只能通过 tail -f error.log 的方式查看 Lua 脚本中的语法错误或类型转换错误。
在运行过程中无法设置断点,只能在代码中插入大量的 ngx.log 语句来输出调试信息。ngx.log 方法只能接受字符串、nil 等类型的值,若要查看 table 里面的内容,需要使用 cjson.encode(table) 进行打印,然后将打印的 JSON 字符串复制到 Chrome 的控制台中转换为 JavaScript 对象,才能看清其层次关系和键值对。如果 table 过大,还会出现日志打印不全的问题,给开发带来极大的困扰。
以下是代码中常见的注释掉的调试代码示例:
-- ngx.log(ngx.ERR, sqlCmd)
-- ngx.log(ngx.ERR, cjson.encode(self.menuTable))
5. XOR 加解密
由于返回的菜单需要进行加密处理,而 OpenResty 原生不支持 3DES 加密算法,因此选择使用 XOR 异或加密。在实现过程中,遇到了一些波折。最初在网上找到的加密方法在处理长字符串时会报错,经过修改后,最终实现了支持长字符串加密的代码:
local XorKey = 4
local s = ngx.encode_base64(json)
local stable = {}
for i = 1, #s do
table.insert(stable, string.char(bit.bxor(string.byte(s, i), XorKey)))
end
local s2 = table.concat(stable)
需要注意的是,在拼接字符串时,直接使用字符串拼接效率较低,改为使用 table 拼接后,性能有了明显提升。
总结
在使用 Lua 进行开发的过程中,我们经历了从蜜月期到蛋疼期的转变。这也让我们重新审视了 Lua 脚本在 Web 服务器中的定位。对于逻辑复杂、需要频繁调整的功能或需求,不建议使用 Lua 实现,否则会让开发人员陷入困境。而对于需求相对稳定的场景,如 API 前端处理用户请求罚值、合法性验证、下载地址统计跳转等,Lua 则是一个非常合适的选择。
因此,我们不应过分神话 Nginx + Lua 的组合,要充分认识到它的适用场景和明显短板。