我说的 cjson
抢占 问题可不是 lua 的 非抢占 式协程,更准确的理解应该是:由于 lua 的协程切换,可能会导致 cjson
上下文不一致的情况。例如:
-- json.lua
local json = require("cjson")
local _M = {}
function _M.encode(data, empty_table_as_object)
if json.encode_empty_table_as_object then
json.encode_empty_table_as_object(empty_table_as_object or false) -- empty table encoded as array default
end
ngx.sleep(0.1)
local ok, data = pcall(json.encode, data)
if ok then
return data
end
end
-- nginx.conf
location /t_arr {
content_by_lua '
local json = require "resty.json"
-- ngx.sleep(0.1)
ngx.say(json.encode({
info = "data should be an array",
data = {}
}, false))
';
}
location /t_obj {
content_by_lua '
local json = require "resty.json"
-- ngx.sleep(0.1)
ngx.say(json.encode({
info = "data should be a object",
data = {}
}, true))
';
}
当这样有并发请求时:
curl http://192.168.3.4/t_arr & \
curl http://192.168.3.4/t_obj
#################################
{"info":"data should be an array","data":{}}
{"info":"data should be a object","data":{}}
这里发生这个问题的关键就是 json.encode
之前, cjson
的上下文会因为 ngx.sleep(0.1)
的介入导致请求 yield
,随之而发生了变化。cjson.safe
其实也会有同样的问题,其仅仅是帮我们把那个 pcall
做了下而已:
The cjson.safe module behaves identically to the cjson module, except when errors are encountered during JSON conversion. On error, the cjson_safe.encode and cjson_safe.decode functions will return nil followed by the error message.
如何解决这个问题?很简单,就是把 ngx.sleep(0.1)
这行去掉即可,另 _M.encode
这个方法不发生请求之间的切换即可。同时要第三方避免对 cjson
模块的滥用,否则依然会有潜在性的安全问题。
同样 cjson
也有个 cjson.new
的方法,可以通过实例化来保证其上下文的绝对安全,但是显然这个是有性能损耗的:
cjson.new can be used to instantiate an independent copy of the Lua CJSON module. The new module has a separate persistent encoding buffer, and default settings. Using a separate cjson module table per preemptive thread (cjson.new)
function _M.encode_new(data, empty_table_as_object)
local instance = json.new()
if instance.encode_empty_table_as_object then
instance.encode_empty_table_as_object(empty_table_as_object or false) -- empty table encoded as array default
end
ngx.sleep(0.1)
local ok, data = pcall(instance.encode, data)
if ok then
return data
end
end
curl http://192.168.3.4/t_arr & \
curl http://192.168.3.4/t_obj
#################################
{"info":"data should be an array","data":[]}
{"info":"data should be a object","data":{}}
最后我的建议是没有必要使用 cjson.new
来让保证 cjson
上下文的绝对安全,我们只需要保证自己封装的模块在执行 cjson.encode
时能保证其操作的是期待的上下文即可。同时项目所有对 json
数据的操作,只通过项目唯一的 _M.encode
、_M.decode
方法就好。
function _M.encode(data, empty_table_as_object)
if json.encode_empty_table_as_object then
json.encode_empty_table_as_object(empty_table_as_object or false) -- empty table encoded as array default
end
local ok, data = pcall(json.encode, data)
if ok then
return data
end
end
function _M.decode(data, empty_table_as_object)
if json.encode_empty_table_as_object then
json.encode_empty_table_as_object(empty_table_as_object or false) -- empty table encoded as array default
end
local ok, data = pcall(json.decode, data)
if ok then
return data
end
end