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

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,字段是 namestatusupdated_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()
}
Redis Hash把用户对象拆成字段并连接到HSET、HGET和key级过期边界的静态说明图
图1:Redis Hash 结构说明图,展示对象字段、字段级读写命令与 key 级过期之间的静态关系,不是运行截图。

读取单个状态时用 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.SETJSON.GET 等命令,并支持路径级访问。它和“Redis 字符串里放一段 JSON”不是一回事:后者只是应用保存的字节,前者需要对应的 RedisJSON 能力与客户端支持。

# RedisJSON 示例:只更新文档中的 status 路径,不覆盖其他字段
JSON.SET user:42 $.status '"active"'
# 读取单个路径,返回值形态取决于客户端的 JSON 命令封装
JSON.GET user:42 $.status

如果线上环境只有基础 Redis 命令,不能把这段命令当成普通 SET 的替代品。先确认服务形态、模块加载方式、集群兼容性和 Go 客户端 API,再决定是否让缓存模型依赖它。

Redis Hash、字符串JSON与RedisJSON围绕更新粒度、读取方式和部署能力的选择关系说明图
图2:三种对象存储方式的选择说明图,连接访问粒度、序列化边界、JSON路径更新和部署能力,不是运行截图。

过期和并发是最后的落点

最稳妥的 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 更适合作为缓存或加速层,主数据仍应放在适合该职责的存储中。

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