第六篇:Redis Hash——一个用户对象应该保存成 JSON,还是保存成 Hash?

上一篇学习了 Redis 中最常用的数据类型:

复制代码
String

我们知道,String 不只能保存普通文字,还可以保存:

复制代码
数字
验证码
Token
JSON
计数器
二进制数据

例如,将一个用户对象转换成 JSON 后保存:

复制代码
SET ark:user:info:1001 "{\"id\":1001,\"name\":\"张三\",\"age\":30}"

Redis 中的数据可以理解为:

复制代码
ark:user:info:1001
        ↓
{"id":1001,"name":"张三","age":30}

这种方式非常常见。

但是,如果现在只想修改用户年龄:

复制代码
30 → 31

使用 String JSON 时,通常需要:

复制代码
读取完整 JSON
→ 反序列化成 User
→ 修改 age
→ 重新序列化
→ 覆盖整个 JSON

这时就会产生一个问题:

Redis 有没有一种数据类型,可以把一个对象拆成多个字段保存,并单独读取或修改某个字段?

答案就是:

Hash

这一篇重点讲清楚:

复制代码
Redis Hash 到底是什么
Hash 的 Key、Field、Value 分别代表什么
HSET、HGET、HMGET、HGETALL 如何使用
Hash 怎样保存一个用户对象
如何单独修改对象中的一个字段
Hash 和 String JSON 有什么区别
Hash 能不能设置过期时间
新版 Redis 如何给单独 Field 设置 TTL
Spring Boot 如何操作 Hash
用户缓存到底应该选择 JSON 还是 Hash

一、Redis Hash 是什么

Redis Hash 是一个由多个 Field-Value 组成的数据结构。

可以把它表示为:

复制代码
Redis Key
├── Field 1 → Value 1
├── Field 2 → Value 2
├── Field 3 → Value 3
└── Field 4 → Value 4

例如保存用户 1001:

复制代码
ark:user:info:1001
├── id    → 1001
├── name  → 张三
├── age   → 30
└── phone → 13800000000

这里有三层概念:

复制代码
ark:user:info:1001
→ Redis Key

name、age、phone
→ Hash Field

张三、30、13800000000
→ Hash Value

Redis 官方将 Hash 描述为一种由 Field-Value 对组成的记录结构,适合表示基础对象,也可以用于保存一组相关计数器。


二、Hash 和 Java Map 很像

Redis Hash 可以类比成 Java 中嵌套的一层 Map:

复制代码
Map<String, Map<String, String>> redis = new HashMap<>();

其中外层 Map 的 Key 是 Redis Key:

复制代码
ark:user:info:1001

内层 Map 保存用户字段:

复制代码
Map<String, String> userFields = new HashMap<>();

userFields.put("id", "1001");
userFields.put("name", "张三");
userFields.put("age", "30");
userFields.put("phone", "13800000000");

整体可以理解为:

复制代码
redis.put(
        "ark:user:info:1001",
        userFields
);

读取姓名:

复制代码
String name = redis
        .get("ark:user:info:1001")
        .get("name");

Redis 中对应:

复制代码
HGET ark:user:info:1001 name

虽然使用方式类似,但仍然要记住:

复制代码
Java Map
→ 位于当前 JVM 进程

Redis Hash
→ 位于 Redis Server 进程

Java 必须通过 Redis 客户端发送命令,才能读取或修改 Hash。


三、Hash 中的 Key 和 Field 不要混淆

假设执行:

复制代码
HSET ark:user:info:1001 name 张三

这里:

复制代码
ark:user:info:1001
→ Redis Key

name
→ Hash Field

张三
→ Field 对应的 Value

很多初学者容易把:

复制代码
name

也称为 Redis Key。

更准确的说法是:

复制代码
ark:user:info:1001
→ Key

name
→ Field,也可以叫 Hash Key

在 Spring Data Redis 中,泛型名称有时会写成:

复制代码
hashKey

它指的就是 Hash 内部的 Field,而不是最外层 Redis Key。

完整结构是:

复制代码
Redis Key
    ↓
Hash
├── Field → Value
├── Field → Value
└── Field → Value

四、HSET:向 Hash 中写入字段

Redis Hash 最基础的写入命令是:

复制代码
HSET key field value

例如:

复制代码
HSET ark:user:info:1001 name 张三

此时 Redis 中形成:

复制代码
ark:user:info:1001
└── name → 张三

继续写入年龄:

复制代码
HSET ark:user:info:1001 age 30

现在变成:

复制代码
ark:user:info:1001
├── name → 张三
└── age  → 30

继续写入手机号:

复制代码
HSET ark:user:info:1001 phone 13800000000

结果:

复制代码
ark:user:info:1001
├── name  → 张三
├── age   → 30
└── phone → 13800000000

Redis 的 HSET 可以一次写入一个或多个 Field;当指定 Field 已经存在时,新值会覆盖旧值。


五、一次写入多个字段

不需要为每个字段分别发送一次命令。

可以一次写入多个 Field:

复制代码
HSET ark:user:info:1001 \
    id 1001 \
    name 张三 \
    age 30 \
    phone 13800000000

整理后就是:

复制代码
HSET key
    field1 value1
    field2 value2
    field3 value3

一次命令写入多个字段,可以减少客户端和 Redis 之间的网络往返。

如果分别执行:

