一、五大基础数据类型的底层实现原理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 HashtableRedis 在 ZSet 中同时使用了这两种结构封装在zset结构体中Hashtable: 存储Member - Score的映射。保证了ZSCORE命令原本 $O(1)$ 的时间复杂度。Skiplist: 存储有序的节点。跳表是一种典型的 空间换时间 结构通过维护多层索引实现 $O(\log N)$ 的范围查询如ZRANGE。为什么不用红黑树跳表实现更简单且在并发环境下虽然 Redis 是单线程但设计思路上区间查找效率通常优于平衡树。底层映射数据类型数据量少/元素小 (Encoding)数据量大/元素大 (Encoding)Stringint/embstrrawListziplist(旧) /quicklist(新)quicklistHashziplist/listpackhashtableSetintsethashtableZSetziplist/listpackskiplisthashtable二、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 依然保留用于每天的归档备份。场景 DRedis 4.0 的“混合持久化”背景想要 RDB 的快速启动又想要 AOF 的数据安全。原理在 AOF 重写时将当前的内存数据做成 RDB 快照放在 AOF 文件的开头后续的增量操作依然记录为 AOF 日志。文件结构[RDB 快照内容] [AOF 增量日志]优势重启极快前半段直接加载 RDB。数据丢失极少后半段是秒级日志。