关于openresty中lua时间转换引出的思考

好久没更新博客了,打算写一篇记录下最近项目的心得。最近在做一个openresty写lua的项目遇到不少难题,虽然一一攻克,但是回过头来想想,确实应该正确定位openresty中lua的功能。

公司的棋牌游戏菜单打算采用lua来从mysql数据库中取出,根据各种蛋疼的规则,然后生成json返回给客户端,这样客户端只要解析json生成菜单树即可,排序,节点属性,显示等等一切逻辑都被记录在json字符串中,由于数据库中记录的菜单数据比较原始,所以lua承担了比较繁重的逻辑和重新组合节点的任务,捎带还要有缓存支持。

首先说明下为什么会选择lua去生成这个json字符串,最主要的原因当然是性能问题,luajit可以算是最快的脚本语言了,同时配合nginx的高效,我们可以无状态的横向部署多台nginx作负载均衡,毕竟对于几万人在线的公司棋牌游戏菜单获取压力也着实不小。

其次因为之前公司很多对外api前端逻辑和下载统计等都是用lua来处理的,其性能和稳定性都不错,也算尝到了甜头,所以这次菜单的功能也顺理成章的使用lua来处理了。

但是这次游戏菜单会根据不同的站点生成不同的json,还有其他一些逻辑,节点的父类子类总之比较麻烦,所以我在编写这个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目录中,直接requrie即可使用,非常方便,他也拥有很多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中连接可能存在各种问题,包括数据查询不到,链接错误等等,所以我最后的代码中充斥着返回err,nil和关闭归还数据库连接的代码,如下:
local err,db = self:connect() –连接mysql数据库
if err then
self:close_conn()
return err,nil
end
每次sql操作都需要执行,当然也不能对lua语言有这奢望了

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

大量雷同的代码,不写上详细的注释,我怕1个月后,自己都无法维护了

4、恶心的调试
其实最影响我开发效率的就是调试,我实在想不出能有什么更好的调试方法,只要lua脚本出错,nginx就会报500,于是我只能开着secureCRT软件,通过 tail -f error.log 的方式,查看lua脚本中各种语法错误或者是类型转换错误。

在运行中无法设置断点,想要知道程序运行的如何,只能在代码中到处打log,恶心的是ngx.log输出的log还是下面这种格式的,让人无法一下子找到自己打的log在哪里?
2014/03/14 12:46:00 [error] 12729#0: 1251 [lua] mysql_class2.lua:275: filterEmpty(): 1, client: 192.168.11.30, server: trymenu.6998test.com, request: “GET /menu?city=gg HTTP/1.1”, host: “192.168.28.27:3001”
2014/03/14 12:46:00 [error] 12729#0:
1251 [lua] mysql_class2.lua:276: filterEmpty(): null, client: 192.168.11.30, server: trymenu.6998test.com, request: “GET /menu?city=gg HTTP/1.1”, host: “192.168.28.27:3001”
2014/03/14 12:46:00 [error] 12729#0: *1251 [lua] mysql_class2.lua:277: filterEmpty(): nil, client: 192.168.11.30, server: trymenu.6998test.com, request: “GET /menu?city=gg HTTP/1.1”, host: “192.168.28.27:3001”
大家能看出来,其实上面我打印了1,null,nil 这3个变量值吗?ngx.log方法只能接受字符串,nil等类型的值,所以你想要看table里面的内容,嘿嘿,自己用 cjosn.encode(table) 打印把,然后将打印的json字符串我还要丢到chrome的consle里转换成js对象才能看清他们的层次关系和key,val值,如果table过大甚至还会出现log打印不全的问题,当时开发实在是抓狂了!

我的代码到处是注释掉的如下代码:
– ngx.log(ngx.ERR, sqlCmd)
–ngx.log(ngx.ERR,cjson.encode(self.menuTable))

5、xor加解密
由于返回的菜单需要一个加密算法,神马3DES貌似Openresty原生不支持,于是想到了使用XOR异或来生成加密,通过同样的XOR异或key来解密,但是用lua实现这个功能也是耗费了一番波折,先在网上搜到了一个加密的方法,可惜我们字符串太长,这个方法会报错:
— 使用密钥对字符串进行加密(解密)
– @param string str 原始字符串(加密后的密文)
– @param string key 密钥
– @return string 加密后的密文(原始字符串)
local function encrypt(str, key)
local strBytes = { str:byte(1, #str) }
local keyBytes = { key:byte(1, #key) }
local n, keyLen = 1, #keyBytes
for i = 1, #strBytes do
strBytes[i] = bit.bxor(strBytes[i], keyBytes[n])

    n = n + 1

    if n > keyLen then
        n = n - keyLen
    end
end
return string.char(unpack(strBytes))

end
不过感谢这位博主,给了我思路,链接地址:http://zivn.me/?p=183
于是我简单修改下代码,让算法支持长字符串的加密,最后代码如下:
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形式,明细快多了
str = str .. (加密字符)
上面这种方式小的拼接没影响,大字符串拼接就坑爹了

总结
经历过和tengine或openresty中lua脚本的蜜月期,慢慢成为蛋疼期,也让我重新定义lua脚本在web server的定位,对于逻辑复杂,经常需要调整的功能或者需求,坚决不要让lua去做,这会让开发人员很抓狂,如果对需求相对稳定,比如我们api前端处理用户请求罚值,合法性验证,下载地址统计跳转等等,lua非常胜任。
所以千万不要神话nginx+lua,他有他适合的地方,也有非常明显的短板。
最后线上项目地址:https://github.com/DoubleSpout/tryMenu