复制代码
HSET ark:user:info:1001 id 1001
HSET ark:user:info:1001 name 张三
HSET ark:user:info:1001 age 30
HSET ark:user:info:1001 phone 13800000000

就产生了四次 Redis 请求。

一次执行:

复制代码
HSET ark:user:info:1001 \
    id 1001 \
    name 张三 \
    age 30 \
    phone 13800000000

通常只需要一次请求。


六、HGET:读取一个字段

读取 Hash 中的单个字段:

复制代码
HGET key field

例如:

复制代码
HGET ark:user:info:1001 name

返回:

复制代码
张三

读取年龄:

复制代码
HGET ark:user:info:1001 age

返回:

复制代码
30

读取不存在的 Field:

复制代码
HGET ark:user:info:1001 address

返回空结果。

这时可能存在两种情况:

复制代码
整个 Redis Key 不存在
或者
Redis Key 存在,但 address Field 不存在

单纯通过 HGET 返回空,通常不能区分这两种情况。


七、HMGET:一次读取多个字段

如果同时需要姓名和年龄,不建议分别执行:

复制代码
HGET ark:user:info:1001 name
HGET ark:user:info:1001 age

可以使用:

复制代码
HMGET ark:user:info:1001 name age

返回结果会按照 Field 参数的顺序排列:

复制代码
1) 张三
2) 30

如果其中一个 Field 不存在:

复制代码
HMGET ark:user:info:1001 name address age

可能返回:

复制代码
1) 张三
2) nil
3) 30

HMGET 可以在一次命令中读取多个字段,从而减少多次单字段查询带来的网络往返。


八、HGETALL:读取整个 Hash

读取全部 Field 和 Value:

复制代码
HGETALL ark:user:info:1001

返回内容类似:

复制代码
1) id
2) 1001
3) name
4) 张三
5) age
6) 30
7) phone
8) 13800000000

可以整理成:

复制代码
id    → 1001
name  → 张三
age   → 30
phone → 13800000000

在 Spring Boot 中,通常会转换成:

复制代码
Map<Object, Object>

或者:

复制代码
Map<String, String>

HGETALL 很适合读取字段数量较少的对象。

但不能看到 Hash 就无脑使用 HGETALL


九、为什么大 Hash 要谨慎使用 HGETALL

假设一个 Hash 中只有:

复制代码
4 个字段

执行 HGETALL 没有什么明显问题。

但如果某个 Hash 中有:

复制代码
10 万个 Field

执行:

复制代码
HGETALL huge:hash

会一次性返回大量数据。

这可能带来:

复制代码
Redis 处理时间变长
网络传输数据过大
客户端内存占用增加
其他 Redis 请求受到影响

因此:

复制代码
小对象
→ 可以使用 HGETALL

大型 Hash
→ 优先按需 HGET、HMGET,或者使用 HSCAN 分批遍历

不要把大量无边界增长的数据全部塞进一个 Hash,再频繁执行 HGETALL


十、HEXISTS:判断 Field 是否存在

判断 Hash 中是否存在某个 Field:

复制代码
HEXISTS ark:user:info:1001 phone

如果存在,返回:

复制代码
1

不存在则返回:

复制代码
0

需要注意:

复制代码
EXISTS
→ 判断最外层 Redis Key 是否存在

HEXISTS
→ 判断 Hash 内部 Field 是否存在

例如:

复制代码
EXISTS ark:user:info:1001

判断:

复制代码
整个用户 Hash 是否存在

而:

复制代码
HEXISTS ark:user:info:1001 phone

判断:

复制代码
用户 Hash 中是否有 phone 字段

十一、HDEL:删除一个或多个字段

删除手机号字段:

复制代码
HDEL ark:user:info:1001 phone

执行前:

复制代码
ark:user:info:1001
├── id    → 1001
├── name  → 张三
├── age   → 30
└── phone → 13800000000

执行后:

复制代码
ark:user:info:1001
├── id   → 1001
├── name → 张三
└── age  → 30

也可以一次删除多个字段:

复制代码
HDEL ark:user:info:1001 phone age

这里删除的是 Hash 内部字段,不是整个 Redis Key。

删除整个 Hash 仍然使用:

复制代码
DEL ark:user:info:1001

需要区分:

复制代码
HDEL
→ 删除 Hash 内部一个或多个 Field

DEL
→ 删除整个 Redis Key,包括整个 Hash

十二、Hash 中所有 Field 删除后会怎样

假设一个 Hash 只有一个字段:

复制代码
ark:test:hash
└── name → 张三

执行:

复制代码
HDEL ark:test:hash name

最后一个 Field 被删除后,这个 Hash Key 也会随之消失。

此时执行:

复制代码
EXISTS ark:test:hash

会返回:

复制代码
0

因为 Redis 不会保留一个完全为空的 Hash。

可以理解为:

复制代码
Hash 中还有 Field
→ Key 存在

最后一个 Field 被删除
→ 整个 Key 消失

十三、HLEN:查看 Hash 有多少字段

查看用户 Hash 中的字段数量:

复制代码
HLEN ark:user:info:1001

如果当前有:

复制代码
id
name
age
phone

返回:

复制代码
4

HLEN 统计的是:

复制代码
Field 数量

而不是:

复制代码
Value 字符数量
或者
Redis Key 数量

例如:

复制代码
ark:user:info:1001
├── id
├── name
├── age
└── phone

HLEN 返回 4。


十四、HKEYS 和 HVALS

获取所有 Field:

