玩转 Redis · Hash 篇
环境:Docker 容器
redis-learn(Redis 7.4.11)本篇地位:五大类型 2/5。一句话:存"一个东西的多个属性",默认用它
0. 一分钟回顾
痛点:用 String 存一个用户要贴 N 张便签(user:1:name、user:1:age…),碎、乱、整体过期难定。
Hash = 一张便签上画表格,三层结构:
key(表名)→ field(行名)→ value(格子内容)
me:profile → name → fatcoder
age → 22
学名与比喻:官方名 Hash(因底层用"哈希表"技术实现,核心原理篇再讲),"表格"是使用时的心智模型——用时想表格,查文档搜 Hash。长得像 JS 对象 {name:"x", age:22}、Python 字典。
1. 命令全家桶(H 开头 = Hash 专属)
| 操作 | 命令 | 一句话说明 |
|---|---|---|
| 增/改 | HSET key f v [f v …] | 设置行(一条命令能设多行);同名 = 覆盖。返回新增行数 |
| 增 | HSETNX key f v | 这行空才写(SETNX 的表格版) |
| 查 | HGET key f | 取一格(需要两个参数:表名+行名) |
| 查 | HMGET key f1 f2 | 取几格 |
| 查 | HGETALL key | 整表倒出(行名、值交替输出)⚠️ 大表禁用 |
| 查 | HKEYS key | 行名清单 |
| 查 | HVALS key | 值清单 |
| 查 | HLEN key | 有几行 |
| 查 | HEXISTS key f | 行在不在(1/0) |
| 改 | HINCRBY key f n | 格内数字 ±n(新行自动从 0 起) |
| 删 | HDEL key f1 [f2…] | 删行(返回删掉几行) |
| 删 | DEL key | 删整表(通用指令) |
教科书场景——购物车:HSET cart:100 goods:1 2 goods:5 1 → HINCRBY cart:100 goods:1 1 → HGETALL cart:100 结算 → EXPIRE cart:100 2592000(30 天自动清空)。
2. 核心理解点(带练中亲手验证)
- HSET 返回"新增行数":更新已有行返回
0,但值确实改了(返回 0 ≠ 失败,HGET 验证) - 格子存的都是字符串,
HINCRBY等算术家族会自动"字符串↔数字"翻译(翻译→计算→存回,原子打包);翻不动就报not an integer;普通命令(HGET)不翻译只念原文 - 空表自动消失:行删光 → key 本尊自动没了(String 不会这样,性格差异)
- 长度三兄弟:
HLEN=行数;STRLEN=String 值字节数;HSTRLEN key f=某格内容字节数 - HGETALL 大表 = 事故:单线程被长时间占用、全量传输、客户端内存风险——与
KEYS同罪。大 N 禁全量,要几行用 HMGET,全表遍历用 HSCAN(后续讲)
3. 实操流水(真实命令 + 结果)
3.1 建表与批量加行
127.0.0.1:6379> HSET me:profile name "fatcoder"
1 ← 新增 1 行
127.0.0.1:6379> HSET me:profile age "21" city "shanghai" hobby "balls"
3 ← 一条命令新增 3 行
3.2 读单行 / 整表
127.0.0.1:6379> HGET me:profile age
21
127.0.0.1:6379> HGETALL me:profile
name
fatcoder ← 行名、值交替排队输出
age
21
city
shanghai
hobby
balls
3.3 更新已有行:返回 0 但已生效
127.0.0.1:6379> HSET me:profile age 22
0 ← 新增 0 行(只是更新)
127.0.0.1:6379> HGET me:profile age
22 ← 亲眼验证:确实改了!
3.4 表内计数器:从没建过的行自动起步
127.0.0.1:6379> HINCRBY me:profile score 10
10 ← score 行不存在,自动从 0 起 +10
127.0.0.1:6379> HINCRBY me:profile score 10
20
127.0.0.1:6379> HINCRBY me:profile score 10
30
127.0.0.1:6379> HINCRBY me:profile score 10
40
3.5 删行 + 数行
127.0.0.1:6379> HDEL me:profile hobby
1 ← 删掉 1 行
127.0.0.1:6379> HLEN me:profile
4 ← 5 行变 4 行
3.6 空表自动消失(开篇演示 + 推理验证)
HSET user:100 name "小红" age 18 city "成都" → 3
HDEL user:100 city → 1
HLEN user:100 → 2
HDEL user:100 name age → 2
EXISTS user:100 → 0 ← 行删光,key 自动没了
4. 待深入专区(挂起,学完 List 篇后回访)
Hash 格子只能存字符串,不能嵌套:
- 想塞"子表格":
HSET k address HSET demo:sub x y→ 报错(key 后参数必须"行名 值"成对,5 个散装参数凑不成对) - 存 JSON 字符串可行但有代价:
HSET k address '{"city":"shanghai","district":"pudong"}'→ 存取都行,但 Redis 不懂内部结构,改一字段要整串读出→程序改→写回 - 工程三解法:①拍平(city、district 平级两行)②分表(me:profile 主表 + me:address 单独一张,用 key 冒号层级替代内容嵌套)③存 JSON(整体取出、少改内部的场景)
- 后续线索:RedisStack 的 RedisJSON 专门类型(生态篇)
5. 踩坑实录
| 坑 | 现象 | 教训 |
|---|---|---|
| HGET 只给表名 | HGET me:profile → ERR 参数数量 | HGET 要两个参数:表名 + 行名 |
| 误读 HSET 返回 0 | 以为更新失败 | 0 = 新增 0 行 ≠ 失败;拿不准就 HGET 验证 |
| 误解 HLEN | 以为是字符串长度 | HLEN = 行数;量长度的是 STRLEN/HSTRLEN |
| 以为格子里存的是数字 | score +10 能算 | 存的是字符串,算术命令会自动翻译 |
6. 选型:String 贴 N 张 vs Hash 一张表
| 对比项 | String | Hash |
|---|---|---|
| 存对象 N 个属性 | N 条 key | 1 条 key N 行 ✅ |
| 只改一个属性 | SET user:1:age 19 | HSET user:1 age 19(平手) |
| 整体过期 | 挨条 EXPIRE ❌ | 一次 EXPIRE ✅ |
| 知道有几个属性 | 没办法 | HLEN ✅ |
| 属性计数 | INCR | HINCRBY(平手) |
结论:存对象默认 Hash;单值/计数场景用 String。
7. 自检清单
- HSET 更新已有行返回几?值到底改没改?怎么确认?
- HGET 需要几个参数?分别是什么?
- HLEN 量的是什么?
- score 格子里存的是数字还是字符串?HINCRBY 靠什么算的?
- 行删光后 key 还在吗?String 存空值呢?
- 删一行用啥?删整表用啥?
- 10 万行的表能 HGETALL 吗?三条理由?
- 格子里能存子表格吗?工程上三种替代法?
8. 类型地图(2/5)
| 篇章 | 形状 | 典型用途 | 招牌命令 | 状态 |
|---|---|---|---|---|
| 📗 String | 一行字的纸条 | 缓存、计数器 | SET GET INCR | ✅ |
| 📘 Hash | 小表格(字段↔值) | 对象、单字段修改、购物车 | HSET HGET HINCRBY | ✅ |
| 📙 List | 两头进出的队伍 | 消息队列、最新列表 | LPUSH RPOP | ⬅️ 下一站 |
| 📕 Set | 不重复的一筐球 | 去重、标签、共同好友 | SADD SINTER | ⏳ |
| 📓 ZSet | 带分数自动排队 | 排行榜、优先级 | ZADD ZRANGE | ⏳ |