Redis 数据结构底层实现是什么?

2025年 阅读约 15 分钟 面试指南 · Redis

深入解析Redis数据结构底层实现:SDS动态字符串、ziplist压缩列表、skiplist跳表、intset整数集合、dict字典、quicklist快速列表,附面试模拟问答。

一句话总结

Redis 每种对外数据类型底层都有多种实现,根据数据量和元素大小自动切换:String → SDS(简单动态字符串)List → quicklist(3.2+)Hash → ziplist(小)或 dict(大)Set → intset(纯整数小)或 dictZSet → ziplist(小)或 skiplist + dict(大)。核心设计理念:空间换时间、小数据用紧凑结构、大数据用高效结构

初级理解

五种基本类型与底层编码

类型底层编码(小数据)底层编码(大数据)
Stringint / embstrraw(SDS)
Listquicklist(3.2+)quicklist
Hashziplist(压缩列表)hashtable(dict)
Setintset(整数集合)hashtable(dict)
ZSetziplist(压缩列表)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) 范围查询),两者共享元素。