复制代码
HKEYS ark:user:info:1001

可能返回:

复制代码
id
name
age
phone

获取所有 Value:

复制代码
HVALS ark:user:info:1001

可能返回:

复制代码
1001
张三
30
13800000000

一般来说:

复制代码
HKEYS
→ 只需要字段名时使用

HVALS
→ 只需要字段值时使用

HGETALL
→ Field 和 Value 都需要时使用

如果 Hash 很大,这几个全量读取命令都要谨慎。


十五、HINCRBY:修改 Hash 中的数字字段

Hash Field 的 Value 也可以保存数字。

例如用户统计信息:

复制代码
ark:user:stats:1001
├── loginCount → 10
├── score      → 80
└── failCount  → 2

写入:

复制代码
HSET ark:user:stats:1001 \
    loginCount 10 \
    score 80 \
    failCount 2

登录次数加一:

复制代码
HINCRBY ark:user:stats:1001 loginCount 1

结果:

复制代码
loginCount → 11

积分增加 20:

复制代码
HINCRBY ark:user:stats:1001 score 20

结果:

复制代码
score → 100

减少也可以传负数:

复制代码
HINCRBY ark:user:stats:1001 score -10

结果:

复制代码
score → 90

Redis Hash 支持通过 HINCRBY 原子修改数字 Field,因此一个 Hash 也很适合保存一组相关计数器。


十六、HINCRBY 遇到不存在的 Field

假设 Hash 中没有:

复制代码
shareCount

执行:

复制代码
HINCRBY ark:user:stats:1001 shareCount 1

Redis 会将不存在的 Field 当作初始值 0:

复制代码
0 + 1 = 1

然后创建:

复制代码
shareCount → 1

因此不需要提前:

复制代码
HSET ark:user:stats:1001 shareCount 0

可以直接执行 HINCRBY


十七、HINCRBY 遇到非数字 Value

假设:

复制代码
HSET ark:user:info:1001 name 张三

然后执行:

复制代码
HINCRBY ark:user:info:1001 name 1

Redis 无法把"张三"解析成整数,会返回错误。

所以:

复制代码
HINCRBY
→ 只能操作能够表示整数的 Field Value

例如:

复制代码
"10"
→ 可以

"-5"
→ 可以

"张三"
→ 不可以

"{\"count\":10}"
→ 不可以

十八、用 Hash 保存一个用户对象

假设 Java 用户对象是:

复制代码
public record User(
        Long id,
        String name,
        Integer age,
        String phone
) {
}

使用 Hash 保存时,可以设计:

复制代码
Redis Key:
ark:user:info:1001

字段:

复制代码
id    → 1001
name  → 张三
age   → 30
phone → 13800000000

Redis 命令:

复制代码
HSET ark:user:info:1001 \
    id 1001 \
    name 张三 \
    age 30 \
    phone 13800000000

读取姓名:

复制代码
HGET ark:user:info:1001 name

修改年龄:

复制代码
HSET ark:user:info:1001 age 31

删除手机号:

复制代码
HDEL ark:user:info:1001 phone

读取全部:

复制代码
HGETALL ark:user:info:1001

这就是 Hash 保存对象的基本方式。


十九、同一个对象也可以保存成 String JSON

用户对象也可以转换成 JSON:

复制代码
{
  "id": 1001,
  "name": "张三",
  "age": 30,
  "phone": "13800000000"
}

然后使用 String 保存:

复制代码
SET ark:user:info:1001 "{\"id\":1001,\"name\":\"张三\",\"age\":30,\"phone\":\"13800000000\"}"

Redis 中的数据是:

复制代码
ark:user:info:1001
        ↓
一整段 JSON 字符串

读取:

复制代码
GET ark:user:info:1001

此时 Redis 不理解 JSON 内部的:

复制代码
id
name
age
phone

对于 Redis 来说,它只是一整段 String Value。


二十、String JSON 和 Hash 的结构区别

String JSON

复制代码
ark:user:info:1001
        ↓
{"id":1001,"name":"张三","age":30}

整个对象是一个 Value。

Hash

复制代码
ark:user:info:1001
├── id   → 1001
├── name → 张三
└── age  → 30

对象被拆成多个 Field-Value。

最直接的区别是:

复制代码
String JSON
→ Redis 只看到一整段数据

Hash
→ Redis 能单独操作每个字段

二十一、修改一个字段时有什么区别

假设要把年龄:

复制代码
30

修改为:

复制代码
31

String JSON 的修改过程

通常需要:

复制代码
GET 完整 JSON
        ↓
反序列化成 User
        ↓
修改 age
        ↓
重新序列化成 JSON
        ↓
SET 覆盖完整 Value

伪代码:

复制代码
String json = stringRedisTemplate
        .opsForValue()
        .get(key);

User user = objectMapper.readValue(
        json,
        User.class
);

User updatedUser = new User(
        user.id(),
        user.name(),
        31,
        user.phone()
);

String newJson = objectMapper
        .writeValueAsString(updatedUser);

stringRedisTemplate.opsForValue()
        .set(key, newJson);

Hash 的修改过程

直接执行:

复制代码
HSET ark:user:info:1001 age 31

Spring Boot 中:

复制代码
stringRedisTemplate.opsForHash()
        .put(
                key,
                "age",
                "31"
        );

所以:

复制代码
经常单独修改字段
→ Hash 更方便

二十二、读取完整对象时有什么区别

