Redis 核心模块详解
Redis 是一个高性能的内存数据库,其核心架构由多个模块组成,每个模块负责不同的功能。本文详细介绍 Redis 的核心模块及其工作原理。
1. 事件驱动模型(Event Loop)
Redis 采用单线程事件驱动模型,使用 I/O 多路复用技术(epoll、kqueue、select)处理多个客户端连接。
核心组件
1.1 事件循环(aeEventLoop)
1 | // Redis 事件循环结构(简化) |
1.2 事件类型
- 文件事件(File Event):处理客户端连接、读写操作
- 时间事件(Time Event):处理定时任务,如过期键清理、RDB 持久化等
1.3 工作流程
- 初始化:创建事件循环,注册多路复用器(epoll/kqueue/select)
- 事件注册:将客户端连接注册为可读/可写事件
- 事件等待:调用
aeApiPoll()等待事件发生 - 事件处理:
- 处理文件事件:读取客户端命令、执行、返回结果
- 处理时间事件:执行定时任务
- 循环执行:重复步骤 3-4
优势
- 高性能:单线程避免了上下文切换和锁竞争
- 低延迟:事件驱动模型响应迅速
- 简单可靠:单线程模型避免了复杂的并发问题
2. 内存管理模块
Redis 需要高效管理内存,包括内存分配、回收、淘汰策略等。
2.1 内存分配器
Redis 使用多种内存分配策略:
- jemalloc(默认):Facebook 开发的内存分配器,性能优秀
- tcmalloc:Google 开发的内存分配器
- libc malloc:系统默认的内存分配器
2.2 内存淘汰策略
当内存达到 maxmemory 限制时,Redis 会根据配置的策略删除键:
1 | # 配置示例 |
淘汰策略:
- noeviction:不删除,返回错误(默认)
- allkeys-lru:从所有键中选择最近最少使用的键删除
- volatile-lru:从设置了过期时间的键中选择最近最少使用的删除
- allkeys-lfu:从所有键中选择使用频率最低的键删除
- volatile-lfu:从设置了过期时间的键中选择使用频率最低的删除
- allkeys-random:随机删除任意键
- volatile-random:随机删除设置了过期时间的键
- volatile-ttl:删除最接近过期时间的键
2.3 渐进式删除(Lazy Free)
Redis 4.0 引入渐进式删除,避免大键删除时阻塞主线程:
1 | # 配置示例 |
2.4 内存压缩
对于字符串类型,Redis 使用多种编码方式节省内存:
- int:整数编码(8 字节)
- embstr:小于 44 字节的字符串,嵌入到对象中
- raw:大于 44 字节的字符串,单独分配内存
3. 数据结构模块
Redis 提供丰富的数据结构,每种结构都有多种底层实现,根据数据特征自动选择最优编码方式。
3.1 Redis 对象系统(redisObject)
Redis 使用对象系统统一管理所有数据结构:
1 | // Redis 对象结构(简化) |
对象类型(type):
OBJ_STRING:字符串类型OBJ_LIST:列表类型OBJ_HASH:哈希类型OBJ_SET:集合类型OBJ_ZSET:有序集合类型
编码方式(encoding):同一类型可能使用不同的编码方式,Redis 会根据数据大小和特性自动选择最优编码。
3.2 字符串(String)
3.2.1 底层实现
1. INT 编码(REDIS_ENCODING_INT)
- 适用场景:整数字符串(范围:-2^63 到 2^63-1)
- 存储方式:整数直接存储在
ptr中,不额外分配内存 - 优势:节省内存,无需额外的字符串对象
2. EMBSTR 编码(REDIS_ENCODING_EMBSTR)
- 适用场景:字符串长度 ≤ 44 字节
- 存储方式:字符串嵌入到
redisObject中,使用连续内存 - 优势:
- 减少内存分配次数(只需分配一次)
- 提高缓存局部性
- 减少内存碎片
3. RAW 编码(REDIS_ENCODING_RAW)
- 适用场景:字符串长度 > 44 字节
- 存储方式:使用 SDS(Simple Dynamic String)存储
- 结构:
ptr指向 SDS 结构
3.2.2 SDS(Simple Dynamic String)结构
1 | struct sdshdr { |
SDS 的优势:
- O(1) 获取长度:无需遍历字符串
- 二进制安全:可以存储任意二进制数据,包括 ‘\0’
- 预分配空间:减少内存重分配次数
- 自动扩容:空间不足时自动扩展
SDS 的扩容策略:
- 如果修改后长度 < 1MB,分配 2 倍空间
- 如果修改后长度 ≥ 1MB,分配额外 1MB 空间
使用场景:
- 缓存
- 计数器
- 分布式锁
- 二进制数据存储
3.3 列表(List)
3.3.1 底层实现
1. ZIPLIST 编码(REDIS_ENCODING_ZIPLIST)
- 适用场景:元素数量少且元素小
- 转换条件:
- 任意元素长度 > 64 字节,或
- 元素数量 > 512
2. QUICKLIST 编码(REDIS_ENCODING_QUICKLIST,Redis 3.2+ 默认)
- 适用场景:大多数场景
- 结构:双向链表 + ZIPLIST 的组合
- 特点:每个节点是一个 ZIPLIST
3. LINKEDLIST 编码(REDIS_ENCODING_LINKEDLIST,已废弃)
- Redis 3.2 之前使用
- 已被 QUICKLIST 替代
3.3.2 ZIPLIST(压缩列表)结构
内存布局:
1 | <zlbytes> <zltail> <zllen> <entry1> <entry2> ... <entryN> <zlend> |
zlbytes(4 字节):整个 ZIPLIST 占用的字节数zltail(4 字节):到达表尾节点的偏移量zllen(2 字节):节点数量(如果节点数 ≥ 65535,需要遍历整个列表)entry:节点数据zlend(1 字节):结束标记(0xFF)
节点结构(entry):
1 | <prevlen> <encoding> <data> |
prevlen:前一个节点的长度(用于反向遍历)encoding:编码方式(整数或字符串)data:实际数据
ZIPLIST 的优势:
- 内存紧凑,减少内存碎片
- 适合存储小数据
ZIPLIST 的劣势:
- 插入/删除需要重新分配内存
- 节点过多时性能下降
3.3.3 QUICKLIST(快速列表)结构
1 | typedef struct quicklist { |
QUICKLIST 的优势:
- 结合了 ZIPLIST 和链表的优点
- 支持压缩(LZF 算法),减少内存占用
- 性能与内存使用平衡
- 支持头尾快速插入/删除
使用场景:
- 消息队列
- 最新列表
- 栈和队列
3.4 哈希(Hash)
3.4.1 底层实现
1. ZIPLIST 编码(REDIS_ENCODING_ZIPLIST)
- 适用场景:字段少且值小
- 转换条件:
- 任意键值对长度 > 64 字节,或
- 键值对数量 > 512
- 存储方式:按顺序存储 field-value 对
2. HT 编码(REDIS_ENCODING_HT)
- 适用场景:字段多或值大
- 结构:字典(Dict/HashTable)
3.4.2 字典(Dict)结构
1 | typedef struct dict { |
哈希函数:使用 MurmurHash2 算法,性能优秀,分布均匀
解决冲突:链地址法(拉链法),冲突的键值对形成链表
渐进式 Rehash:
- 触发条件:
- 扩容:当负载因子(used/size)> 1 时触发
- 缩容:当负载因子 < 0.1 时触发
- 执行过程:
- 分配
ht[1]空间(大小为 2 的幂次) - 将
rehashidx设为 0,开始 rehash - 每次对字典执行增删改查操作时,将
ht[0]在rehashidx索引位置的所有键值对 rehash 到ht[1] rehashidx加 1- 当所有键值对都 rehash 完成后,将
ht[1]设为ht[0],ht[1]清空
- 分配
- 优势:避免一次性 rehash 造成阻塞,保证服务可用性
使用场景:
- 对象存储(用户信息、商品信息等)
- 小对象使用 ZIPLIST,大对象使用 HT
3.5 集合(Set)
3.5.1 底层实现
1. INTSET 编码(REDIS_ENCODING_INTSET)
- 适用场景:所有元素都是整数
- 转换条件:
- 元素数量 > 512,或
- 包含非整数元素
- 结构:有序整数数组
2. HT 编码(REDIS_ENCODING_HT)
- 适用场景:包含非整数元素或元素数量多
- 结构:字典(value 为 NULL,只存储 key)
3.5.2 INTSET(整数集合)结构
1 | typedef struct intset { |
INTSET 的特点:
- 元素按从小到大排序
- 支持三种整数编码(16/32/64 位)
- 自动升级编码(当新元素超出当前编码范围时)
INTSET 的升级过程:
- 根据新元素类型,扩展数组大小
- 将原有元素转换为新类型
- 将新元素添加到数组
- 更新 encoding 属性
INTSET 的优势:
- 内存紧凑,节省空间
- 查找效率高(二分查找,O(log N))
- 支持范围查询
使用场景:
- 纯整数集合(用户ID、商品ID等)
- 集合运算(交集、并集、差集)
- 去重操作
3.6 有序集合(Sorted Set)
3.6.1 底层实现
1. ZIPLIST 编码(REDIS_ENCODING_ZIPLIST)
- 适用场景:元素少且小
- 转换条件:
- 任意元素长度 > 64 字节,或
- 元素数量 > 128
- 存储方式:按 score 排序,存储 member-score 对
2. SKIPLIST 编码(REDIS_ENCODING_SKIPLIST)
- 适用场景:大多数场景
- 结构:跳跃表 + 字典
3.6.2 跳跃表(SkipList)结构
1 | typedef struct zset { |
跳跃表原理:
- 多层有序链表结构
- 底层包含所有元素,上层是索引层
- 查找、插入、删除时间复杂度 O(log N)
- 通过随机层数保证平衡性
跳跃表的查找过程:
- 从最高层开始,向右查找
- 如果当前节点的 score 小于目标 score,继续向右
- 如果当前节点的 score 大于目标 score,向下一层
- 重复步骤 2-3,直到找到目标或到达底层
跳跃表的插入过程:
- 查找插入位置
- 随机生成层数(1 到 MAX_LEVEL)
- 创建新节点,连接各层的前后指针
- 更新各层的跨度
跳跃表的优势:
- 实现简单,相比平衡树更容易维护
- 支持范围查询(ZRANGE、ZREVRANGE)
- 性能接近平衡树(红黑树)
- 支持排名查询(ZRANK、ZREVRANK)
为什么使用跳跃表 + 字典:
- 字典:O(1) 查找 member 的 score(ZSCORE 命令)
- 跳跃表:O(log N) 范围查询和排序(ZRANGE、ZRANK 等)
使用场景:
- 排行榜(游戏排行、热门文章等)
- 延迟队列(按时间排序)
- 范围查询(按分数范围查询)
3.7 编码转换总结表
| 数据类型 | 编码方式 | 转换条件 |
|---|---|---|
| String | INT | 整数字符串 |
| EMBSTR | 长度 ≤ 44 字节 | |
| RAW | 长度 > 44 字节 | |
| List | ZIPLIST | 元素 ≤ 64 字节 且 数量 ≤ 512 |
| QUICKLIST | 默认(Redis 3.2+) | |
| Hash | ZIPLIST | 键值对 ≤ 64 字节 且 数量 ≤ 512 |
| HT | 键值对 > 64 字节 或 数量 > 512 | |
| Set | INTSET | 所有元素都是整数 且 数量 ≤ 512 |
| HT | 包含非整数 或 数量 > 512 | |
| ZSet | ZIPLIST | 元素 ≤ 64 字节 且 数量 ≤ 128 |
| SKIPLIST | 元素 > 64 字节 或 数量 > 128 |
3.8 内存优化策略
Redis 通过多种策略优化内存使用:
- 编码自动选择:根据数据特征自动选择最优编码方式
- 压缩列表:小数据使用 ZIPLIST 节省内存
- 整数编码:整数使用 INT 编码,无需额外的字符串对象
- 共享对象:小整数(0-9999)共享对象池,减少内存占用
- 渐进式 Rehash:避免一次性迁移造成内存峰值
- 压缩:QUICKLIST 支持 LZF 压缩,减少内存占用
3.9 性能特点总结
| 数据结构 | 查找 | 插入 | 删除 | 范围查询 | 说明 |
|---|---|---|---|---|---|
| String | O(1) | O(1) | O(1) | - | 直接访问 |
| List | O(N) | O(1) | O(1) | O(N) | 头尾 O(1),中间 O(N) |
| Hash | O(1) | O(1) | O(1) | O(N) | 字典实现 |
| Set | O(1) | O(1) | O(1) | O(N) | 字典实现 |
| ZSet | O(1) | O(log N) | O(log N) | O(log N + M) | 跳跃表实现 |
3.10 实际应用建议
String
- 缓存、计数器、分布式锁
- 大字符串使用 RAW 编码
List
- 消息队列、最新列表
- 优先使用 QUICKLIST(默认)
Hash
- 对象存储、用户信息
- 小对象使用 ZIPLIST,大对象使用 HT
Set
- 去重、集合运算
- 纯整数集合使用 INTSET
ZSet
- 排行榜、延迟队列
- 大集合使用 SKIPLIST(默认)
4. 命令处理模块
4.1 命令执行流程
- 接收命令:从客户端连接读取命令
- 解析命令:将命令字符串解析为命令对象
- 查找命令:在命令表中查找对应的命令处理器
- 参数验证:检查参数数量、类型是否正确
- 执行命令:调用命令处理器执行命令
- 返回结果:将结果发送给客户端
4.2 命令表
Redis 维护一个命令表,包含所有支持的命令及其处理器:
1 | // 命令结构(简化) |
4.3 命令分类
- 读命令:GET、HGET、LRANGE 等
- 写命令:SET、HSET、LPUSH 等
- 管理命令:CONFIG、INFO、DEBUG 等
- 事务命令:MULTI、EXEC、DISCARD 等
5. 持久化模块
Redis 提供两种持久化方式:RDB 和 AOF。
5.1 RDB 持久化
工作原理:
- 在指定时间点生成数据快照
- 将内存数据序列化为二进制文件
触发方式:
- 手动触发:
SAVE(阻塞)、BGSAVE(后台) - 自动触发:根据配置的
save规则
1 | save 900 1 # 900秒内至少1个键变化 |
BGSAVE 实现:
- 主进程 fork 子进程
- 子进程将数据写入 RDB 文件
- 主进程继续处理请求(使用写时复制 COW)
5.2 AOF 持久化
工作原理:
- 记录所有写命令
- 追加到 AOF 文件末尾
同步策略:
- always:每次写操作都同步(最安全,性能最低)
- everysec:每秒同步一次(默认,平衡性能和安全)
- no:由操作系统决定(性能最好,安全性最低)
1 | appendonly yes |
AOF 重写:
- 目的:压缩 AOF 文件,去除冗余命令
- 触发:文件大小增长超过指定百分比
- 实现:fork 子进程,将当前数据库状态写入新 AOF 文件
5.3 混合持久化
Redis 4.0+ 支持混合持久化:
1 | aof-use-rdb-preamble yes |
工作原理:
- AOF 文件前半部分是 RDB 格式(快照)
- AOF 文件后半部分是 AOF 格式(增量命令)
优势:
- 恢复速度快(RDB 部分)
- 数据安全性高(AOF 部分)
6. 复制模块(Replication)
Redis 复制用于实现主从复制,提供数据冗余和读扩展。
6.1 复制流程
6.1.1 建立连接
- 从节点向主节点发送
PSYNC命令 - 主节点响应,开始复制
6.1.2 全量同步(Full Sync)
触发条件:
- 从节点首次连接主节点
- 从节点复制偏移量不在主节点复制积压缓冲区中
流程:
- 主节点执行
BGSAVE,生成 RDB 文件 - 主节点将 RDB 文件发送给从节点
- 从节点接收 RDB 文件,加载到内存
- 主节点将 RDB 生成期间的命令发送给从节点
- 从节点执行这些命令,完成同步
6.1.3 部分同步(Partial Sync)
触发条件:
- 从节点复制偏移量在主节点复制积压缓冲区中
流程:
- 主节点从复制积压缓冲区发送缺失的命令
- 从节点执行这些命令,完成同步
6.2 复制积压缓冲区(Replication Backlog)
作用:
- 保存主节点最近执行的写命令
- 用于部分同步
配置:
1 | repl-backlog-size 1mb |
6.3 心跳机制
作用:
- 检测主从连接是否正常
- 从节点向主节点报告复制偏移量
配置:
1 | repl-ping-replica-period 10 |
7. 哨兵模块(Sentinel)
Redis Sentinel 用于监控 Redis 主从节点,实现自动故障转移。
7.1 核心功能
- 监控(Monitoring):定期检查主从节点状态
- 通知(Notification):发现故障时通知管理员
- 自动故障转移(Automatic Failover):主节点故障时自动提升从节点
- 配置提供者(Configuration Provider):为客户端提供主节点地址
7.2 故障检测
7.2.1 主观下线(SDOWN)
- 单个 Sentinel 认为节点不可用
- 通过
PING命令检测
7.2.2 客观下线(ODOWN)
- 多个 Sentinel 都认为节点不可用
- 需要达到
quorum数量
1 | sentinel monitor mymaster 127.0.0.1 6379 2 |
7.3 故障转移流程
- 客观下线确认:多个 Sentinel 确认主节点下线
- 选举领头 Sentinel:使用 Raft 算法选举
- 选择新主节点:
- 排除不健康的从节点
- 选择优先级高的从节点
- 选择复制偏移量大的从节点
- 故障转移:
- 将选中的从节点提升为主节点
- 通知其他从节点复制新主节点
- 更新客户端配置
8. 集群模块(Cluster)
Redis Cluster 提供分布式解决方案,实现数据分片和高可用。
8.1 数据分片
槽(Slot)分配:
- Redis 集群将键空间划分为 16384 个槽
- 每个主节点负责一部分槽
- 键通过
CRC16(key) % 16384计算槽号
槽分配示例:
1 | 节点 A:0-5460 |
8.2 节点通信
Gossip 协议:
- 节点间定期交换信息
- 包括节点状态、槽分配、故障信息等
消息类型:
PING:检测节点是否存活PONG:响应 PINGMEET:请求加入集群FAIL:标记节点下线PUBLISH:发布订阅消息
8.3 故障转移
8.3.1 故障检测
- 节点间通过
PING检测状态 - 节点长时间未响应,标记为 PFAIL(疑似下线)
- 多数节点认为节点下线,标记为 FAIL(已下线)
8.3.2 选举流程
- 从节点发起选举:发现主节点下线后等待一段时间
- 主节点投票:每个主节点在一个配置纪元内只能投一票
- 选举成功:从节点获得超过半数主节点投票
- 接管槽位:新主节点接管原主节点的槽位
8.4 客户端重定向
MOVED 重定向:
- 客户端请求的键不在当前节点
- 返回
MOVED <slot> <ip>:<port> - 客户端更新槽位映射,重定向到正确节点
ASK 重定向:
- 槽位正在迁移中
- 返回
ASK <slot> <ip>:<port> - 客户端临时重定向,不更新槽位映射
9. 发布订阅模块(Pub/Sub)
Redis 提供发布订阅功能,实现消息传递。
9.1 核心概念
- 频道(Channel):消息的通道
- 发布者(Publisher):发送消息
- 订阅者(Subscriber):接收消息
9.2 命令
1 | # 订阅频道 |
9.3 实现原理
- 维护频道订阅关系字典
- 发布消息时遍历订阅者,发送消息
- 使用链表存储订阅者
10. 过期键处理模块
Redis 支持键的过期时间,自动删除过期键。
10.1 过期时间设置
1 | EXPIRE key 60 # 60秒后过期 |
10.2 过期键删除策略
10.2.1 定时删除
- 设置过期时间时创建定时器
- 过期时立即删除
- 优点:内存释放及时
- 缺点:CPU 开销大
10.2.2 惰性删除
- 访问键时检查是否过期
- 过期则删除
- 优点:CPU 开销小
- 缺点:内存可能泄漏
10.2.3 定期删除
- 定期扫描过期键并删除
- Redis 默认采用惰性删除 + 定期删除
定期删除流程:
- 从过期字典中随机抽取 20 个键
- 删除其中过期的键
- 如果过期键比例 > 25%,重复步骤 1
10.3 过期键处理时机
- 读操作:GET、HGET 等,惰性删除
- 写操作:SET、HSET 等,先删除过期键再写入
- 定期任务:每秒执行 10 次定期删除
11. 事务模块
Redis 提供简单的事务支持。
11.1 事务命令
1 | MULTI # 开始事务 |
11.2 事务特性
- 原子性:所有命令要么全部执行,要么全部不执行
- 隔离性:事务执行期间不会被其他客户端打断
- 一致性:事务执行前后数据库状态一致
- 持久性:取决于持久化配置
11.3 WATCH 机制
作用:
- 监视键是否被其他客户端修改
- 如果被修改,事务执行失败
实现原理:
- 维护被监视键的字典
- 执行写命令时检查键是否被监视
- 如果被监视,标记为脏键,事务执行时返回空
12. Lua 脚本模块
Redis 支持执行 Lua 脚本,实现复杂的原子操作。
12.1 脚本执行
1 | # 执行脚本 |
12.2 脚本特性
- 原子性:脚本执行期间不会被其他命令打断
- 可复制:主节点执行脚本,从节点重放
- 可持久化:脚本会记录到 AOF 文件
12.3 脚本限制
- 执行时间限制:默认 5 秒,可通过
lua-time-limit配置 - 脚本阻塞:脚本执行期间阻塞其他命令
- 脚本缓存:Redis 会缓存已执行的脚本
13. 慢查询日志模块
Redis 记录执行时间超过阈值的命令。
13.1 配置
1 | slowlog-log-slower-than 10000 # 记录超过10毫秒的命令 |
13.2 命令
1 | SLOWLOG GET 10 # 获取最近10条慢查询 |
13.3 实现原理
- 使用链表存储慢查询日志
- 每条记录包含:ID、时间戳、执行时间、命令、客户端信息
14. 客户端管理模块
Redis 管理所有客户端连接。
14.1 客户端结构
1 | // 客户端结构(简化) |
14.2 客户端限制
1 | maxclients 10000 # 最大客户端连接数 |
14.3 客户端命令
1 | CLIENT LIST # 列出所有客户端 |
15. 模块化架构
Redis 4.0+ 支持模块化扩展,可以加载自定义模块。
15.1 模块加载
1 | loadmodule /path/to/module.so |
15.2 模块 API
- 数据类型 API:定义新的数据类型
- 命令 API:定义新的命令
- 键空间通知 API:监听键变化
- 阻塞命令 API:实现阻塞操作
总结
Redis 的核心模块相互协作,共同实现了高性能、高可用的内存数据库:
- 事件驱动模型:提供高性能的网络处理能力
- 内存管理:高效管理内存,支持多种淘汰策略
- 数据结构:丰富的数据类型,灵活的编码方式
- 命令处理:统一的命令执行框架
- 持久化:RDB 和 AOF 保证数据安全
- 复制:主从复制实现数据冗余和读扩展
- 哨兵:自动故障转移,提高可用性
- 集群:分布式部署,水平扩展
- 发布订阅:消息传递功能
- 过期键处理:自动清理过期数据
- 事务:简单的原子操作支持
- Lua 脚本:复杂逻辑的原子执行
- 慢查询日志:性能监控和优化
- 客户端管理:连接管理和限制
这些模块共同构成了 Redis 强大的功能体系,使其成为高性能应用的理想选择。