一、核心数据结构与底层实现原理
1.1 SDS(Simple Dynamic String):Redis字符串的底层实现
Redis没有直接使用C语言的字符串(以空字符结尾的字符数组),而是实现了自己的简单动态字符串(Simple Dynamic String, SDS)。SDS的设计解决了C字符串的多个固有缺陷,是Redis高性能和安全性的基石之一。
SDS相比C字符串的三大优势:
- 常数时间获取长度:通过len字段直接返回,O(1)时间复杂度;C字符串需遍历计算,O(n)
- 避免缓冲区溢出:修改前检查剩余空间,不足时自动扩展,防止内存越界
- 二进制安全:不以\0结尾,可存储任意二进制数据(如图片、JSON)
SDS的空间预分配策略
当SDS需要扩展时,Redis采用“空间预分配”避免频繁重分配:
- 若修改后长度 < 1MB,则新分配空间 = 原长度(即free = len)
- 若修改后长度 ≥ 1MB,则新分配空间 = 1MB(即free = 1MB)
这种策略使连续追加操作(如 APPEND)摊还时间复杂度为O(1),大幅提升写入性能。
惰性空间释放(Lazy Space Release)
当SDS缩短内容时,并不立即回收多余内存,而是更新free字段,保留空间供下次使用。这减少了内存分配/释放的开销,尤其适用于频繁增删的场景。
1.2 字典(Dict)与哈希表(Hash Table)
Redis的字典是其所有键值对存储的核心结构,底层基于哈希表实现,采用链地址法解决哈希冲突。其设计兼顾了动态扩容、渐进式rehash与内存效率。
哈希冲突处理:链地址法
每个哈希桶指向一个链表(dictEntry),新节点插入链表头部(头插法),保证插入O(1)。链表节点结构:
渐进式rehash(Incremental Rehash)
为避免一次性rehash导致主线程阻塞,Redis采用渐进式策略:
- 初始化:创建新哈希表ht[1],设置rehashidx=0
- 增量迁移:每次执行增删改查时,额外迁移ht[0][rehashidx]中的链表到ht[1]
- 后台迁移:定时任务(serverCron)每毫秒迁移100个桶
- 完成:ht[0].used=0后,释放ht[0],ht[1]成为新ht[0],rehashidx=-1
哈希负载因子与自动扩容
| 状态 | 负载因子(used/size) | 触发条件 | 扩容策略 |
|---|---|---|---|
| 正常状态 | < 1.0 | 无压力 | 维持原大小 |
| 负载过高 | >= 1.0(且未BGSAVE/BGREWRITEAOF) | 主动触发rehash | size × 2 |
| 负载极高 | >= 5.0 | 强制触发rehash | size × 2(即使有子进程) |
扩容后size为2的幂(如16→32→64…),sizemask = size - 1,便于通过位运算快速计算索引:index = hash & sizemask。
1.3 Redis五大数据结构的底层映射
Redis对外提供5种基础数据类型,但底层仅使用7种数据结构,通过编码优化实现高效存储:
| Redis类型 | 底层数据结构 | 触发条件 | 典型编码 |
|---|---|---|---|
| String | SDS、int、embstr | 整数且长度≤20;短字符串(≤39字节) | int / embstr / raw |
| List | quicklist(双向链表+压缩列表) | 长度≥2或元素≥512字节 | quicklist |
| Set | intset(整数集合)或hashtable | 全为整数且数量≤512 → intset | intset / hashtable |
| Hash | ziplist(压缩列表)或hashtable | 字段数≤512且值≤64字节 → ziplist | ziplist / hashtable |
| ZSet | ziplist或skiplist+dict | 元素数≤128且值≤64字节 → ziplist | ziplist / skiplist |
二、持久化机制:RDB与AOF深度对比
2.1 RDB(Redis Database)快照持久化
RDB是Redis的二进制快照文件,记录某一时刻的全部数据。其核心在于fork子进程完成持久化,避免阻塞主线程。
RDB触发方式
- SAVE:阻塞主线程,同步生成RDB(生产环境禁用)
- BGSAVE:fork子进程异步持久化(默认方式)
- 自动触发:根据save配置(如900秒1次修改)
RDB文件结构
包含:REDIS魔数 → 版本号 → DB内容(键值对)→ EOR(EOF标记)→ CRC64校验和。
优点与缺点
| 优势 | 劣势 |
|---|---|
| 文件紧凑,恢复速度快 | 可能丢失最后一次快照后的数据 |
| 适合冷备份与灾难恢复 | fork子进程时内存翻倍(写时复制COW) |
| 兼容性强,跨版本迁移稳定 | 大实例生成慢(秒级阻塞fork) |
2.2 AOF(Append Only File)日志持久化
AOF以命令日志形式记录写操作,通过重放命令恢复数据,提供更高数据安全性。
AOF写入策略
重写机制(AOF Rewrite)
为避免AOF文件无限增长,Redis支持重写:生成新文件,仅保留最小命令集(如 SET k v 替代多次 APPEND)。
重写触发条件:
- 手动触发:BGREWRITEAOF
- 自动触发(满足任一):
- 当前AOF文件大小 > auto-aof-rewrite-min-size(默认64MB)
- 且增长比例 > auto-aof-rewrite-percentage(默认100%)
AOF重写流程
- 主线程fork子进程,子进程遍历当前数据库生成新AOF
- 主线程将新写入命令暂存aof_rewrite_buf
- 子进程写完后,主线程将aof_rewrite_buf追加到新文件
- 原子替换旧AOF文件
2.3 RDB vs AOF:选择策略
官方推荐组合使用:RDB用于快速恢复+AOF用于数据安全。
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 缓存型应用 | RDB为主 | 数据可重建,无需强一致性 |
| 计数/订单系统 | AOF+everysec | 丢失数据≤1秒,性能与安全平衡 |
| 混合部署集群 | 主节点AOF,从节点RDB | 主节点保障数据安全,从节点快速同步 |
混合持久化(Redis 4.0+)
开启 aof-use-rdb-preamble yes 后,AOF文件开头插入RDB快照,后续追加增量命令。优势:
- 重启速度提升50%以上(RDB部分快速加载)
- 数据丢失窗口缩小至1秒(AOF部分补偿)
- 兼容纯AOF恢复流程
三、内存管理与淘汰策略
3.1 内存分配器:jemalloc vs malloc
Redis默认使用jemalloc(5.0+),替代早期malloc,原因如下:
- 减少内存碎片:通过 arenas 与 bin 管理小内存分配
- 支持内存回收:主动调用
malloc_trim归还内存给OS - 多线程友好:无全局锁,提升并发性能
fragmentation_ratio = used_memory_rss / used_memory。>1.5 表示碎片严重,需重启或手动触发 MEMORY PURGE。
3.2 过期键删除策略
Redis采用定期删除 + 惰性删除组合策略:
定期删除(Active Expiration)
serverCron任务每100ms执行一次,随机检查20个键的过期时间,删除已过期键。若过期键比例高,会增加检查数量,避免内存堆积。
惰性删除(Lazy Deletion)
访问键时检查是否过期,若过期立即删除并返回空。确保不会读到已过期数据。
内存不足时的淘汰策略
| 策略名称 | 适用场景 | 算法说明 | |
|---|---|---|---|
| noeviction | 写操作返回错误 | 不淘汰,OOM时写操作失败(默认) | |
| allkeys-lru | 通用缓存 | 从所有键中淘汰最近最少使用的 | Redis 3.0后使用近似LRU(随机采样5个键,淘汰最久未访问) |
| allkeys-lfu | 热点数据优先 | 基于访问频率(LFU),计数衰减机制 | 每分钟衰减一次,避免冷数据误淘汰 |
| volatile-ttl | 预设过期时间的数据 | 仅从设置了TTL的键中淘汰,TTL最小优先 |
3.3 内存回收:对象共享与延迟释放
共享对象池(Shared Objects):Redis启动时创建0~9999的整数对象,所有相同整数键共享同一对象,节省内存。
SET k 100),字符串键需满足 OBJECT ENCODING == embstr 且长度≤39字节。
延迟释放(Lazy Free):Redis 4.0引入,异步释放大对象(如大Key、慢查询),避免阻塞主线程:
UNLINK key:异步删除FLUSHALL ASYNC/FLUSHDB ASYNC- 配置项:
lazyfree-lazy-eviction yes
四、高可用方案:主从复制与哨兵
4.1 主从复制原理与全量/部分同步
Redis复制基于 PSYNC 命令实现,支持全量复制与部分复制。
全量复制(Full Resynchronization)
触发条件:首次复制或主节点历史复制偏移量丢失(buffer过小)。
- 从节点发送
PSYNC ? -1(问号表示未知runid,-1表示未知offset) - 主节点返回
FULLRESYNC <runid> <offset>,并启动BGSAVE - 从节点接收RDB文件,清空自身数据,加载RDB
- 主节点发送缓冲区中的写命令(复制buffer)
部分复制(Partial Resynchronization)
触发条件:网络短暂中断,主节点保留足够复制偏移量。
- 从节点发送
PSYNC <runid> <offset> - 主节点检查runid匹配且offset在复制buffer范围内
- 返回
+CONTINUE,仅发送缺失部分数据
repl-backlog-size(默认1MB)。若写入量大,需调大避免全量同步。
4.2 哨兵(Sentinel)高可用架构
Sentinel实现自动故障转移,核心功能:
- 监控:每秒向主/从节点发送PING,超时(down-after-milliseconds)标记为ODOWN
- 通知:通过发布订阅通知客户端
- 故障转移:选举从节点升级为主节点
- 配置更新:客户端通过
SENTINEL GET-MASTER-ADDR-BY-NAME获取新主节点
故障转移流程
- 主节点SDOWN(主观下线)→ 多个Sentinel确认ODOWN(客观下线)
- 选举领头Sentinel(Raft算法)
- 领头Sentinel选择最优从节点(优先级、复制offset、运行时间)
- 从节点执行
SLAVEOF NO ONE升级为主节点 - 其他从节点执行
SLAVEOF <新主IP> <端口> - 原主节点恢复后,自动降级为从节点
五、集群架构:数据分片与一致性
5.1 Redis Cluster数据分片原理
Redis Cluster采用虚拟槽(slot)分片,共16384个槽(0~16383),每个键通过 CRC16(key) & 16383 映射到槽。
槽分配方式
- 手动分配:
CLUSTER ADDSLOTS <slot> ... - 自动分配:
redis-cli --cluster create
节点通信机制
节点间通过Gossip协议交换信息:
- 每秒向随机5个节点发送PING
- 收到PING回复后,若发现节点信息陈旧,更新本地元数据
- 故障检测:60%以上节点标记节点为PFAIL → OFAIL(客观下线)
MIGRATE <目标节点> "" <key> 0 5000,目标节点接收后返回OK,源节点删除键。
5.2 集群高可用与脑裂问题
集群通过主从副本实现高可用,但存在脑裂风险:
- 脑裂场景:主节点网络分区,从节点被提升为主,客户端写入新主;网络恢复后,原主数据丢失
- 解决方案:
- 设置
min-replicas-to-write 1:主节点至少有1个从节点才接受写 - 设置
min-replicas-max-lag 10:从节点延迟超过10秒不参与选举
六、线程模型与网络I/O
6.1 单线程为何高性能?
Redis单线程指命令执行线程,但I/O、持久化、集群通信由其他线程处理。其高性能源于:
- 纯内存操作:无磁盘I/O瓶颈
- 非阻塞I/O:epoll/kqueue事件驱动
- 避免线程切换:无锁竞争,无上下文切换开销
- 简单协议:RESP协议解析高效
6.2 网络模型:Reactor模式实现
Redis使用基于libevent/libev的事件库,核心结构:
事件循环流程:
- 处理时间事件(如过期键检查、cluster心跳)
- 调用
aeApiPoll(epoll_wait)阻塞等待I/O事件 - 处理就绪的文件事件(读/写)
- 循环执行