String JSON

只需要一次 GET:

复制代码
GET ark:user:info:1001

Java 得到 JSON 后,直接反序列化成 User。

复制代码
Redis GET
→ 得到完整 JSON
→ 转换成 User

对象结构、嵌套关系和数据类型都可以由 JSON 表达。

Hash

执行:

复制代码
HGETALL ark:user:info:1001

Java 得到:

复制代码
Map<String, String>

然后还要手动转换:

复制代码
"id"    → Long
"age"   → Integer
"name"  → String
"phone" → String

因此:

复制代码
每次都读取完整对象
→ String JSON 通常更直接

二十三、复杂嵌套对象更适合哪一种

假设用户对象中还有地址:

复制代码
{
  "id": 1001,
  "name": "张三",
  "address": {
    "province": "江苏省",
    "city": "苏州市",
    "detail": "工业园区"
  }
}

String JSON 可以直接保存整个结构:

复制代码
用户
├── id
├── name
└── address
    ├── province
    ├── city
    └── detail

Hash 本身不支持无限嵌套的 Hash 结构。

Hash 的 Field 对应的 Value 仍然是一段字符串或字节数据。

如果想使用 Hash 保存地址,可能需要:

方案一:地址再次转 JSON

复制代码
ark:user:info:1001
├── id      → 1001
├── name    → 张三
└── address → {"province":"江苏省","city":"苏州市"}

方案二:展开字段

复制代码
ark:user:info:1001
├── id               → 1001
├── name             → 张三
├── address:province → 江苏省
├── address:city     → 苏州市
└── address:detail   → 工业园区

方案三:拆成另一个 Hash

复制代码
ark:user:info:1001
├── id
└── name

ark:user:address:1001
├── province
├── city
└── detail

对象结构越复杂,String JSON 通常越自然。


二十四、JSON 能保留字段类型,Hash 可以吗

JSON 中可以表达:

复制代码
{
  "id": 1001,
  "enabled": true,
  "score": 95.5,
  "name": "张三"
}

反序列化时可以恢复成:

复制代码
id      → Long
enabled → Boolean
score   → Double
name    → String

而使用 StringRedisTemplate 操作 Hash 时,Field 和 Value 通常都是字符串:

复制代码
id      → "1001"
enabled → "true"
score   → "95.5"
name    → "张三"

读取后需要根据业务字段自己转换类型:

复制代码
Long id = Long.valueOf(
        values.get("id")
);

Boolean enabled = Boolean.valueOf(
        values.get("enabled")
);

因此对于标准 Java 对象缓存:

复制代码
String JSON
→ 对象转换通常更统一

Hash
→ 字段级操作更方便,但类型转换更分散

二十五、Hash 能不能设置 TTL

当然可以。

最常见的方式是给整个 Hash Key 设置 TTL:

复制代码
EXPIRE ark:user:info:1001 1800

表示:

复制代码
整个用户 Hash
→ 30 分钟后过期

查看:

复制代码
TTL ark:user:info:1001

可能返回:

复制代码
1768

当 Key 过期后,整个 Hash 消失:

复制代码
id
name
age
phone

都会一起失效。

传统 Redis 使用方式中,TTL 主要设置在最外层 Key 上。


二十六、HSET 会不会清除整个 Hash 的 TTL

这一点和普通 String 的 SET 不一样。

假设:

复制代码
HSET ark:user:info:1001 name 张三 age 30
EXPIRE ark:user:info:1001 1800

此时整个 Hash 有 1800 秒 TTL。

接着更新年龄:

复制代码
HSET ark:user:info:1001 age 31

通常情况下,Hash Key 原来的 TTL 会继续保留。

也就是说:

复制代码
HSET 修改 Hash 内部字段
→ 不会像普通 SET 覆盖整个 Key 那样自动清除 Key TTL

但如果先执行:

复制代码
DEL ark:user:info:1001

然后重新创建 Hash,原来的 TTL 自然也不存在了。


二十七、Hash 的每个 Field 能单独设置 TTL 吗

这个问题需要根据 Redis 版本回答。

传统版本的 Redis

过去常见的规则是:

复制代码
TTL 只能设置在整个 Hash Key 上
不能直接给单个 Field 设置 TTL

例如:

复制代码
ark:user:info:1001
├── name  → 张三
├── age   → 30
└── phone → 13800000000

设置:

复制代码
EXPIRE ark:user:info:1001 1800

代表所有 Field 一起过期。

Redis 7.4 及以后

Redis 7.4 开始支持 Hash Field 级别的过期时间,可以通过 HEXPIREHPEXPIRE 等命令给一个或多个 Field 单独设置 TTL。Field 到期后会从 Hash 中自动删除。

例如:

复制代码
HSET ark:user:info:1001 \
    name 张三 \
    tempCode 9527

只给 tempCode 设置 300 秒 TTL:

复制代码
HEXPIRE ark:user:info:1001 300 \
    FIELDS 1 tempCode

查看 Field TTL:

复制代码
HTTL ark:user:info:1001 \
    FIELDS 1 tempCode

300 秒后:

复制代码
tempCode Field 消失
name Field 仍然存在

因此,文章或面试中不要再绝对地说:

复制代码
Redis Hash 永远不能给单独 Field 设置 TTL

更准确的表达是:

传统 Redis 版本主要支持 Key 级 TTL;Redis 7.4 开始增加了 Hash Field 级过期能力,但实际项目能否使用,还要看服务端版本和客户端支持情况。


