Redis Hash实战:高效存储对象与购物车

引言

上一篇我们讲了 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 的底层也会根据数据量自动切换两种编码:

  1. listpack(旧版叫 ziplist):当字段少且值短时使用,用紧凑的连续内存存储,省内存。
  2. 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",要点总结:

  1. 底层 小数据用 listpack 紧凑存储、大数据自动转 hashtable,切换由 hash-max-listpack-entries / hash-max-listpack-value 控制。
  2. 核心命令围绕 field 展开:HSET/HGET 单字段、HMSET/HMGET/HGETALL 批量、HDEL/HEXISTS 增删查、HINCRBY 计数。
  3. 最大优势是"局部更新"------存对象时想改哪个字段就改哪个,不用像 String 那样整个序列化。
  4. 典型场景:存储对象、购物车、对象计数器,凡是"结构固定、字段独立变化"的数据都适合 Hash。

一句话总结:要存"有多个字段、且字段会各自独立变化"的数据时,优先用 Hash 而不是把整个对象序列化成 String。

相关推荐
文丑颜不良啊1 小时前
LeetCode1141~1160题目解析
java·leetcode
A小码哥1 小时前
【外】水星3号实测 1068 token 每秒靠谱吗
数据库
木井巳1 小时前
【动态规划: 路径问题】下降路径最小和
java·算法·leetcode·动态规划·推荐算法
秋饼1 小时前
MySQL 分库分表完全指南:ShardingSphere-JDBC 实战、分片键选型、场景边界、20 个生产坑与 30 道面试点
java·mysql·面试·shardingsphere·分库分表·后端架构
qq_452396231 小时前
第六篇:《SQL 优化的艺术:从慢查询到毫秒级响应》
数据库·sql
郭小李1 小时前
spring 的 @CookieValue 注解 用法
java·后端·spring
ShineWinsu1 小时前
对于Qt:QWidget通用属性的解析—上
数据库·qt·microsoft·面试·gui·属性·qwidget
每天一道题1 小时前
分布式锁真的安全吗?从 Redis 锁、Lease 到 Fencing Token
redis·分布式·安全
程序员塔塔喵2 小时前
【MySQL篇|第十篇】深分页问题
数据库·oracle·深分页问题