问题概述

今天第一次使用 Nginx+lua,写了个需要操作 Redis 的后台接口,该接口的功能主要是接受客户端的 json 格式的 post 请求,实现对保存在 redis 中的任务插入、删除、查询等。虽然 Nginx、lua 等都是刚接触,但这几个接口还是顺风顺水的坐下来了,不能忘了感谢春哥章亦春。

在 Redis 中记录的任务其实很简单,每插入一个任务,就在 redis 中增加一个 HASH 结构,每次查询返回该 SET 的各个 Field 和对应的 Value 值,例如 md5,filesize 等。由于任务类型的不同,有的 Field 可能在该任务中不存在,此时在以 json 格式将查询结果返回时不应显示该 Field。

以 md5 域为例,在对当前任务以 md5 域执行 hget 后,应该对返回结果做一个判断,如果该 HASH 结构并没有设置 md5 这个域,则跳过,继续执行后面的逻辑,如果设置了 md5 域,则把该域的 Value 取出来,插入到结果 table 中,后续再作为 json 格式返回结果的一部分,返回给后台。

测试时,却发现在某些域未设置时,查询结果中却仍然会把该域返回给查询调用者,但其 Value 部分是 null。例如,执行下面的测试用例:

curl -d "{\"queryfile\":[{\"url\":\"/www.baidu.com/img/bdlogo.gif\" }]}" "127.0.0.1/cjson"  

尽管对该任务而言,在插入时并没有设置 md5 域,但返回结果包含了 md5 域:

{"result":[{"url":"\/www.baidu.com\/img\/bdlogo.gif","result":0,"md5":null,"putflag":"remote"}]}  

问题分析

看到这个现象,首先想到的当然是 lua 脚本中对执行 hget md5 操作的返回值判断失效了,我第一次是这么写的:

local md5,err=red:hget(tasklist,"md5")  
if md5 and  md5 ~= ""  then  
    tb.md5=md5  
end  

从后面的结果看,当 md5 值为空时,该判断条件并没有将其过滤掉,依然执行了 tb.md5=md5。由于 redis 模块也是调用春哥的 lua-resty-redis,因此猜测是否春哥把 redis 查询结果中的空值用“null”字符串返回了,于是将上面的几行代码改为:

local md5,err=red:hget(tasklist,"md5")  
if md5 and  md5 ~= null  then  
    tb.md5=md5  
end  

仍然过滤失败,忽然眼前一亮,发现查询结果中显示的是"md5":null,而非"md5":"null",上面这种猜测不攻自破。

red:hget(tasklist,"md5") 肯定是返回了一个跟 null 相关的结果,但这个结果既不是 nil,又不是空字符串,也不是“null“。再次猜测,该值类型可能不是 string,虽然这个猜测看上去很奇怪,因为在设置了 md5 的情况下,其类型的确是 string。于是在判断语句前面加了一句打印信息:

ngx.say("type of null is "..type(md5))  

果然,这个”空值“并不是 string 类型,而是 userdata 类型,userdata 类型当然跟字符串类型不会相等,所以上面的过滤条件不管设置成什么样子,都不会生效,永远会执行 tb.md5=md5

这样是找到原因了,但还未最终解决。既然当 hget 操作返回一个空值时,lua-resty-redis 将其设置为一个 userdata 类型,那我们在代码里该如何过滤这种情况呢?本质问题就是,red:hget 当查询 resdis 结果为空时,到底返回了什么?(不为空时,是 string)

这时候开源的好处就体现出来了,在 https://github.com/agentzh/lua-resty-redis 里扫了下 redis.lua 文件,发现返回的是 ngx.null

恩,问题到这就解决了,将上面的过滤代码改为:

local md5,err=red:hget(tasklist,"md5")  
if md5 and md5 ~= null and md5 ~= ngx.null  then  
    tb.md5=md5  
end  

就能保证返回结果里不会包含值为 null 的域了。

眼高手低

回头看了一下 lua-resty-redis 的文档,发现关于上面的内容,在 Readme 里已经写的清清楚楚了,在 https://github.com/agentzh/lua-resty-redis/blob/master/README.markdown 中,有这么一句:

A non-nil Redis “bulk reply” results in a Lua string as the return value. A nil bulk reply results in a ngx.null return value.

ngx.null是什么?

那么 ngx.null 到底是什么东西呢? 在 http://wiki.nginx.org/HttpLuaModule 有如下说明:

The ngx.null constant is a NULL light userdata usually used to represent nil values in Lua tables etc and is similar to the lua-cjson library’s cjson.null constant. This constant was first introduced in the v0.5.0rc5 release.

ngx.null 在 print、ngx.print、ngx.log、ngx.say 等函数中,有如下特点:

Lua nil arguments are accepted and result in literal “nil” strings while Lua booleans result in literal “true” or “false” strings. And the ngx.null constant will yield the “null” string output.

为什么要这么设计?

lua-resty-redis 中,为什么要把 redis 查询为空的情况返回一个 userdata 类型的 ngx.null?直接返回 nil 不行吗?

答案是不行,因为 nil 在 lua 中有其特殊意义,如果一个变量被设置为 nil,就等于说该变量未定义。那么,如果把 redis 查询为空的结果设置为 nil,就无法把“查询为空”和“未定义”区分开来了,例如在一个 table 中,一个 key 对应一个 value,如果将该 value 设置为 nil,则相当让 key value 对消失,这显然是不合理的。所以必须用一个 userdata 类型的独特的值来表示这种查询为空,但又不等同于未定义的变量,例如 ngx.null。同样的情况想必在 sql 的 lua 模块中也会出现,用来处理记录中键值查询为空的情况。

再说说 nil

nil 是一种类型,该类型只有一个值,这个值也叫 nil。该值的作用只有一个,表示一个变量不存在。跟 C\C++ 等常规语言不同,”不存在“跟空、0 完全是两个概念。在 C 语言中,一个字符串如果为空,那么它就只有一个为 0 的 nul 结束符,如果对其进行逻辑判断,则是假。

但 lua 中,只要一个变量不是 nil 类型或者是 boolean 类型中的 false,则对它进行逻辑判断,结果是真,即使该值是一个数字 0,或者是一个空字符串。