二十八、Field TTL 是否适合普通用户缓存

虽然新版 Redis 支持单独 Field TTL,但普通用户对象缓存通常没有必要让每个字段分别过期。

例如:

复制代码
name 30 分钟
age 20 分钟
phone 40 分钟
address 10 分钟

这种设计会让数据生命周期变得复杂。

读取完整用户时,可能出现:

复制代码
id 存在
name 存在
phone 已过期
address 已过期

最终得到一个字段残缺的用户对象。

对于普通对象缓存,更常见的做法仍然是:

复制代码
整个对象统一缓存
整个 Key 统一过期

Field TTL 更适合一些字段本身具有独立生命周期的场景,例如:

复制代码
会话中的临时属性
按字段保存的短期状态
事件或传感器数据
独立过期的计数信息

不能因为 Redis 支持,就给每个普通对象字段都设计一套 TTL。


二十九、Hash 和 String JSON 对比

对比项 String JSON Hash
数据形式 整个对象一段 JSON 对象拆成 Field-Value
读取完整对象 一个 GET HGETALL 或 HMGET
读取单个字段 通常要读取完整 JSON HGET
修改单个字段 通常重新写完整 JSON HSET
Java 对象转换 JSON 序列化较自然 通常需要 Map 转对象
嵌套对象 表达方便 需要 JSON、展开或拆 Key
字段类型 JSON 可以表达类型 常按字符串处理
整体 TTL 支持 支持
Field TTL 不适用 Redis 7.4+ 支持
适合场景 完整对象缓存 字段独立读写、统计数据

三十、什么情况下优先选择 String JSON

以下场景更适合 String JSON:

1. 每次都读取完整对象

例如用户详情接口:

复制代码
GET /user/1001

接口需要返回完整用户信息。

流程:

复制代码
GET Redis JSON
→ 反序列化成 User
→ 返回

2. 对象结构比较复杂

例如对象中存在:

复制代码
嵌套对象
List
Map
地址
权限列表
角色列表

JSON 表达更加自然。

3. 业务通常整体更新对象

例如:

复制代码
数据库用户信息更新
→ 删除整个缓存
→ 下次重新加载完整对象

这种 Cache Aside 模式,不需要在 Redis 中频繁单独修改字段。

4. 多个服务之间已经统一 JSON 格式

Java、Node.js、Python 都可以理解相同 JSON。

因此完整对象缓存中,String JSON 是非常常见的方案。


三十一、什么情况下优先选择 Hash

以下场景更适合 Hash:

1. 经常只读取单个字段

例如只查询用户昵称:

复制代码
HGET ark:user:info:1001 name

不用获取整个对象。

2. 经常单独更新字段

例如:

复制代码
登录次数
积分
状态
最后访问时间
失败次数

可以直接:

复制代码
HSET ark:user:info:1001 status 1

或者:

复制代码
HINCRBY ark:user:stats:1001 loginCount 1

3. 数据天然就是一组 Field-Value

例如运行状态:

复制代码
ark:device:status:2001
├── online      → 1
├── battery     → 80
├── temperature → 32
└── mode        → working

设备的多个状态字段可能独立更新。

4. 保存一组相关计数器

例如:

复制代码
ark:article:stats:2001
├── view     → 1000
├── like     → 80
├── comment  → 30
└── favorite → 20

可以分别使用 HINCRBY 更新。


三十二、用户信息到底选 JSON 还是 Hash

没有绝对答案。

要看实际访问方式。

方案一:用户完整信息缓存

如果业务通常是:

复制代码
按用户 ID 查询完整对象
数据库更新后删除缓存
下一次重新加载

推荐:

复制代码
String + JSON

例如:

复制代码
ark:user:info:1001
→ 完整用户 JSON

原因是:

复制代码
对象序列化简单
读取完整对象方便
嵌套结构容易表达
缓存删除和重建清晰

方案二:用户运行状态或统计

如果业务经常独立更新:

复制代码
登录次数
登录失败次数
在线状态
最后操作时间
积分

推荐单独设计 Hash:

复制代码
ark:user:stats:1001
├── loginCount
├── failCount
├── score
└── lastActiveTime

原因是:

复制代码
字段可以独立修改
数字字段可以原子自增
不用反复读取完整 JSON

实际项目可以同时使用:

复制代码
ark:user:info:1001
→ String JSON,缓存完整用户信息

ark:user:stats:1001
→ Hash,保存动态统计字段

不是必须二选一。


三十三、不要把数据库表机械地复制成 Hash

假设 MySQL 有一张 user 表:

复制代码
id
name
phone
password
status
create_time
update_time
deleted

不能因为 Hash 能保存字段,就把所有数据库字段全部复制到 Redis:

复制代码
ark:user:info:1001
├── id
├── name
├── phone
├── password
├── status
├── create_time
├── update_time
└── deleted

需要考虑:

复制代码
哪些字段真的需要缓存
哪些字段存在安全风险
哪些字段业务根本不会读取
哪些字段变化频繁
哪些字段适合放在一起

尤其不能把:

复制代码
密码
敏感密钥
不必要的个人隐私

随意保存进缓存。

Redis 数据设计不是数据库表的简单复制。


三十四、一个 Hash 不要保存无限增长的数据

例如将所有用户放进同一个 Hash:

