导航菜单

String vs Hash 存储对象

🟡 中等

题目描述

在 Redis 中存储对象,应该使用 String 还是 Hash?它们的区别是什么?

示例场景

# 用户对象
{
  "id": 1,
  "name": "Alice",
  "age": 25,
  "email": "alice@example.com"
}

# 方式 1:String 序列化存储
SET user:1 '{"id":1,"name":"Alice","age":25,"email":"alice@example.com"}'
GET user:1

# 方式 2:Hash 存储字段
HSET user:1 id 1 name "Alice" age 25 email "alice@example.com"
HGET user:1 name
HGETALL user:1

提示

  • 考虑内存占用
  • 考虑读写性能
  • 考虑灵活性
  • 考虑编码方式

解法

参考答案 (3 个标签)
String Hash 对象存储

String 存储对象

# 序列化存储
SET user:1 '{"id":1,"name":"Alice","age":25}'

# 获取
GET user:1
# {"id":1,"name":"Alice","age":25}

# 修改单个字段(需要先获取再设置)
GET user:1
# 解析 JSON
# 修改字段
# 序列化 JSON
SET user:1 '{"id":1,"name":"Bob","age":25}'

特点

  • 整体存储,一次性读写
  • 修改需要序列化/反序列化
  • 内部使用 SDS(Simple Dynamic String)

Hash 存储对象

# 设置字段
HSET user:1 id 1 name "Alice" age 25

# 获取单个字段
HGET user:1 name
# "Alice"

# 获取所有字段
HGETALL user:1
# 1) "id"
# 2) "1"
# 3) "name"
# 4) "Alice"
# 5) "age"
# 6) "25"

# 修改单个字段
HSET user:1 age 26

# 删除字段
HDEL user:1 email

特点

  • 按字段存储,灵活读写
  • 可以单独修改某个字段
  • 内部使用压缩表或哈希表

对比分析

特性StringHash
内存占用较大较小(压缩表)
读取性能O(1)O(1)
修改性能低(需要序列化)高(直接修改字段)
灵活性低(整体替换)高(字段级别操作)
过期控制整个 key 过期整个 key 过期
网络开销小(一次命令)大(多次命令)

底层实现

String 编码

// SDS (Simple Dynamic String)
struct sdshdr {
    int len;        // 已使用长度
    int free;       // 剩余空间
    char buf[];     // 字节数组
}

// 编码方式
// int: 整数且值 < 1e10
// embstr: 字符串 ≤ 39 字节(一次分配)
// raw: 字符串 > 39 字节(两次分配)

Hash 编码

// 压缩表(ziplist)
// 条件:
// 1. 所有键值对的键和值的字符串长度都 < 64 字节
// 2. 键值对数量 < 512 个

// 哈希表(hashtable)
// 条件:
// 1. 任一键或值的字符串长度 >= 64 字节
// 2. 键值对数量 >= 512 个

struct dictht {
    dictEntry **table;  // 哈希表数组
    unsigned long size; // 哈希表大小
    unsigned long used; // 已有节点数量
}

使用建议

使用 String 的场景

# 1. 对象字段固定,整体读写
SET session:abc123 '{"user_id":1,"login_time":"2024-01-01"}'

# 2. 需要原子性修改
INCR user:1:login_count

# 3. 二进制数据
SET avatar:1 <binary_data>

使用 Hash 的场景

# 1. 对象字段多,需要单独修改
HSET user:1 name "Alice" age 25 email "alice@example.com"
HSET user:1 age 26  # 只修改 age

# 2. 购物车(商品ID + 数量)
HSET cart:1 item:1 5 item:2 3 item:3 2
HINCRBY cart:1 item:1 1  # 增加 item:1 的数量

# 3. 用户属性
HSET user:1 name "Alice" age 25 city "Beijing"
HGET user:1 city

扩展:内存优化

Hash 的内存优化

# 配置 ziplist 参数
hash-max-ziplist-entries 512   # 键值对数量
hash-max-ziplist-value 64      # 字节数

# 查看 encoding
OBJECT encoding user:1
# "ziplist" 或 "hashtable"

# 强制使用 ziplist
# 确保字段少且值小
HSET small key1 "value1" key2 "value2"
OBJECT encoding small
# "ziplist"

String 的内存优化

# 使用整数编码
SET counter 1000
OBJECT encoding counter
# "int"

# 使用 embstr 编码
SET short "hello"
OBJECT encoding short
# "embstr"

# 避免 raw 编码
SET long "very very long string..."
OBJECT encoding long
# "raw"

性能测试

String 读写

# 写入 10000 次
time redis-benchmark -t set -n 10000 -q
# SET: 80000 requests/sec

# 读取 10000 次
time redis-benchmark -t get -n 10000 -q
# GET: 90000 requests/sec

Hash 读写

# HSET 10000 次
time redis-benchmark -t hset -n 10000 -q
# HSET: 70000 requests/sec

# HGET 10000 次
time redis-benchmark -t hget -n 10000 -q
# HGET: 75000 requests/sec

# HGETALL 10000 次
time redis-benchmark -t hgetall -n 10000 -q
# HGETALL: 50000 requests/sec

搜索