问题概述

今天第一次在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 ~= “nullthen
    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.

首先不应该是自责,而是再次赞一下agentzh的态度,业界标杆。

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凭空消失,这显然是不合理的。所以必须用一个userdata类型的独特的值来表示这种查询为空,但又不等同于未定义的变量,例如ngx.null。同样的情况想必在sql的lua模块中也会出现,用来处理记录中键值查询为空的情况。

幽灵般的nil

这就要说道lua中神奇的nil了。nil是一种类型,该类型只有一个值,这个值也叫nil。改值的作用只有一个,表示一个变量不存在。跟CC++等常规语言不同,”不存在“跟空、0完全是两个概念。在C语言中,一个字符串如果为空,那么它就只有一个为0的nul结束符,如果对齐进行逻辑判断,则是假。
但lua中,只要一个变量不是nil类型或者是boolean类型中的false,则对它进行逻辑判断,结果是真,即使该值是一个数字0,或者是一个空字符串。