This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/Redis/02-核心数据类型.md
T
2026-05-17 00:06:11 +08:00

10 KiB
Raw Blame History

tags, create time
tags create time
Redis
缓存
数据结构
2026-05-15 18:11

Redis 核心数据类型与底层编码

概述

[!QUESTION] 同一个 SET 命令,有时占内存几字节,有时几百字节——是 Bug 吗? 不是。Redis 会根据值的特征自动选择最省内存的内部表示。这就是你要理解的"编码"概念。

Redis 有五种基础数据类型:String、Hash、List、Set、ZSet。但每种类型在不同场景下会切换不同的内部编码——理解这一点比单纯记忆命令更重要,因为它直接决定了操作的时间复杂度和内存消耗。


flowchart TD
    subgraph "String"
        S1["int — 整数"]
        S2["embstr (≤44B) — 短字符串"]
        S3["raw — 长字符串"]
    end

    subgraph "Hash"
        H1["ziplist — 元素少且小"]
        H2["hashtable — 超出阈值后切换"]
    end

    subgraph "List"
        L1["quicklist (ziplist 节点 + 指针)"]
    end

    subgraph "Set"
        ST1["intset — 全是整数"]
        ST2["hashtable — 出现非整数"]
    end

    subgraph "ZSet"
        Z1["ziplist — 元素少且差距小"]
        Z2["skiplist + hashtable — 大规模数据"]
    end

一、String(字符串)

三种内部编码

编码 触发条件 典型场景
int 值可表示为长整型 计数器 (INCR)
embstr ≤ 44 字节(Redis 7 之前是 39B) 短文本存储
raw > 44 字节 大对象 JSON / Base64

[!QUESTION] embstr 和 raw 的区别?

  • embstr:一块连续内存分配,创建/销毁只需一次 malloc/free,效率高
  • raw:分开分配 SDS 结构体和 redisObject 头,支持扩容
  • 超过 44B 时 embstr 不可变,只能切 raw

代码示例(Go go-redis)

// 设置过期时间(原子操作)
rdb.Set(ctx, "token:abc", jwtPayload, 24*time.Hour)

// 原子递增计数器
rdb.Incr(ctx, "counter:api")

// MGET 批量获取
vals, _ := rdb.MGet(ctx, "user:1", "user:2", "user:3").Result()

[!INSIGHT] 编码切换是透明的 你不需要自己管理编码——REDISOBJECT.encoding 字段由 Redis 自动维护。用 MEMORY USAGE key 可以看到实际占用字节数,这是验证编码效果最直接的方式。

二、Hash(哈希表)

内部编码切换

flowchart LR
    A["ziplist"] -->|"超出阈值: 节点数>5 或值≥64B"| B["hashtable"]
    
    style A fill:#e1f5fe
    style B fill:#fff3e0

阈值由两个配置控制:

  • hash-max-ziplist-size 5:节点数 > 5 或某个字段长度 ≥ 64B → 切 hashtable
  • hash-max-ziplist-value 64:单个值 > 64B → 切 hashtable

适用场景

场景 命令 说明
用户信息缓存 HSET user:1 name alice age 25 只更新需要改的字段
表单数据存储 HMSET / HGETALL 替代 String 存整个 JSON

String vs Hash 选型

[!QUESTION] 为什么更新一个字段要读写整个 JSON? String 存的是完整序列化字符串,改任何内容都要重新序列化写入。Hash 是结构化的键值对,只修改目标字段即可。

quadrantChart
    title "String vs Hash — 场景选择矩阵"
    x-axis "简单操作" --> "复杂操作"
    y-axis "内存紧凑" --> "灵活存取"
    "String (JSON)": [0.3, 0.75]
    "Hash (字段级)": [0.7, 0.85]
