Redis Hash与JSON字符串存储对象的选择边界
来源:17golang原创
时间:2026-09-20 11:44:11 476浏览 收藏
我在给用户资料做缓存时,最容易踩的坑不是命令不会用,而是把“对象”三个字直接等同于一种 Redis 结构。需要按字段改昵称、按字段读状态时,Hash 很顺手;只想把一份已经序列化好的响应整体放进去,普通字符串 JSON 更直接;如果业务要求在 JSON 文档内部按路径修改,才值得评估 RedisJSON。
官方地址:https://redis.io/
- Hash 把对象字段拆成 Redis field,适合局部读写;但 HGETALL 会随字段数增长。
- 字符串 JSON 的边界清晰,适合整对象 SET/GET;局部改一个字段要重新序列化并覆盖整值。
- RedisJSON 支持 JSONPath 级访问与更新,但部署、客户端和运维能力要先确认。
- EXPIRE 作用在 key 上,HSET 不会清掉已有 key 的过期时间;字段级 TTL 要另看 Redis 版本能力。
先把“对象怎么被使用”说清楚
同一个 User 对象,如果每次请求都读取全部字段,Hash 和字符串 JSON 都能完成任务;如果只改 status,Hash 可以直接 HSET,而字符串 JSON 通常要 GET、反序列化、修改、再 SET。这个差异会随着对象大小和写入频率放大,所以选型应先看访问粒度。
| 需求 | 优先方案 | 要留意的边界 |
|---|---|---|
| 字段稳定、经常局部读写 | Redis Hash | 全量读取使用 HGETALL,字段多时要控制对象大小 |
| 整对象缓存、序列化结果直接透传 | 字符串 JSON | 局部修改需要整值重写,注意并发覆盖 |
| 需要 JSONPath 局部更新 | RedisJSON | 确认模块、客户端和集群运维支持 |
Hash:把字段级操作放到缓存层
Redis 官方把 Hash 描述为 field-value 集合。典型的 key 可以是 user:42,字段是 name、status、updated_at。在 Go 客户端中,保存对象时可以显式列出允许进入缓存的字段,避免把数据库内部字段无意暴露出去。
package main
import (
"context"
"time"
"github.com/redis/go-redis/v9"
)
func saveUserHash(ctx context.Context, rdb *redis.Client) error {
// Hash 只保存缓存契约中的字段,便于按字段修改和排查。
key := "user:42"
if err := rdb.HSet(ctx, key,
"name", "Lin",
"status", "active",
"updated_at", "2026-09-20T10:00:00+08:00",
).Err(); err != nil {
return err
}
// 过期时间挂在 key 上;HSET 修改字段不会自动清除已有 TTL。
return rdb.Expire(ctx, key, 30*time.Minute).Err()
}

读取单个状态时用 HGet,只取多个已知字段时用 HMGet;只有确实需要组装整个对象时再用 HGetAll。后者的复杂度会随字段数量增加,不能因为对象看起来小就默认全量读取。
字符串 JSON:用整值缓存换取简单边界
如果上游接口本来就产出 JSON,普通字符串方案通常更容易迁移:应用把结构体编码成 JSON,Redis 只负责保存和返回字节。Redis 不理解其中的字段,因此也不会替你完成 JSONPath 查询或字段级合并。
package main
import (
"context"
"encoding/json"
"time"
"github.com/redis/go-redis/v9"
)
type UserCache struct {
Name string `json:"name"`
Status string `json:"status"`
}
func saveUserJSON(ctx context.Context, rdb *redis.Client, user UserCache) error {
// 字符串 JSON 适合整对象缓存,写入前由应用负责序列化。
payload, err := json.Marshal(user)
if err != nil {
return err
}
// SET 的 EX 让写入和 key 级 TTL 在一次命令中完成。
return rdb.Set(ctx, "user:42", payload, 30*time.Minute).Err()
}
上面的示例还需要在导入区加入 encoding/json。这里故意把序列化责任留在应用层:字段变更时要考虑旧缓存兼容,多个写请求同时更新不同字段时也要避免“后写覆盖先写”的整值丢失。
RedisJSON:只有文档路径更新才值得引入
Redis 官方 JSON 文档提供 JSON.SET、JSON.GET 等命令,并支持路径级访问。它和“Redis 字符串里放一段 JSON”不是一回事:后者只是应用保存的字节,前者需要对应的 RedisJSON 能力与客户端支持。
# RedisJSON 示例:只更新文档中的 status 路径,不覆盖其他字段 JSON.SET user:42 $.status '"active"' # 读取单个路径,返回值形态取决于客户端的 JSON 命令封装 JSON.GET user:42 $.status
如果线上环境只有基础 Redis 命令,不能把这段命令当成普通 SET 的替代品。先确认服务形态、模块加载方式、集群兼容性和 Go 客户端 API,再决定是否让缓存模型依赖它。

过期和并发是最后的落点
最稳妥的 key 设计通常先统一过期策略:Hash 与字符串 JSON 都可以对 key 使用 EXPIRE,字符串 JSON 也可在 SET 时带 EX。如果字段需要不同生命周期,先确认目标 Redis 版本是否提供字段级过期命令,再评估它是否值得增加客户端和运维复杂度。
- 局部字段更新多、字段集合相对稳定:优先 Hash,并限制 HGETALL 的对象大小。
- 整对象读写多、缓存内容直接作为接口响应:优先字符串 JSON,配合版本字段或乐观覆盖策略。
- 必须按 JSONPath 改嵌套字段或数组:评估 RedisJSON,同时把模块能力写进部署清单。
- 多个写者会同时改同一对象:不要只靠“最后一次 SET”,必要时使用事务、版本号或重新设计 key。
我的经验是先选能覆盖主要访问路径、依赖最少的结构:Hash 和字符串 JSON 往往已经够用。只有“必须原子更新嵌套文档”这个需求真实存在时,再把 RedisJSON 纳入方案。这样迁移和故障排查时,数据结构本身不会成为额外的隐藏前提。
相关问题
Hash 能不能保存嵌套对象?
可以把嵌套对象单独序列化成某个 field 的字符串,但 Redis 不会理解其中的层级;如果需要路径级操作,应重新评估 RedisJSON 或拆分 key。
HSET 之后还需要重新 EXPIRE 吗?
已有 key 的 HSET 不会清掉 key 的 TTL,但首次创建 key 或采用不同生命周期策略时,仍应明确设置并检查 TTL。
JSON 字符串一定比 Hash 占空间少吗?
不一定。空间取决于字段名重复、对象大小、编码方式和 Redis 内部实现,不能只凭结构名称下结论,应在代表性数据上测量。
什么时候不该把对象放 Redis?
当数据需要复杂查询、强一致持久化或长期审计时,Redis 更适合作为缓存或加速层,主数据仍应放在适合该职责的存储中。
-
226 收藏
-
489 收藏
-
485 收藏
-
349 收藏
-
236 收藏
-
130 收藏
-
364 收藏
-
309 收藏
-
354 收藏
-
223 收藏
-
324 收藏
-
218 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习