一句话总结
Redis 每种对外数据类型底层都有多种实现,根据数据量和元素大小自动切换:String → SDS(简单动态字符串);List → quicklist(3.2+);Hash → ziplist(小)或 dict(大);Set → intset(纯整数小)或 dict;ZSet → ziplist(小)或 skiplist + dict(大)。核心设计理念:空间换时间、小数据用紧凑结构、大数据用高效结构。
初级理解
五种基本类型与底层编码
| 类型 | 底层编码(小数据) | 底层编码(大数据) |
| String | int / embstr | raw(SDS) |
| List | quicklist(3.2+) | quicklist |
| Hash | ziplist(压缩列表) | hashtable(dict) |
| Set | intset(整数集合) | hashtable(dict) |
| ZSet | ziplist(压缩列表) | skiplist + dict |
# 查看 key 的底层编码
OBJECT ENCODING mykey
# 示例
SET num 123
OBJECT ENCODING num # "int"(整数编码)
SET str "hello"
OBJECT ENCODING str # "embstr"(短字符串编码)
SET longstr "a very long string..."
OBJECT ENCODING longstr # "raw"(SDS 编码)
一句话总结:Redis 每种类型都有多种底层实现,小数据用紧凑结构省内存,大数据用高效结构提性能。
中级深入
SDS(Simple Dynamic String)— 为什么不用 C 字符串?
| C 字符串问题 | SDS 解决方案 |
| 获取长度 O(n)(遍历到 \0) | len 字段 O(1) 获取 |
| 拼接容易缓冲区溢出 | 自动扩容,不会溢出 |
| 不能存二进制(\0 截断) | len 判断结束,可存二进制 |
| 频繁修改导致内存重分配 | 预分配 + 惰性释放 |
# SDS 结构(Redis 3.2+ 分多种长度类型)
struct sdshdr {
int len; # 已使用长度
int free; # 剩余可用空间
char buf[];# 实际数据
};
# 空间预分配:扩容时多分配一些空间
# 惰性释放:缩短时不立即释放,用 free 记录
# embstr vs raw
# embstr: ≤44 字节,内存连续分配(一次 malloc)
# raw: >44 字节,内存分开分配(两次 malloc)
# embstr 是只读的,修改会转为 raw
ziplist(压缩列表)— 内存紧凑的列表
ziplist 是一块连续内存,由一系列 entry 组成,每个 entry 可以存整数或字符串。特点是内存紧凑,但插入删除需要移动内存(连锁更新风险)。
# ziplist 结构
# [zlbytes][zltail][zllen][entry1][entry2]...[entryN][zlend]
# zlbytes: 总字节数
# zltail: 最后一个 entry 的偏移(方便从尾部操作)
# zllen: entry 数量
# zlend: 结束标记 0xFF
# 使用条件(Hash/ZSet 默认配置)
# Hash: 元素 < 512 个 && 每个 value < 64 字节
# ZSet: 元素 < 128 个 && 每个 member < 64 字节
# 超过阈值自动转为 dict/skiplist
skiplist(跳表)— ZSet 的核心
跳表是多层有序链表,通过随机层高实现 O(log n) 的查找效率。相比红黑树,跳表实现简单、支持范围查询。
# 跳表结构
# Level 3: 1 ──────────────> 9
# Level 2: 1 ────> 5 ────> 9
# Level 1: 1 → 3 → 5 → 7 → 9
# Level 0: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9
# 查找 7:
# Level 3: 1 → 9(大于7,回退到1,下降)
# Level 2: 1 → 5 → 9(大于7,回退到5,下降)
# Level 1: 5 → 7(找到!)
# ZSet 为什么同时用 skiplist + dict?
# skiplist: 范围查询 ZRANGE(O(log n + m))
# dict: 按 member 查 score(O(1))
# 两者共享元素,不额外占用内存
中级要点:SDS 解决 C 字符串痛点;ziplist 紧凑但插入慢;skiplist 实现简单支持范围查询。
高级拓展
quicklist — Redis 3.2+ List 的底层
quicklist 是 linkedlist + ziplist 的结合体:双向链表,每个节点是一个 ziplist。兼顾了 linkedlist 的插入效率和 ziplist 的内存紧凑。
# quicklist 结构
# quicklist → node1(ziplist) ↔ node2(ziplist) ↔ node3(ziplist)
# 每个 ziplist 默认 8KB(可配置)
# 配置
# list-max-ziplist-size: 每个 quicklist 节点的 ziplist 最大大小
# list-compress-depth: 两端不压缩的节点数(两端常访问,中间压缩)
intset(整数集合)— Set 的小数据编码
intset 是有序整数数组,通过二分查找实现 O(log n) 查询。当 Set 中全是整数且数量较少时使用。
# intset 结构
struct intset {
uint32_t encoding; # 编码(INT16/INT32/INT64)
uint32_t length; # 元素数量
int8_t contents[]; # 有序整数数组
};
# 升级:插入 INT32 到 INT16 的 intset → 整个 intset 升级为 INT32
# 注意:只升级不降级!
实战场景
场景:ZSet 实现实时排行榜
# 用户积分排行榜
ZADD rank 100 user:1 200 user:2 150 user:3
# Top 3
ZREVRANGE rank 0 2 WITHSCORES
# user:2 200, user:3 150, user:1 100
# 查询 user:1 排名
ZREVRANK rank user:1 # 2(第3名,从0开始)
# 底层:skiplist 保证 O(log n) 插入和查询
# 为什么不用 MySQL ORDER BY?
# 1. Redis 内存操作,微秒级
# 2. skiplist 天然有序,不需要每次排序
# 3. 支持实时更新分数 ZINCRBY
面试模拟
面试官:Redis 的 String 底层是什么?
你:SDS(简单动态字符串)。相比 C 字符串:1)O(1) 获取长度(有 len 字段);2)自动扩容防止缓冲区溢出;3)二进制安全(用 len 判断结束而非 \0);4)空间预分配和惰性释放减少内存重分配。短字符串(≤44字节)用 embstr 编码,内存连续;长字符串用 raw 编码。
面试官:ZSet 为什么用跳表而不用红黑树?
你:1)跳表实现更简单,代码量少;2)跳表天然支持范围查询(ZRANGE),红黑树需要中序遍历;3)跳表通过随机层高实现概率平衡,插入效率与红黑树相当。ZSet 同时用 dict(O(1) 按 member 查 score)和 skiplist(O(log n) 范围查询),两者共享元素。