维度 方案 A — String(JSON) 方案 B — Hash(字段级)
读取 GET user:1 ✅ 一行搞定 HGETALL user:1 需合并
局部更新 ❌ 读 → 改 → 写整个 JSON ✅ HSET user:1 email b@x.com
内存占用 较高(重复 key、序列化开销) 较低(ziplist 紧凑存储)
适用规模 小数据、读多写少 中等大小、频繁部分更新

[!TIP] Hash 在 ziplist 编码下内存占用比单独 String 省 ~40%,适合字段数 < 100 的对象缓存。

方案 A — String(JSON 整体存取)

GET user:1 → {"name":"alice","age":25,"email":"a@x.com"}
✅ 读写简单  ✅ 序列化一致
❌ 更新一个字段要读写整个 JSON

方案 B — Hash(字段级操作)

HSET user:1 email b@x.com
✅ 局部更新,内存更紧凑
❌ HGETALL 不适合超大 hash

[!CAUTION] 警惕大 Hash HGETALL 在 hashtable 编码下是 O(N),hash 很大时会阻塞 Redis。如果字段数超过几千,考虑拆成多个 key 或改回 String(JSON)。

三、List(列表)

quicklist 结构

[!QUESTION] 为什么不用双向链表 + SDS,而要用 quicklist? 普通双向链表每个节点都有指针开销(16B)。quicklist 把多个元素打包进一个 ziplist 紧凑块中,大幅减少指针浪费。

flowchart LR
    H["head"] <--> Z1["ziplist 节点 #1\n(≤ 4096 项)"]
    Z1 <--> Z2["ziplist 节点 #2\n(≤ 4096 项)"]
    Z2 <--> Z3["ziplist 节点 #N\n(≤ 4096 项)"]
    Z3 <--> T["tail"]
    
    classDef linkPoint fill:#e8f5e9,stroke:#4caf50
    classDef znode fill:#fff3e0,stroke:#ff9800
    class H,T linkPoint
    class Z1,Z2,Z3 znode

List 统一使用 quicklist(压缩链表),每个节点是一个 ziplist:

  • list-max-ziplist-size 5:默认每个节点最多 4096 个元素
  • list-compress-depth 0:默认不压缩首尾节点(方便头部操作)

典型场景

场景 命令 说明
消息队列 LPUSH + BRPOP 单消费者阻塞式消费
最新 N 条记录 LRANGE 0 -1 配合 LTRIM 固定长度
延迟队列 ZSet score = timestamp 比 List 灵活
// 发布新消息
rdb.LPush(ctx, "news-feed", jsonMessage)
// 修剪到最近 1000 条
rdb.LTrim(ctx, "news-feed", 0, 999)
// 取出最近 10 条
items, _ := rdb.LRange(ctx, "news-feed", 0, 9).Result()

四、Set(集合)

intset → hashtable 切换

flowchart LR
    E["Set 添加元素"] --> Q{"全部是\n整数?"}
    Q -->|"是"| I["intset\n有序数组 · 内存紧凑"]
    Q -->|"否 — 出现字符串"| H["hashtable\nO(1) 查找 · 集合运算"]
    
    classDef encoding fill:#e1f5fe,stroke:#2196f3
    classDef encoding2 fill:#fff3e0,stroke:#ff9800
    class I encoding
    class H encoding2

集合操作(唯一能力)

SADD tags:post1 go golang redis
SADD tags:post2 redis docker golang

SDIFF tags:post1 tags:post2   # go  — post1 独有
SINTER tags:post1 tags:post2  # golang redis — 共有的
SUNION tags:post1 tags:post2  # go golang redis docker
SCARD tags:post1              # 3 — 元素个数
SISMEMBER tags:post1 go       # 1 — 判断成员

应用场景

  • 点赞/收藏列表:SADD like:post:42 user:100
  • 共同好友:SINTER user:A:friends user:B:friends
  • 随机抽取:SRANDMEMBER lottery 5(不删除)/ SPOP lottery 5(删除)

[!TIP] Set 的 SSCAN 命令 SSCAN 可以增量迭代大集合,避免单次命令阻塞 Redis——和 Hash 的 HSCAN 同理。