复制代码
ark:all:users
├── 1001 → 张三
├── 1002 → 李四
├── 1003 → 王五
├── ...
└── 几百万用户

这种设计可能形成一个超大 Hash。

问题包括:

复制代码
单个 Key 数据量过大
迁移和备份成本增加
全量读取风险增大
删除和扫描成本增加
集群中一个 Key 只能落在一个槽位

更常见的是:

复制代码
一个用户一个 Key

例如:

复制代码
ark:user:info:1001
ark:user:info:1002
ark:user:info:1003

或者根据明确业务合理分组。

不要为了减少 Key 数量,就把所有数据无边界地塞入一个 Hash。


三十五、Spring Boot 如何写入一个 Hash 字段

使用:

复制代码
stringRedisTemplate.opsForHash()

写入单个字段:

复制代码
@Service
public class UserRedisService {

    private final StringRedisTemplate stringRedisTemplate;

    public UserRedisService(
            StringRedisTemplate stringRedisTemplate
    ) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public void saveUserName(
            Long userId,
            String name
    ) {
        String key = "ark:user:info:" + userId;

        stringRedisTemplate.opsForHash()
                .put(
                        key,
                        "name",
                        name
                );
    }
}

底层对应:

复制代码
HSET ark:user:info:1001 name 张三

三十六、Spring Boot 如何一次写入多个字段

准备 Map:

复制代码
public void saveUser(User user) {
    String key = "ark:user:info:" + user.id();

    Map<String, String> values = new HashMap<>();

    values.put(
            "id",
            String.valueOf(user.id())
    );

    values.put(
            "name",
            user.name()
    );

    values.put(
            "age",
            String.valueOf(user.age())
    );

    values.put(
            "phone",
            user.phone()
    );

    stringRedisTemplate.opsForHash()
            .putAll(key, values);
}

底层效果类似:

复制代码
HSET ark:user:info:1001 \
    id 1001 \
    name 张三 \
    age 30 \
    phone 13800000000

Spring Data Redis 的 HashOperations 提供了读取、写入、删除、计数、获取全部 Entry 和扫描等 Hash 操作封装。


三十七、写入 Hash 后设置过期时间

复制代码
public void saveUser(
        User user,
        long timeout,
        TimeUnit unit
) {
    String key = "ark:user:info:" + user.id();

    Map<String, String> values = new HashMap<>();

    values.put("id", String.valueOf(user.id()));
    values.put("name", user.name());
    values.put("age", String.valueOf(user.age()));
    values.put("phone", user.phone());

    stringRedisTemplate.opsForHash()
            .putAll(key, values);

    stringRedisTemplate.expire(
            key,
            timeout,
            unit
    );
}

调用:

复制代码
saveUser(
        user,
        30,
        TimeUnit.MINUTES
);

这里需要注意:

复制代码
putAll
和
expire

仍然是两次 Redis 操作。

如果第一步成功、第二步失败,Hash 可能没有 TTL。

对于普通缓存,可以通过事务、Pipeline、Lua,或者统一的缓存封装进一步处理。

当前阶段先理解:

复制代码
Hash 写入字段
→ HSET

整个 Hash 设置 TTL
→ EXPIRE

三十八、Spring Boot 如何读取一个字段

复制代码
public String getUserName(Long userId) {
    String key = "ark:user:info:" + userId;

    Object value = stringRedisTemplate
            .opsForHash()
            .get(
                    key,
                    "name"
            );

    return value == null
            ? null
            : value.toString();
}

底层对应:

复制代码
HGET ark:user:info:1001 name

三十九、Spring Boot 如何读取多个字段

复制代码
public List<Object> getUserBaseInfo(
        Long userId
) {
    String key = "ark:user:info:" + userId;

    List<Object> fields = List.of(
            "name",
            "age",
            "phone"
    );

    return stringRedisTemplate.opsForHash()
            .multiGet(
                    key,
                    fields
            );
}

底层类似:

复制代码
HMGET ark:user:info:1001 name age phone

返回顺序与传入 Field 顺序一致。


四十、Spring Boot 如何读取整个 Hash

复制代码
public Map<Object, Object> getUserHash(
        Long userId
) {
    String key = "ark:user:info:" + userId;

    return stringRedisTemplate.opsForHash()
            .entries(key);
}

返回:

复制代码
{
    id=1001,
    name=张三,
    age=30,
    phone=13800000000
}

然后可以转换成 User:

复制代码
public User getUser(Long userId) {
    Map<Object, Object> values =
            getUserHash(userId);

    if (values.isEmpty()) {
        return null;
    }

    return new User(
            Long.valueOf(
                    values.get("id").toString()
            ),
            values.get("name").toString(),
            Integer.valueOf(
                    values.get("age").toString()
            ),
            values.get("phone").toString()
    );
}

这种手动转换适合帮助理解。

正式项目可以进一步封装映射逻辑,避免各个业务类重复转换。


四十一、Spring Boot 如何修改一个字段

复制代码
public void updateUserAge(
        Long userId,
        Integer age
) {
    String key = "ark:user:info:" + userId;

    stringRedisTemplate.opsForHash()
            .put(
                    key,
                    "age",
                    String.valueOf(age)
            );
}

底层对应:

复制代码
HSET ark:user:info:1001 age 31

这里不会读取和重写其他字段。


四十二、Spring Boot 如何删除字段

