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
常见坑点
- 没有字段级 TTL:你不能让单个字段过期。TTL 作用于整个 Key。如果需要字段级过期,使用独立的 Key。
- 没有原子的多 Key Hash 操作:
HMGET只在一个 Hash 内工作。要从多个 Hash 读取字段,使用MULTI/EXEC或 pipeline。 - 大 Hash 阻塞:对 10 万字段的 Hash 执行
HGETALL会阻塞 Redis。对大 Hash 使用HSCAN:
HSCAN user:1001 0 COUNT 10
- 类型混淆:所有 Hash 值都是字符串。
HGET返回"30"而不是整数30。在应用中解析。 - 过度使用 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 的对象建模的威力。