一、五大基础数据类型的底层实现原理
Redis 之所以快,除了它是基于内存的操作外,更重要的是它为每种数据类型都精心设计了底层的编码。Redis 不是简单的 "Key-Value" 存储,它会根据数据量的多少和类型的不同,动态选择最节省内存或最高效的数据结构。
本文将带你通过源码视角,通过 redisObject 对象出发,解析 String、List、Hash、Set、ZSet 的底层实现。
核心对象:redisObject
在 Redis 中,我们存储的每一个键值对,其 Value 并不是直接存储为原始数据,而是被封装在一个 redisObject 结构体中。
typedef struct redisObject {
unsigned type:4; // 类型:String, List, Hash...
unsigned encoding:4; // 编码:int, raw, embstr, ziplist...
unsigned lru:24; // LRU 时间戳(用于淘汰策略)
int refcount; // 引用计数
void *ptr; // 指向底层数据结构的指针
} robj;
关键点: type 决定了对外的数据类型,而 encoding 决定了内部真正的底层实现。
1. String (字符串)
String 是 Redis 最基本的数据类型。
底层实现
-
int: 如果字符串本身是整数且在 Long 范围内,Redis 会直接存储为整数,避免分配额外的内存。
-
SDS (Simple Dynamic String): 如果存储的是文本字符串,Redis 使用自定义的 SDS 结构,而不是 C 语言的字符串。
SDS 的优势
-
O(1) 获取长度 : SDS 头部维护了
len属性。 -
二进制安全 : 可以存储图片、视频流数据(不以
\0为结束符)。 -
杜绝缓冲区溢出: 修改字符串时会自动检测空间。
编码格式 (Encoding)
-
int: 存储整数。
-
embstr : 存储短字符串(<= 44 字节)。它将
redisObject和SDS连续分配在同一块内存中,减少内存碎片和分配次数。 -
raw : 存储长字符串(> 44 字节)。
redisObject和SDS分开分配。
2. List (列表)
List 用于存储有序的字符串列表。
底层实现演进
-
早期版本 : 数据少时用 Ziplist (压缩列表) ,数据多时用 Linkedlist (双向链表)。
-
现代版本 (3.2+) : 统一使用 Quicklist。
核心结构解析
-
Ziplist: 一块连续的内存空间,没有指针的开销,极其节省内存,但插入删除需要内存重排,不适合大量数据。
-
Quicklist : 是 "Linkedlist + Ziplist" 的结合体。它是一个双向链表,但链表中的每个节点不再是一个单独的元素,而是一个 Ziplist。
- 优势: 既保留了链表的插入效率,又利用 Ziplist 减少了内存碎片。
注 : 在 Redis 7.0 之后,
listpack逐渐取代了ziplist,但在逻辑上依然是 "链表 + 紧凑列表" 的组合。
3. Hash (哈希)
Hash 用于存储对象(键值对集合)。
底层实现
-
Ziplist / Listpack (压缩列表):
-
触发条件: 元素数量少(默认 < 512)且所有元素的值都较短(默认 < 64 字节)。
-
原理: Key 和 Value 作为两个节点紧挨着存入压缩列表。查找时需要遍历,时间复杂度 O(N),但因为数据量小,CPU 消耗可忽略,换取的是极高的内存利用率。
-
-
Hashtable (字典/哈希表):
-
触发条件: 数据量大或元素值过长。
-
原理: 使用 MurmurHash 算法计算哈希值,通过链地址法解决冲突。
-
扩容机制 : 采用 渐进式 Rehash,避免一次性拷贝大量数据导致 Redis 阻塞。
-
4. Set (集合)
Set 用于存储不重复的元素。
底层实现
-
Intset (整数集合):
-
触发条件 : 集合中所有元素都是整数,且元素数量较少(默认 < 512)。
-
原理 : 底层是一个有序的数组。查找时使用二分查找,时间复杂度 O(\\log N)。
-
-
Hashtable (字典):
-
触发条件: 元素包含非整数,或数量过多。
-
原理: 利用字典的 Key 存储元素,Value 统一指向 NULL。
-
5. ZSet (有序集合)
ZSet 是 Redis 最具特色的数据结构,它维护了元素的 Score 从而实现排序。
底层实现
-
Ziplist / Listpack:
-
触发条件: 元素少且短。
-
原理: 元素和 Score 紧挨着存储,按 Score 从小到大排序。
-
-
Skiplist + Hashtable (跳表 + 字典):
-
触发条件: 元素多或长。
-
这是 ZSet 性能强大的核心。
-
为什么是 Skiplist + Hashtable?
Redis 在 ZSet 中同时使用了这两种结构(封装在 zset 结构体中):
-
Hashtable : 存储
Member -> Score的映射。保证了ZSCORE命令原本 O(1) 的时间复杂度。 -
Skiplist : 存储有序的节点。跳表是一种典型的 "空间换时间" 结构,通过维护多层索引,实现 O(\\log N) 的范围查询(如
ZRANGE)。- 为什么不用红黑树? 跳表实现更简单,且在并发环境下(虽然 Redis 是单线程,但设计思路上)区间查找效率通常优于平衡树。
底层映射
| 数据类型 | 数据量少/元素小 (Encoding) | 数据量大/元素大 (Encoding) |
|---|---|---|
| String | int / embstr |
raw |
| List | ziplist (旧) / quicklist (新) |
quicklist |
| Hash | ziplist / listpack |
hashtable |
| Set | intset | hashtable |
| ZSet | ziplist / listpack |
skiplist + hashtable |
二、Redis数据持久化
Redis 是内存数据库,一旦断电或进程重启,内存中的数据就会丢失。为了解决这个问题,Redis 提供了持久化机制,将内存数据写入磁盘。
1. RDB - 内存快照
RDB 是 Redis 默认的持久化方案。它就像给数据库拍了一张"照片"。
核心原理
在指定的时间间隔内(例如"60秒内有1000个改动"),Redis 会执行 **bgsave**命令。
-
Fork 子进程:Redis 父进程 fork 出一个子进程。
-
Copy-on-Write (写时复制):这是 RDB 不阻塞主线程的关键。子进程共享父进程的内存数据,只有当父进程修改某块数据时,OS 才会为该块数据复制一份副本给子进程。
-
写入文件:子进程将内存数据写入临时的 RDB 文件,完成后替换旧文件。
优缺点分析
-
优点:
-
恢复速度极快:RDB 是紧凑的二进制文件,Redis 加载它比加载文本格式的 AOF 快得多。
-
适合冷备:文件小,易于传输,适合用于灾难恢复(Disaster Recovery)。
-
-
缺点:
-
数据丢失风险:因为是定时快照,如果 Redis 意外宕机,你会丢失最后一次快照后的所有修改(可能几分钟的数据)。
-
重量级操作 :
fork操作在内存很大(如几十 GB)时会阻塞主线程毫秒甚至秒级。
-
2.AOF ------ 操作日志
核心机制
-
记录方式: 以文本协议格式记录所有的写命令(Write/Set/Del)。
-
刷盘策略 (
appendfsync) ------ 这是业务调优的重点:-
Always:每条命令都刷盘。数据最安全,但性能极差(不推荐)。 -
Everysec:每秒刷盘一次。默认策略,兼顾性能与安全(最多丢1秒数据)。 -
No:操作系统决定何时刷盘。
-
AOF 重写 (Rewrite)
-
痛点: 随着时间推移,日志文件会变得无限大(比如反复
INCR一亿次,日志有一亿条,但最终状态只是一个数字)。 -
解决: Redis 后台重写 AOF 文件,只保留恢复当前状态所需的最小命令集。
业务优缺点
-
优点(业务侧):
- 数据安全性高: 最多只丢失 1 秒数据。
-
缺点(业务侧):
-
文件体积大: 通常比 RDB 文件大很多。
-
恢复速度慢: 重启时需要一条条重放命令,数据量大时启动非常慢。
-
3.业务场景中的策略选择
场景 A:纯缓存模式
-
业务特征: 数据均可从数据库(MySQL/PostgreSQL)重新加载;缓存丢失不影响业务正确性,只影响短暂的性能。
-
策略: 关闭 RDB 和 AOF。
-
理由: 追求极致性能,节省磁盘 IO。
场景 B:可以容忍少量数据丢失,追求高性能
-
业务特征: 比如论坛的点赞数、页面的访问统计、非关键的日志分析。
-
策略: 仅使用 RDB。
-
配置建议: 比如每 15 分钟存一次。
-
理由: 即使丢了 15 分钟的数据,对业务影响很小,但能换来极快的备份和恢复体验。
场景 C:数据一致性要求高 (传统做法)
-
业务特征: 购物车数据、即时消息队列、无法从数据库恢复的业务数据。
-
策略: 开启 AOF (
everysec),同时也开启 RDB(作为备份)。 -
注意: Redis 重启时默认加载 AOF,因为数据更全。但 RDB 依然保留用于每天的归档备份。
场景 D:Redis 4.0+ 的"混合持久化"
-
背景: 想要 RDB 的快速启动,又想要 AOF 的数据安全。
-
原理: 在 AOF 重写时,将当前的内存数据做成 RDB 快照放在 AOF 文件的开头,后续的增量操作依然记录为 AOF 日志。
-
文件结构:
[RDB 快照内容] + [AOF 增量日志] -
优势:
-
重启极快(前半段直接加载 RDB)。
-
数据丢失极少(后半段是秒级日志)。
-