复制代码
public long deleteUserFields(
        Long userId,
        String... fields
) {
    String key = "ark:user:info:" + userId;

    Long deleted = stringRedisTemplate
            .opsForHash()
            .delete(
                    key,
                    (Object[]) fields
            );

    return deleted == null ? 0L : deleted;
}

调用:

复制代码
deleteUserFields(
        1001L,
        "phone",
        "age"
);

底层类似:

复制代码
HDEL ark:user:info:1001 phone age

四十三、Spring Boot 如何进行 Hash 自增

用户积分增加 10:

复制代码
public long increaseScore(
        Long userId,
        long delta
) {
    String key = "ark:user:stats:" + userId;

    Long result = stringRedisTemplate
            .opsForHash()
            .increment(
                    key,
                    "score",
                    delta
            );

    return result == null ? 0L : result;
}

底层对应:

复制代码
HINCRBY ark:user:stats:1001 score 10

这种方式比:

复制代码
HGET score
→ Java 加 10
→ HSET score

更加安全,因为 Redis 在服务端完成原子自增。


四十四、封装 Hash Key

继续使用统一 Key 构建类:

复制代码
public final class RedisKeys {

    private static final String PROJECT = "ark";

    private RedisKeys() {
    }

    public static String userInfo(Long userId) {
        return PROJECT
                + ":user:info:"
                + userId;
    }

    public static String userStats(Long userId) {
        return PROJECT
                + ":user:stats:"
                + userId;
    }
}

使用:

复制代码
String key = RedisKeys.userInfo(userId);

不要在各个 Service 中到处手写:

复制代码
"ark:user:info:" + userId

统一封装可以减少:

复制代码
拼写错误
前缀不一致
模块命名混乱
后期批量修改困难

四十五、常见错误一:Key 和 Field 顺序写反

正确:

复制代码
HSET ark:user:info:1001 name 张三

结构:

复制代码
HSET key field value

错误理解可能写成:

复制代码
HSET name ark:user:info:1001 张三

这样 Redis 会把:

复制代码
name

当成外层 Redis Key。

结果形成:

复制代码
name
└── ark:user:info:1001 → 张三

虽然命令可能执行成功,但数据结构已经完全错误。


四十六、常见错误二:把 Hash 当成嵌套对象

Hash 只有一层 Field-Value:

复制代码
Key
├── Field → Value
└── Field → Value

它不是:

复制代码
Key
└── Field
    └── 子 Field
        └── 更深层 Field

如果 Value 是 JSON:

复制代码
address → {"province":"江苏省"}

Redis 默认只把它当成一整段 Value,不会自动理解 JSON 内部结构。


四十七、常见错误三:频繁 HGETALL 大 Hash

如果只需要:

复制代码
name
和
age

不要为了方便总是执行:

复制代码
HGETALL ark:user:info:1001

更适合:

复制代码
HMGET ark:user:info:1001 name age

原则是:

复制代码
需要什么字段
→ 获取什么字段

避免无意义地传输整个大 Hash。


四十八、常见错误四:把所有用户放进一个 Hash

不推荐:

复制代码
ark:users
├── 1001 → 用户 JSON
├── 1002 → 用户 JSON
├── 1003 → 用户 JSON
└── 无限增长

更常见:

复制代码
ark:user:info:1001
ark:user:info:1002
ark:user:info:1003

一个超大 Hash 会增加维护、迁移和故障处理难度。


四十九、常见错误五:看到 Hash 就认为一定比 JSON 省内存

Hash 和 String JSON 谁更节省内存,没有一个脱离数据规模、字段数量、Redis 编码方式和 Value 内容的绝对结论。

不能简单记成:

复制代码
Hash 一定更省内存

或者:

复制代码
JSON 一定更省内存

实际设计首先考虑:

复制代码
访问模式
更新方式
对象复杂度
开发成本
数据规模

再通过压测和内存分析验证。


五十、常见错误六:缓存对象时直接修改 Hash,却不更新 MySQL

假设:

复制代码
HSET ark:user:info:1001 name 李四

只修改了 Redis。

MySQL 中仍然是:

复制代码
张三

以后缓存过期,重新查询 MySQL:

复制代码
名字又变回张三

因为 Redis 通常只是缓存,MySQL 才是正式数据源。

正常业务通常是:

复制代码
客户端提交修改
        ↓
更新 MySQL
        ↓
删除 Redis 缓存
        ↓
下一次重新加载

而不是只修改 Redis 中的用户缓存。

Hash 可以方便地修改字段,不代表缓存应该成为正式数据源。


五十一、Hash 更适合动态状态,而不一定适合正式对象缓存

例如设备状态:

复制代码
ark:device:status:2001
├── online      → 1
├── battery     → 82
├── mode        → working
├── temperature → 31
└── updateTime  → 1720000000

设备可能频繁上报某个字段:

复制代码
HSET ark:device:status:2001 battery 81

或者:

复制代码
HSET ark:device:status:2001 online 0

这种场景 Hash 很自然,因为:

复制代码
字段独立变化
经常只读取部分状态
数据本身就是扁平属性集合

对你未来的机器人、IoT 项目来说,Hash 可以用于:

复制代码
设备在线状态
设备实时参数
任务执行状态
机器人运行统计
告警计数
简化的设备影子

不过高频实时数据是否都应写 Redis,还需要结合数据量、写入频率和业务需求设计。


五十二、本篇动手实践

进入 redis-cli,依次执行。

1. 创建用户 Hash

