首页/数据/Redis 进阶速查/对象缓存:String 存 JSON 与 Hash 的取舍
数据结构与选型42 条

对象缓存:String 存 JSON 与 Hash 的取舍

整对象读写用 String 加 JSON 简单直接;需要频繁更新单字段或字段级原子递增时用 Hash,避免每次读改写整个序列化对象。

Redis 进阶速查数据更新于 2026-10-13

对象缓存怎么存

缓存一个业务对象有两种常见做法:整体序列化成字符串存进 String,或拆成字段存进 Hash。前者实现简单,读取一次就能拿到完整对象,解析工作交给应用;后者支持只取部分字段、只更新某个字段,并在字段较少时享受紧凑编码带来的内存优势。选择的关键在于读写模式:整对象读写为主就用 String,单字段频繁更新或需要按字段过期就用 Hash。

基本信息

String 存法SET key '<序列化内容>' EX <ttl>,应用负责序列化与反序列化
Hash 存法HSET key field value ...,配合 HMGET 取部分字段
字段级原子操作HINCRBY 递增数值字段,HSETNX 仅在字段不存在时写入
字段级过期7.0 起支持对单个字段设置过期时间
内存差异字段较少的 Hash 使用紧凑编码,通常比序列化字符串更省

典型使用场景

  • 商品详情与用户资料:整对象读取为主,String 加序列化最直接
  • 计数器与状态位集合:一个主体的多种计数用 Hash 合并到一个键,字段级原子递增
  • 购物车:按商品维度增删改,Hash 的字段操作天然贴合
  • 配置与开关集合:字段数量可控、读多写少,Hash 便于单独读取与更新
  • 需要部分字段独立过期的场景:例如限时优惠标记与基础资料共存在一个键里

两种写法对照

两种写法对照
redis-cli SET user:1001 '{"name":"alice","age":30}' EX 600   # 整对象序列化
redis-cli GET user:1001
redis-cli HSET user:1002 name alice age 30                    # 字段化存储
redis-cli HMGET user:1002 name age                            # 只取需要的字段
redis-cli HSET user:1002 age 31                               # 单字段更新,无需读改写整条
redis-cli HINCRBY user:1002 login_count 1                     # 字段级原子递增
redis-cli OBJECT ENCODING user:1002                           # 字段少时应为 listpack

两种存法对比

维度String 加序列化Hash 字段
整对象读取一次 GET 拿到全部HGETALL 取全部字段
部分字段读取需要全部取回后在应用侧筛选HMGET 只取需要的字段
单字段更新读改写整条,存在并发覆盖风险HSET 直接改一个字段
字段级过期不支持7.0 起支持按字段设置过期
内存序列化体积大时偏高字段少时紧凑编码更省
字段极少的对象用 Hash 未必更省:每个键本身都有对象头与字典条目的固定开销,如果对象只有一两个字段,键数量翻倍带来的固定开销可能超过序列化字符串的体积优势。判断标准是对象字段数与键总数,而不是只看单键占用。
String 存序列化内容在并发更新时有两个写者互相覆盖的风险:两个请求各自读回旧值、修改不同字段、再整体写回,后写的会覆盖前者的修改。需要字段级并发更新时,Hash 的字段操作或乐观锁版本号才是正确做法。

易错点

  • 把每个字段都做成独立键,键数量暴涨带来大量固定开销与更多往返,应按对象聚合为 Hash
  • 用 String 存对象却需要频繁更新单个字段,每次都要读回整条再写回,并发下容易互相覆盖
  • 序列化内容过大时仍用 String 存,反序列化开销与网络传输量都偏高,可评估压缩或改用 Hash 存热点字段
  • 字段数增长到远超阈值后仍当作小对象估算内存,编码升级带来的开销被完全忽略

数据来源与参考

  • Redis 官方文档:Hashes 数据类型
  • Redis 官方文档:HINCRBY / HSETNX 命令
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