引言
上一篇我们讲了 List,这篇来到第三种类型------Hash(哈希) 。如果说 String 是"一个 Key 存一个值",那 Hash 就是"一个 Key 存一组 field-value 键值对",它天然适合用来存储对象。
比如要存一个用户信息,用 String 得把整个对象序列化成 JSON 塞进去;用 Hash 则可以把"姓名、年龄、城市"拆成一个个 field 分别存,想改哪个就改哪个,不必整个重新序列化。这篇文章我会把 Hash 的底层结构、命令、内部编码讲清楚,最后用"存储对象"和"购物车"两个实战场景收尾。
一、Hash 是什么
Redis 的 Hash 是一个 field-value 映射表 ,可以把它理解成 Java 里的 Map<String, Map<String, String>>------外层 Key 定位到哪个哈希,内层 field 定位到具体字段。
plaintext
Key field value
user:1 -> name -> zhangsan
age -> 18
city -> beijing
它特别适合存储"结构固定、字段独立变化"的对象数据,尤其是需要局部更新某个字段的场景。
二、底层结构:listpack 与 hashtable
和 List 类似,Hash 的底层也会根据数据量自动切换两种编码:
- listpack(旧版叫 ziplist):当字段少且值短时使用,用紧凑的连续内存存储,省内存。
- hashtable:当字段多或值长时,自动转成真正的哈希表,查找更快。
两个配置项(默认值)控制切换阈值:
plaintext
hash-max-listpack-entries 512 # field 数量超过 512 转 hashtable
hash-max-listpack-value 64 # 单个 value 超过 64 字节转 hashtable
记住结论:**小 Hash 用 listpack 省内存,大 Hash 自动转 hashtable 提性能,无需手动干预。**
三、常用命令详解
3.1 设置与获取
plaintext
127.0.0.1:6379> HSET user:1 name zhangsan
(integer) 1
127.0.0.1:6379> HSET user:1 age 18 city beijing # 新版本 HSET 支持一次设多个 field
(integer) 2
127.0.0.1:6379> HGET user:1 name
"zhangsan"
127.0.0.1:6379> HMGET user:1 name age # 批量获取
1) "zhangsan"
2) "18"
127.0.0.1:6379> HGETALL user:1 # 获取全部 field-value
1) "name"
2) "zhangsan"
3) "age"
4) "18"
5) "city"
6) "beijing"
3.2 删除与判断
plaintext
127.0.0.1:6379> HDEL user:1 city # 删除字段
(integer) 1
127.0.0.1:6379> HEXISTS user:1 name # 判断字段是否存在
(integer) 1
127.0.0.1:6379> HLEN user:1 # 字段数量
(integer) 2
127.0.0.1:6379> HSETNX user:1 vip 1 # 字段不存在才设置
(integer) 1
3.3 获取所有 field / value
plaintext
127.0.0.1:6379> HKEYS user:1 # 所有 field
1) "name"
2) "age"
127.0.0.1:6379> HVALS user:1 # 所有 value
1) "zhangsan"
2) "18"
3.4 数字运算
plaintext
127.0.0.1:6379> HINCRBY user:1 age 1 # 字段整数自增
(integer) 19
127.0.0.1:6379> HINCRBYFLOAT goods:1 price 0.5 # 浮点自增
"100.5"
HINCRBY 是原子操作,可以做"某个对象的某个计数"。
四、内部编码速查
用 OBJECT ENCODING 查看:
plaintext
127.0.0.1:6379> OBJECT ENCODING user:1
"listpack"
| 编码 | 触发条件 | 特点 |
|---|---|---|
| listpack | 字段少且值短(默认 ≤512 字段、≤64 字节) | 紧凑、省内存 |
| hashtable | 字段多或值长 | 查找快 |
五、典型应用场景
5.1 存储对象(相比 String JSON 的优势)
存用户信息,两种方式对比:
方式一:String 存 JSON
java
redis.opsForValue().set("user:1", "{\"name\":\"zhangsan\",\"age\":18,\"city\":\"beijing\"}");
方式二:Hash 存字段
java
redis.opsForHash().put("user:1", "name", "zhangsan");
redis.opsForHash().put("user:1", "age", "18");
redis.opsForHash().put("user:1", "city", "beijing");
Hash 的优势:
- 局部更新 :只改年龄,String 要整个取出来反序列化、改、再序列化写回;Hash 直接
HSET user:1 age 19一行搞定。 - 按字段读写:只关心某个字段时不用把整个对象都拉出来,网络和内存开销更小。
- 小对象省内存:listpack 紧凑编码比 JSON 字符串更省空间。
所以"需要频繁局部更新的对象",优先用 Hash。
5.2 购物车
经典场景:cart:用户ID 为 Key,商品ID 为 field,数量 为 value。
java
// 加入购物车(数量 +1)
redis.opsForHash().increment("cart:1001", "goods:2001", 1);
// 加入购物车(数量 +2)
redis.opsForHash().increment("cart:1001", "goods:2002", 2);
// 查看购物车全部商品和数量
Map<Object, Object> cart = redis.opsForHash().entries("cart:1001");
// 删除某商品
redis.opsForHash().delete("cart:1001", "goods:2001");
// 修改数量
redis.opsForHash().put("cart:1001", "goods:2002", "5");
购物车天然适合 Hash:一个用户一个 Key,商品是 field,数量是 value,增删改查都直观。
5.3 对象计数器
java
// 文章 1001 的点赞数、收藏数分别计数
redis.opsForHash().increment("article:1001:count", "like", 1);
redis.opsForHash().increment("article:1001:count", "favorite", 1);
比用多个 String Key 更聚合、更好管理。
5.4 缓存"部分字段"频繁变更的对象
比如用户在线状态、积分等高频变更字段,用 Hash 单独存取,避免整对象反复序列化。
六、Spring 中操作 Hash(opsForHash)
java
@Autowired
private StringRedisTemplate redis;
public void hashDemo() {
// HSET 单个字段
redis.opsForHash().put("user:1", "name", "zhangsan");
// HSET 批量(传 Map)
Map<String, String> map = new HashMap<>();
map.put("age", "18");
map.put("city", "beijing");
redis.opsForHash().putAll("user:1", map);
// HGET
Object name = redis.opsForHash().get("user:1", "name");
// HGETALL
Map<Object, Object> all = redis.opsForHash().entries("user:1");
// HEXISTS
Boolean exists = redis.opsForHash().hasKey("user:1", "name");
// HDEL
Long deleted = redis.opsForHash().delete("user:1", "city");
// HINCRBY
Long age = redis.opsForHash().increment("user:1", "age", 1);
}
七、命令速查表
| 命令 | 作用 |
|---|---|
| HSET / HGET | 设置 / 获取单个 field |
| HMSET / HMGET | 批量设置 / 获取 |
| HGETALL | 获取全部 field-value |
| HDEL | 删除字段 |
| HLEN | 字段数量 |
| HEXISTS | 判断字段是否存在 |
| HSETNX | 字段不存在才设置 |
| HKEYS / HVALS | 所有 field / 所有 value |
| HINCRBY / HINCRBYFLOAT | 字段数值自增 |
八、总结
Redis 的 Hash 是"一个 Key 存一组 field-value",要点总结:
- 底层 小数据用 listpack 紧凑存储、大数据自动转 hashtable,切换由
hash-max-listpack-entries/hash-max-listpack-value控制。 - 核心命令围绕 field 展开:HSET/HGET 单字段、HMSET/HMGET/HGETALL 批量、HDEL/HEXISTS 增删查、HINCRBY 计数。
- 最大优势是"局部更新"------存对象时想改哪个字段就改哪个,不用像 String 那样整个序列化。
- 典型场景:存储对象、购物车、对象计数器,凡是"结构固定、字段独立变化"的数据都适合 Hash。
一句话总结:要存"有多个字段、且字段会各自独立变化"的数据时,优先用 Hash 而不是把整个对象序列化成 String。