json.set 无法安全实现局部更新,因其路径写入不保证“读-改-写”原子性,高并发下易丢数据;需用 lua 脚本封装 json.get + 内存修改 + json.set 全量写回,依赖 redis 单线程执行保障原子性。

为什么不能直接用 JSON.SET 做局部更新
因为 JSON.SET 是全量覆盖,不是局部修改。你传一个新对象进去,整个字段就替换了,中间状态完全不可控——这在并发写同个 JSON 字段时会丢数据。比如两个客户端同时读出 {"a":1,"b":2},各自改 a 和 b 再写回,必然有一个覆盖另一个。
真正需要的是类似 JSON.SET user:123 $.a 100 这种路径级原子操作,但 Redis 原生 JSON 模块(v4+)的 JSON.SET 虽支持路径,仍不保证“读-改-写”原子性;它只保证单次命令原子,不解决竞态。
- 想局部改
$.user.profile.age?必须先JSON.GET,再本地解析、修改、JSON.SET—— 三步,非原子 - 用 Lua 就能把这三步压进一个服务端原子上下文里
- 注意:Redis 7.0+ 的
JSON.SET加了XX/NX等选项,但依然不解决“读取后计算再写入”的原子性问题
怎么写一个安全的 Lua 脚本做 JSON 局部更新
核心思路:用 EVAL 把 JSON 解析、路径定位、值替换、序列化全部塞进 Lua,靠 Redis 单线程执行保障原子性。别指望 redis.call("JSON.GET",...) 后再用 Lua 处理——JSON 字符串得自己解析,而 Redis 内置的 cjson 不可用(Lua 沙箱禁用),所以得用纯字符串操作或预编译逻辑。
更实际的做法是:把更新逻辑写死在脚本里,用参数传入 key、path、new_value,由 Lua 用 redis.call("JSON.GET", key) 拿原始 JSON,然后调用 redis.call("JSON.SET", key, path, new_value) —— 等等,这还是两步?错,JSON.SET 本身支持路径和原子写入,但前提是你要确保它不会被并发干扰。所以真正安全的组合是:JSON.GET + Lua 内存中 patch + JSON.SET 全量写回,且全程无网络往返。
- 脚本必须用
redis.call("JSON.GET", KEYS[1])读,不能用redis.pcall(失败会中断脚本) - 如果 JSON 值为空或不存在,
JSON.GET返回nil,Lua 中要显式判断,否则cjson.decode(nil)会报错 —— 但注意:Redis Lua 沙箱里没有cjson!得用redis.call("JSON.GET",...)拿字符串,再用string.match或提前约定结构简化逻辑 - 推荐做法:只支持简单路径如
$.a或$.data.count,用正则提取字段名,拼接新 JSON 字符串,避免通用 JSON 解析 - 示例脚本片段:
local json = redis.call("JSON.GET", KEYS[1]) if not json then return nil end -- 假设只更新 $.score,且原结构固定 local new_json = string.gsub(json, '"score":%s*%d+', '"score":' .. ARGV[1]) redis.call("JSON.SET", KEYS[1], "$", new_json) return 1
EVAL 调用时的参数和坑
脚本本身没状态,但传参稍有不慎就会失败。KEYS 和 ARGV 必须严格对应,且所有 key 必须显式传入 KEYS 列表(Redis Cluster 强制要求),否则集群模式下直接报错 CROSSSLOT Keys in request don't hash to the same slot。
- KEYS 参数只能是 key 名,不能是表达式或拼接结果,例如
"user:"..ARGV[1]在 Lua 里合法,但 Redis 不认这个为 KEY,会导致集群路由失败 - ARGV 可以传任意字符串,但 JSON 值里如果有双引号、反斜杠,必须由客户端提前转义,Lua 里不做二次处理
- 错误信息如
(error) ERR Error running script (call to f_...): @user_script:5: user_script:5: bad argument #2 to 'gsub' (string expected, got nil),通常是因为JSON.GET返回 nil(key 不存在或字段路径错),没做空值判断 - 性能上,Lua 脚本执行时间不能超
lua-time-limit(默认 5 秒),复杂 JSON 遍历或大 payload 容易触发BUSY错误
Redis 版本和模块依赖的实际约束
别假设你的 Redis 装了 JSON 模块。很多云厂商托管 Redis(比如阿里云、腾讯云基础版)默认不启用 redis-json,得手动开启或选高配版本。而且模块版本影响功能边界:
- Redis Stack 或 Redis 7.0+ 自带 JSON 模块,支持
JSON.SET路径语法;6.x 需单独加载redis-json.so - 6.2 以下版本连
JSON.GET都没有,Lua 里根本没法读 JSON —— 此时只能退化为字符串字段 + 正则替换,风险自担 - 模块未加载时执行
JSON.GET会直接报错(error) ERR unknown command `JSON.GET`,这个错误发生在 EVAL 前,脚本甚至不会运行 - 验证方式很简单:
MODULE LIST看输出里有没有name:rejson或name:json
最易被忽略的一点:Lua 脚本里不能调用 redis.call("MULTI") 或任何事务命令,Redis 的 Lua 沙箱禁止嵌套事务。所谓“原子性”,纯粹来自 Redis 单线程顺序执行脚本这一事实,不是靠 MULTI/EXEC 实现的。