复制代码
HSET ark:user:info:1001 \
    id 1001 \
    name 张三 \
    age 30 \
    phone 13800000000

2. 读取单个字段

复制代码
HGET ark:user:info:1001 name
HGET ark:user:info:1001 age

3. 一次读取多个字段

复制代码
HMGET ark:user:info:1001 name age phone

4. 读取完整 Hash

复制代码
HGETALL ark:user:info:1001

5. 判断字段是否存在

复制代码
HEXISTS ark:user:info:1001 phone
HEXISTS ark:user:info:1001 address

6. 查看字段数量

复制代码
HLEN ark:user:info:1001

7. 修改年龄

复制代码
HSET ark:user:info:1001 age 31
HGET ark:user:info:1001 age

8. 删除手机号

复制代码
HDEL ark:user:info:1001 phone
HGETALL ark:user:info:1001

9. 设置整个 Hash 的 TTL

复制代码
EXPIRE ark:user:info:1001 300
TTL ark:user:info:1001

10. 测试 HSET 后 TTL 是否保留

复制代码
HSET ark:user:info:1001 name 李四
TTL ark:user:info:1001

观察 TTL 是否继续存在。

11. 创建统计 Hash

复制代码
HSET ark:user:stats:1001 \
    loginCount 0 \
    score 100 \
    failCount 0

12. 原子自增

复制代码
HINCRBY ark:user:stats:1001 loginCount 1
HINCRBY ark:user:stats:1001 score 10
HINCRBY ark:user:stats:1001 failCount 1
HGETALL ark:user:stats:1001

13. Redis 7.4+ 测试 Field TTL

先确认 Redis 版本支持后执行:

复制代码
HSET ark:test:field-ttl \
    normalField normal \
    tempField temporary

tempField 设置 60 秒过期:

复制代码
HEXPIRE ark:test:field-ttl 60 \
    FIELDS 1 tempField

查看:

复制代码
HTTL ark:test:field-ttl \
    FIELDS 1 tempField

如果命令不支持,说明当前 Redis 版本较低,继续使用整个 Key 的 TTL 即可。


五十三、本篇总结

Redis Hash 是一种由多个 Field-Value 组成的数据类型。

整体结构是:

复制代码
Redis Key
├── Field → Value
├── Field → Value
└── Field → Value

例如:

复制代码
ark:user:info:1001
├── id    → 1001
├── name  → 张三
├── age   → 30
└── phone → 13800000000

常用命令包括:

复制代码
HSET
HGET
HMGET
HGETALL
HEXISTS
HDEL
HLEN
HKEYS
HVALS
HINCRBY

String JSON 和 Hash 各有适用场景:

复制代码
完整对象读取
复杂嵌套结构
统一序列化
→ 更适合 String JSON

单字段读取
单字段修改
一组相关计数器
扁平动态状态
→ 更适合 Hash

普通对象缓存通常仍然可以优先使用:

复制代码
String + JSON

而用户统计、设备状态等字段独立变化的数据,可以使用:

复制代码
Hash

传统 Redis 主要给整个 Hash Key 设置 TTL;Redis 7.4 开始支持 Hash Field 级别的过期能力,但实际使用前要确认 Redis Server 和客户端版本。

最终可以用一句话概括:

Redis Hash 可以理解成 Redis 中的一张小型 Field-Value 属性表,它适合保存结构扁平、字段需要独立读取或修改的对象状态;而完整、复杂的 Java 对象缓存,通常使用 String JSON 会更加直接。


下一篇预告

下一篇继续学习 Redis 的集合类数据结构:

《Redis List、Set、ZSet 分别解决什么问题?》

下一篇会重点讲清楚:

复制代码
List 为什么可以表示队列
LPUSH、RPUSH、LPOP、RPOP 如何使用
Set 为什么能够自动去重
SADD、SREM、SISMEMBER 如何使用
Set 如何实现点赞用户和共同关注
ZSet 中的 Score 是什么
ZADD、ZRANGE、ZRANK 如何使用
ZSet 如何实现排行榜
List、Set、ZSet 和 Java 集合有什么区别
简单消息队列为什么不一定要使用 Redis List
真实项目中应该怎样选择数据结构

学完下一篇,Redis 最常用的五种基础数据类型就会形成完整认知。

相关推荐
SEO_juper3 小时前
2026年Schema自动注入实战:用Python批量给1000个页面加上JSON-LD,AI引用率实测提升38%
人工智能·python·json·seo·独立站
空谷有来人4 小时前
docker安装redis
redis·docker·容器
不谈恋爱的猫5 小时前
redis哨兵模式
数据库·redis·mybatis
Wang's Blog5 小时前
AI Agent白手起家37: LangChain 输出解析器实战:文本、JSON、XML 与 Pydantic
xml·langchain·json
clamlss5 小时前
Redis Key 和 Value 设计原则
redis
灵析表格6 小时前
json_ObjectToKV 函数深度研究报告
json·excel·wps·灵析表格·excel公式盒子
wtsolutions7 小时前
JSON 怎么导入 WPS?怎么转换成 WPS 表格?WPS 怎么打开 JSON?完整教程
json·wps
月落归舟7 小时前
Redis 三种消息队列实现方案
数据库·redis·list
智购科技智能售货柜7 小时前
自动售货机商品识别YOLO模型训练实战:从6万张图片到98%识别率的完整复盘~YH
运维·服务器·数据库·人工智能·redis·物联网·yolo