五、ZSet / Sorted Set(有序集合)

skiplist + hashtable 双结构

[!QUESTION] 为什么需要两套数据结构? skiplist 天然有序、支持高效范围查询,但按 member 查找要遍历;hashtable 查找 O(1),但无序。两者互补,代价是每个元素存了两份索引。

flowchart LR
    Key["key: leaderboard"] --> SL["skiplist\n按 score 排序 · 范围查询 O(log N)"]
    Key --> HT["hashtable\n按 member 查找 · O(1) 定位"]
    
    classDef primary fill:#e8f5e9,stroke:#4caf50
    classDef secondary fill:#fff3e0,stroke:#ff9800
    class SL secondary
    class HT secondary

这是 Redis 中最复杂的数据结构,代价也是最高的——每个元素存在两份索引。

关键命令

# 添加(member → score)
ZADD leaderboard 100 "player1" 200 "player2" 150 "player3"

# 正序排名 1~5
ZRANGE leaderboard 0 4 WITHSCORES

# 倒序 Top 10
ZREVRANGE leaderboard 0 9 WITHSCORES

# 获取分数
ZSCORE leaderboard "player2"       # 200

# 获取排名(从 0 开始)
ZRANK leaderboard "player3"        # 1
ZREVRANK leaderboard "player3"     # 2

# 分数范围查询
ZRANGEBYSCORE leaderboard 0 150

# 移除指定范围
ZREMRANGEBYRANK leaderboard 0 2

Go 示例

// 排行榜加分(原子操作)
rdb.ZIncr(ctx, "leaderboard", redis.Z{
    Score:  10.5,
    Member: "player42",
})

// 获取 Top 10
members, _ := rdb.ZRevRangeWithScores(ctx, "leaderboard", 0, 9).Result()
for _, m := range members {
    fmt.Printf("%s: %.1f\n", m.Member, m.Score)
}

ZSet vs List 做消息队列对比

维度 List (BRPOP) ZSet (BZPOPMIN)
FIFO 顺序 ✅ 保证 ⚠️ 依赖 score
延迟执行 ❌ 不支持 ✅ score = 执行时间戳
重排优先级 ❌ 需重建 ✅ 改 score 即可
复杂度 O(log(N/M)) O(log N)

[!WARNING] ZSet 性能陷阱 不要往同一个 ZSet 里塞超过百万级别的元素——skiplist 虽然 O(log N),但实际维护成本不小。百万级可以考虑按分片拆成多个 ZSet。

快速对照表

数据类型 编码 最佳场景 时间复杂度
String int / embstr / raw 计数、缓存、分布式锁 O(1)
Hash ziplist / hashtable 对象缓存(部分更新) O(1)~O(N)
List quicklist FIFO 队列、feed流 O(1)~O(N)
Set intset / hashtable 标签、去重、关系运算 O(1)
ZSet ziplist / skiplist+ht 排行榜、延迟任务、权重 O(log N)

核心思想:内存与性能的权衡

Redis 的数据类型本质上是一套编码策略系统。理解这个系统的关键在于三个原则:

  1. 小数据用紧凑编码 — ziplist / intset / embstr 牺牲灵活性换取极致内存效率
  2. 大数据切高性能结构 — hashtable / skiplist / raw 以空间换时间,操作复杂度可控
  3. 切换是自动且平滑的 — Redis 在后台根据阈值决定何时升级编码,开发者只需关注业务语义

[!SUMMARY] 实战检查清单

  • 缓存对象 → Hash(ziplist)或 String(JSON),字段多且常部分更新选 Hash
  • 计数 / 锁 / Token → String(int 编码最省)
  • Feed 流 / 队列 → List(quicklist)
  • 标签 / 去重 / 集合运算 → Set(intset)
  • 排行榜 / 延时任务 / 带权随机 → ZSet(skiplist)

关联笔记