Redis Hash 对象建模:何时以及如何使用 HSET

Redis Hash 是存储对象的天然选择——用户、商品、会话、配置。但有一些微妙的决策会影响内存使用、查询模式和可维护性。本指南讲解用 Hash 建模数据的正确方式。

问题场景

你需要存储一个用户画像。方案 A:把整个对象序列化为 JSON 字符串。方案 B:用 Hash 存储各个字段。

SET user:1001 '{"name":"Alice","age":30,"email":"alice@example.com","role":"admin"}'

HSET user:1001 name "Alice" age "30" email "alice@example.com" role "admin"

两种都能用。但它们有非常不同的特性。

Hash vs String(JSON):何时用哪个

使用 Hash 当:

  • 你需要读取/更新单个字段而不获取整个对象。
  • 对象是扁平结构(没有嵌套)。
  • 你想要对大量小对象进行内存优化(ziplist 编码)。

使用 String(JSON)当:

  • 对象是深度嵌套的。
  • 你总是读写整个对象。
  • 你需要复杂查询(考虑 RedisJSON 模块)。

基本 HSET 模式

创建对象

HSET user:1001 name "Alice" age "30" email "alice@example.com" role "admin" created_at "2026-01-15"
HGETALL user:1001

HSET 支持在单条命令中设置多个字段(Redis 4.0+)。

读取字段

HGET user:1001 name
HGET user:1001 email

HMGET user:1001 name email role

HMGET 一次往返获取多个字段——比多次 HGET 调用好得多。

部分更新

这是 Hash 的亮点。更新一个字段而不影响其他字段:

HSET user:1001 role "superadmin"
HSET user:1001 last_login "2026-02-15"
HGETALL user:1001

用 JSON 字符串的话,你需要 GET、反序列化、修改、序列化、SET——四个操作而不是一个。

Hash 中的原子计数器

HSET product:2001 name "Widget" price "9.99" views "0" stock "100"
HINCRBY product:2001 views 1
HINCRBY product:2001 views 1
HINCRBY product:2001 stock -1
HGETALL product:2001

HINCRBY 原子性地递增数值字段。非常适合计数器、库存和浏览量。

浮点数递增

HSET account:1001 balance "100.00"
HINCRBYFLOAT account:1001 balance 25.50
HINCRBYFLOAT account:1001 balance -10.00
HGET account:1001 balance

检查字段是否存在

HEXISTS user:1001 email
HEXISTS user:1001 phone

用于条件逻辑——例如只在 email 字段存在时才发送验证邮件。

删除字段

HDEL user:1001 created_at
HGETALL user:1001
HLEN user:1001

HDEL 删除特定字段。HLEN 返回剩余字段数。

内存优化:Ziplist 编码

Redis 对小 Hash 使用紧凑的"ziplist"编码。这比大 Hash 使用的 hashtable 编码节省大量内存。

默认阈值:

  • hash-max-ziplist-entries:128(最大字段数)
  • hash-max-ziplist-value:64(每个值的最大字节数)

如果你的 Hash 在这些限制内,Redis 会用 ziplist 存储——大约节省 10 倍内存。

HSET small:hash f1 "v1" f2 "v2" f3 "v3"
OBJECT ENCODING small:hash

你应该看到 ziplist(或 Redis 7+ 中的 listpack)。

坑点

如果任何单个字段值超过 64 字节,整个 Hash 会转换为 hashtable 编码。保持值简短。

HSET test:encoding short "hello"
OBJECT ENCODING test:encoding
HSET test:encoding long "this is a very long string that exceeds the sixty four byte limit for ziplist encoding in redis"
OBJECT ENCODING test:encoding

建模模式

用户画像

HSET user:1001 username "alice" display_name "Alice Chen" email "alice@example.com" role "admin" status "active" login_count "0"
HINCRBY user:1001 login_count 1
HMGET user:1001 username display_name role

购物车

HSET cart:user:1001 "product:2001" "2" "product:2002" "1" "product:2003" "3"
HINCRBY cart:user:1001 "product:2001" 1
HDEL cart:user:1001 "product:2003"
HGETALL cart:user:1001

字段 = 商品 ID,值 = 数量。原子递增实现"加入购物车"。

配置存储

HSET config:app max_upload_size "10485760" maintenance_mode "false" api_rate_limit "100" feature_dark_mode "true"
HGET config:app maintenance_mode
HSET config:app maintenance_mode "true"

会话数据

HSET session:abc123 user_id "1001" ip "192.168.1.1" user_agent "Mozilla/5.0" created_at "1739600000"
EXPIRE session:abc123 3600
HGET session:abc123 user_id
TTL session:abc123

处理嵌套数据

Hash 是扁平的——不支持嵌套。对于嵌套对象,有三种方案:

方案一:用点号扁平化

HSET user:1001 "address.street" "123 Main St" "address.city" "Springfield" "address.zip" "62701"
HGET user:1001 "address.city"

方案二:字段中存 JSON

HSET user:1001 name "Alice" address '{"street":"123 Main St","city":"Springfield","zip":"62701"}'
HGET user:1001 address

方案三:独立 Hash

HSET user:1001 name "Alice" address_key "user:1001:address"
HSET user:1001:address street "123 Main St" city "Springfield" zip "62701"
HGETALL user:1001:address

常见坑点

  1. 没有字段级 TTL:你不能让单个字段过期。TTL 作用于整个 Key。如果需要字段级过期,使用独立的 Key。
  2. 没有原子的多 Key Hash 操作HMGET 只在一个 Hash 内工作。要从多个 Hash 读取字段,使用 MULTI/EXEC 或 pipeline。
  3. 大 Hash 阻塞:对 10 万字段的 Hash 执行 HGETALL 会阻塞 Redis。对大 Hash 使用 HSCAN
HSCAN user:1001 0 COUNT 10
  1. 类型混淆:所有 Hash 值都是字符串。HGET 返回 "30" 而不是整数 30。在应用中解析。
  2. 过度使用 Hash:不要把单个 Hash 当作有百万字段的"数据库表"。使用独立的 Key 配合命名规范。

在线编辑器试一试

前往 Redis 在线编辑器 建模一些对象:

HSET user:1001 name "Alice" age "30" email "alice@example.com" role "admin"
HGETALL user:1001
HMGET user:1001 name role
HSET user:1001 role "superadmin" last_login "2026-02-15"
HINCRBY user:1001 age 1
HGETALL user:1001

HSET cart:user:1001 "product:2001" "2" "product:2002" "1"
HINCRBY cart:user:1001 "product:2001" 1
HGETALL cart:user:1001

注意部分更新和原子递增是怎么无缝工作的。这就是基于 Hash 的对象建模的威力。