登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  Redis

Redis Lua 里用 ARGV 传 JSON 时怎么避免类型误判

来源:17golang原创

时间:2026-09-08 03:46:52 104浏览 收藏

Redis Lua 里通过 ARGV 传入 JSON 时,最稳妥的做法是把它当作原始字符串接收,只在脚本入口用一次 cjson.decode,随后按对象和字段逐个校验类型。不要把“客户端传来的 JSON 字符串”“解码后的 Lua 表”和“要写入 Redis 的字符串”混成一个变量。这样既能保留脚本的原子性,也能避免数字、字符串、布尔值和空值在不同边界发生误判。

要点速览
  • KEYS 只放脚本实际访问的键,业务 JSON 放在 ARGV
  • ARGV[1] 先按字符串处理,cjson.decode 只做一次,并用 pcall 兜住非法 JSON。
  • 写回 Redis 前重新编码对象,返回给客户端时明确区分字符串、整数和错误表。

ARGV 到底是什么:先把原始字符串留在边界外

Redis Lua 中 ARGV 原始字符串经过 cjson.decode 进入对象字段校验的静态边界关系
图1:ARGV、JSON 解码和字段校验是三个不同边界,先保留原文再进入 Lua 对象层。

EVAL script numkeys key [key ...] arg [arg ...] 中,前面的键名进入 KEYS,后面的普通参数进入 ARGV。因此一次调用可以写成下面这样:

-- KEYS[1] 是脚本要操作的键,ARGV[1] 是完整 JSON 字符串
EVAL "return ARGV[1]" 1 order:1001 '{"amount":12,"source":"api"}'

这里的 ARGV[1] 仍然是字符串。它不是 Lua 表,也不会因为内容长得像数字就自动变成数字。真正的类型变化发生在 cjson.decode 之后:JSON 对象会成为 Lua 表,JSON 数字会成为 Lua number,JSON 字符串仍是 Lua string。把这个边界写清楚,后面的判断才有依据。

先解码一次,再按业务类型收口

生产脚本不要到处重复解码,也不要拿 tostring 代替类型校验。下面的入口只接受一个 JSON 对象,并要求 amount 是数字、source 是字符串;任何不符合约定的输入都会在写入前结束。

-- 入口只负责解析和收口,不把未校验的字段写入 Redis
local raw = ARGV[1]
if type(raw) ~= "string" or raw == "" then
    return { err = "ARGV[1] must be a non-empty JSON string" }
end

-- cjson.decode 可能抛出异常,用 pcall 把坏输入变成可识别的错误
local decoded_ok, payload = pcall(cjson.decode, raw)
if not decoded_ok or type(payload) ~= "table" then
    return { err = "payload must be a JSON object" }
end

local amount = payload.amount
local source = payload.source
if type(amount) ~= "number" or type(source) ~= "string" then
    return { err = "amount must be number and source must be string" }
end

-- Redis 命令参数使用明确的字符串表示,避免把 Lua 表直接传给 HSET
redis.call("HSET", KEYS[1], "amount", tostring(amount), "source", source)
return { "ok", tostring(amount) }

如果业务允许 amount 既可以是 JSON 数字也可以是数字字符串,要在协议层明确选择,而不是在脚本里无条件转换。否则 12"12" 会悄悄失去区别,调用方也无法判断这是客户端传错类型,还是服务端主动兼容。

KEYS、ARGV 和返回值要分开设计

Redis Lua 中 KEYS 与 ARGV 进入原子脚本、命令写入和响应返回的静态结构
图2:KEYS 负责键名,ARGV 负责业务参数,Redis 命令和脚本返回值各自保持明确类型。

JSON 对象不能直接作为 Redis 命令的一个参数。需要保存整个对象时,先在 Lua 中完成校验,再用 cjson.encode 重新得到字符串;只保存字段时,则把每个字段显式转为命令接受的值。脚本返回值也要提前约定,因为 RESP2 下 Lua number 会按整数回复,带小数的结果不能直接这样返回。

-- 将已校验的数据重新编码后保存,避免把 Lua table 当成命令参数
local record = {
    amount = amount,
    source = source,
    state = "accepted"
}
redis.call("SET", KEYS[1], cjson.encode(record))

-- 用字符串返回小数或需要保持格式的值,避免 RESP2 的整数转换
return {
    status = "ok",
    value = cjson.encode(record)
}

上面的关联表返回方式适合在脚本内部表达状态,但客户端看到的最终结构仍受 RESP2/RESP3 协议转换影响。若接口需要稳定的跨客户端响应,建议返回短字符串或定长数组,例如 {"ok", cjson.encode(record)},并在客户端统一解析。

发布前用边界清单挡住类型误判

检查位置应保持的类型常见错误
KEYS真实键名把 JSON 字段拼成动态键,集群路由和审计都变得不清晰
ARGV原始字符串同一参数重复解码,或未校验就写入
Lua 表脚本内部对象直接作为 HSET/SET 参数传递
返回值已约定的字符串/数组把小数 Lua number 当作精确小数返回

上线前至少检查四件事:所有访问的键是否都在 KEYS;JSON 是否只解码一次;每个写入字段是否经过 type 判断;返回值是否在目标 RESP 协议下仍保持客户端约定。脚本仍然是一次原子执行,但原子性不会替你修复输入类型,边界校验必须放在命令写入之前。

常见问题

ARGV[1] 是不是可以直接当 Lua 表使用?

不可以。它先是客户端传入的字符串,必须经过 cjson.decode 才会得到 Lua 值;解码前应先检查空串和输入来源。

JSON 对象为什么不能直接传给 HSET?

解码后的对象是 Lua table,不是 Redis 命令参数。要么取出字段并逐项转换,要么用 cjson.encode 把完整对象重新变回字符串。

为什么返回小数时建议转字符串?

Lua 只有一种 number 类型,RESP2 返回 Lua number 时会按整数规则转换。需要保留小数文本时,应按字符串返回并由客户端解析。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>