RedisJSON中JSON.SET的用法小结
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">1 · 为什么要写这篇文章?</a></li><li><a href="#_label1">2 · RedisJSON 与 JSON.SET 概览</a></li><li><a href="#_label2">3 · 语法详解</a></li><li><a href="#_label3">4 · JSONPath 规则速查</a></li><li><a href="#_label4">5 · 返回值与错误处理</a></li><li><a href="#_label5">6 · 典型用法示例</a></li><ul class="second_class_ul"><li><a href="#_lab2_5_0">6.1 替换已有字段</a></li><li><a href="#_lab2_5_1">6.2 追加新字段</a></li><li><a href="#_lab2_5_2">6.3 一次性批量更新多路径</a></li><li><a href="#_lab2_5_3">6.4 结合 NX / XX 条件</a></li></ul><li><a href="#_label6">7 · 易踩坑汇总</a></li><ul class="second_class_ul"></ul><li><a href="#_label7">8 · 性能调优与并发安全</a></li><ul class="second_class_ul"></ul><li><a href="#_label8">9 · Go-Redis 完整示例(可直接运行)</a></li><ul class="second_class_ul"></ul><li><a href="#_label9">10 · 进阶话题</a></li><ul class="second_class_ul"></ul><li><a href="#_label10">11 · 总结</a></li><ul class="second_class_ul"></ul></ul></div><p class="maodian"><a name="_label0"></a></p><h2>1 · 为什么要写这篇文章?</h2><p>在结构化数据存储场景里,传统的字符串键值对已难以满足灵活查询与字段级更新的需求。<strong>RedisJSON</strong> 插件为 Redis 带来了对原生 JSON 文档的读写支持,其中最核心也最常用的指令就是 <code>JSON.SET</code>。本文将带你彻底吃透 <code>JSON.SET</code> 的各个细节——从语法、时间复杂度到多路径批量更新,再到在生产环境中容易踩到的坑和性能调优方法,并提供一套 <strong>Go-Redis</strong> 的完整示例代码,帮助你快速落地。</p>
<p class="maodian"><a name="_label1"></a></p><h2>2 · RedisJSON 与 JSON.SET 概览</h2>
<table><thead><tr><th>特性</th><th>说明</th><th></th></tr></thead><tbody><tr><td>插件版本</td><td>RedisJSON ≥ 1.0 (推荐 2.x 与 Redis 7.x+ 搭配使用)</td><td></td></tr><tr><td>指令</td><td>`JSON.SET key path value `</td></tr><tr><td>功能</td><td>按 JSONPath 将 value 写入 key 对应的 JSON 文档</td><td></td></tr><tr><td>ACL 标记</td><td>@json @write @slow</td><td></td></tr><tr><td>时间复杂度</td><td>O(M + N) —— M 为原值大小,N 为新值大小 × 匹配路径数</td><td></td></tr></tbody></table>
<p class="maodian"><a name="_label2"></a></p><h2>3 · 语法详解</h2>
<div class="jb51code"><pre class="brush:sql;">JSON.SET <key> <path> <value>
</pre></div>
<table><thead><tr><th>参数</th><th>必填</th><th>说明</th></tr></thead><tbody><tr><td>key</td><td>✔</td><td>Redis 中的键;不存在时必须从根路径写入</td></tr><tr><td>path</td><td>✔</td><td>JSONPath 表达式;$ 或 . 代表根</td></tr><tr><td>value</td><td>✔</td><td>合法 JSON 字符串,或原样字符串(无需额外引号)</td></tr><tr><td>NX</td><td></td><td>仅当目标不存在时写入</td></tr><tr><td>XX</td><td></td><td>仅当目标已存在时覆盖</td></tr></tbody></table>
<blockquote><p>小贴士:NX 与 XX 不仅作用于 Redis 键,也作用于 JSON 文档内部键。这点经常被忽略!</p></blockquote>
<p class="maodian"><a name="_label3"></a></p><h2>4 · JSONPath 规则速查</h2>
<ul><li>根路径:$ 或 .</li><li>点式:$.address.city</li><li>数组下标:$.items</li><li>切片:$.items(RedisJSON 2.x 支持)</li><li>通配:$..price 匹配所有层级的 price 字段</li><li>多路径:$..a 与 $..b 可一次性传入多条</li></ul>
<blockquote><p>注意:JSON.SET 不支持在一次调用中写入 不同 的值到多条路径;如果传入多路径,所有匹配点都会被同一个 value 覆盖。</p></blockquote>
<p class="maodian"><a name="_label4"></a></p><h2>5 · 返回值与错误处理</h2>
<table><thead><tr><th>情况</th><th>返回</th></tr></thead><tbody><tr><td>写入成功</td><td>OK</td></tr><tr><td>路径不存在且无法创建</td><td>(nil)</td></tr><tr><td>NX/XX 条件不满足</td><td>(nil)</td></tr><tr><td>根键不存在但路径不是 $</td><td>(error) ERR new objects must be created at the root</td></tr></tbody></table>
<p>生产环境中推荐显式检查返回值,而不要只依赖 err == nil。示例见 § 9。</p>
<p class="maodian"><a name="_label5"></a></p><h2>6 · 典型用法示例</h2>
<p class="maodian"><a name="_lab2_5_0"></a></p><h3>6.1 替换已有字段</h3>
<div class="jb51code"><pre class="brush:sql;">redis> JSON.SET doc $ '{"a":2}'
OK
redis> JSON.SET doc $.a 3
OK
redis> JSON.GET doc $
"[{\"a\":3}]"
</pre></div>
<p class="maodian"><a name="_lab2_5_1"></a></p><h3>6.2 追加新字段</h3>
<div class="jb51code"><pre class="brush:sql;">redis> JSON.SET doc $ '{"a":2}'
OK
redis> JSON.SET doc $.b 8
OK
redis> JSON.GET doc $
"[{\"a\":2,\"b\":8}]"
</pre></div>
<p class="maodian"><a name="_lab2_5_2"></a></p><h3>6.3 一次性批量更新多路径</h3>
<div class="jb51code"><pre class="brush:sql;">redis> JSON.SET doc $ '{"f1":{"a":1},"f2":{"a":2}}'
OK
redis> JSON.SET doc $..a 3
OK
redis> JSON.GET doc
"{\"f1\":{\"a\":3},\"f2\":{\"a\":3}}"
</pre></div>
<p class="maodian"><a name="_lab2_5_3"></a></p><h3>6.4 结合 NX / XX 条件</h3>
<div class="jb51code"><pre class="brush:sql;"># 仅当字段不存在时写入
redis> JSON.SET user:1 $.nickname '"neo"' NX
OK # 第一次成功
redis> JSON.SET user:1 $.nickname '"smith"' NX
(nil) # 条件未满足
</pre></div>
<p class="maodian"><a name="_label6"></a></p><h2>7 · 易踩坑汇总</h2>
<table><thead><tr><th>坑</th><th>现象</th><th>解决方案</th></tr></thead><tbody><tr><td>键不存在却写子路径</td><td>抛错 ERR new objects must be created at the root</td><td>第一次写必须用根路径 $</td></tr><tr><td>值未用 JSON 字符串包裹</td><td>若忘记加引号:JSON.SET $.name neo ⇒ 解析失败</td><td>非数值 / 布尔需双引号或 'neo'</td></tr><tr><td>NX/XX 作用域误解</td><td>误以为只检查 Redis 键</td><td>其实同时检查 JSON 内目标字段</td></tr><tr><td>字符串过长</td><td>超出 512 MB 限制</td><td>拆分存储或压缩再存</td></tr></tbody></table>
<p class="maodian"><a name="_label7"></a></p><h2>8 · 性能调优与并发安全</h2>
<ol><li>Pipeline / MULTI<br />连续多次 JSON.SET 可使用 Pipelining 或事务批量发送,减少 RTT。</li><li>避免大文档频繁写<br />时间复杂度包含原值大小 M:越大的 JSON,写一次越慢。可将热点字段拆分为独立键或使用 JSON.NUMINCRBY 等增量指令。</li><li>合理选择 NX/XX<br />在幂等场景下使用 NX 或 XX 可避免无谓的重写,降低写放大。</li><li>监控慢日志<br />Redis 会将执行时间 > slowlog-log-slower-than 的指令记录;JSON 操作本质属 @slow 类别,应重点关注。</li><li>Lua/RedisGears 乐观锁<br />高并发写同一字段,可结合 WATCH/MULTI 或 Lua 脚本实现 CAS。</li></ol>
<p class="maodian"><a name="_label8"></a></p><h2>9 · Go-Redis 完整示例(可直接运行)</h2>
<blockquote><p><strong>依赖</strong>:Go ≥ 1.22、<a href="https://github.com/redis/go-redis" rel="external nofollow"target="_blank">github.com/redis/go-redis/v9</a></p></blockquote>
<div class="jb51code"><pre class="brush:sql;">// 文件 main.go
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/redis/go-redis/v9"
)
var ctx = context.Background()
type Profile struct {
Namestring `json:"name"`
Age int `json:"age"`
Tags[]string `json:"tags"`
}
func must(err error) {
if err != nil {
log.Fatalf("fatal: %v", err)
}
}
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
defer rdb.Close()
// ---------- 1. 写入根文档 ----------
profile := Profile{Name: "Alice", Age: 27, Tags: []string{"gopher", "redis"}}
raw, _ := json.Marshal(profile)
if res, err := rdb.Do(ctx, "JSON.SET", "user:1001", "$", raw).Text(); err != nil {
must(err)
} else {
fmt.Println("set root:", res) // OK
}
// ---------- 2. 更新子字段 ----------
if res, err := rdb.Do(ctx, "JSON.SET", "user:1001", "$.age", 28, "XX").Text(); err != nil {
must(err)
} else if res == "" {
fmt.Println("XX unmet, age not updated")
} else {
fmt.Println("update age:", res) // OK
}
// ---------- 3. 读取文档 ----------
data, err := rdb.Do(ctx, "JSON.GET", "user:1001", "$").Result()
must(err)
fmt.Println("profile =", data.(string))
// ---------- 4. 错误示例 ----------
if _, err := rdb.Do(ctx, "JSON.SET", "user:1002", "$.name", "\"Bob\"").Result(); err != nil {
fmt.Println("expected error:", err)
}
// Output:
// set root: OK
// update age: OK
// profile = [{"name":"Alice","age":28,"tags":["gopher","redis"]}]
// expected error: ERR new objects must be created at the root
}
</pre></div>
<p>运行后,你将看到成功写入、条件更新、读回以及错误处理的完整流程。</p>
<p class="maodian"><a name="_label9"></a></p><h2>10 · 进阶话题</h2>
<table><thead><tr><th>方向</th><th>价值</th></tr></thead><tbody><tr><td>与 RediSearch 集成</td><td>对 JSON 字段建立二级索引,支持全文检索与聚合分析</td></tr><tr><td>存储版本化配置</td><td>使用 NX 写入新版本,配合 JSON.NUMINCRBY 维护版本号</td></tr><tr><td>灰度发布</td><td>通过数组/对象结构保存多线路配置信息,再用 JSON.DEL 快速回滚</td></tr><tr><td>Streaming JSON</td><td>将大文档拆分到 Stream 或 List,再按需合并到 RedisJSON</td></tr></tbody></table>
<p class="maodian"><a name="_label10"></a></p><h2>11 · 总结</h2>
<ul><li>JSON.SET 是 RedisJSON 最核心的写入指令,先掌握根写入,再逐步学习多路径与条件写。</li><li>时间复杂度与文档大小 线性相关,因此避免频繁重写大 JSON。</li><li>正确使用 NX/XX 可实现幂等更新、乐观锁等高级场景。</li><li>在 Go-Redis 中通过 Do(ctx, "JSON.SET", …) 可无缝调用,务必检查返回值而非只关注 err。</li><li>生产环境要结合 慢日志、Pipeline、事务 等手段进行性能与可靠性保障。</li></ul>
頁:
[1]