目录
[1.1 动态字符串(SDS)原理](#1.1 动态字符串(SDS)原理)
[1.1.1 传统C字符串的问题](#1.1.1 传统C字符串的问题)
[1.1.2 SDS结构设计](#1.1.2 SDS结构设计)
[1.1.3 SDS核心优势](#1.1.3 SDS核心优势)
[1.1.4 SDS编码类型](#1.1.4 SDS编码类型)
[1.2 整数集合(IntSet)原理](#1.2 整数集合(IntSet)原理)
[1.2.1 IntSet结构设计](#1.2.1 IntSet结构设计)
[1.2.2 类型升级机制](#1.2.2 类型升级机制)
[1.2.3 IntSet特性总结](#1.2.3 IntSet特性总结)
[1.3 字典(Dict)原理](#1.3 字典(Dict)原理)
[1.3.1 Dict三组件结构](#1.3.1 Dict三组件结构)
[1.3.2 哈希计算与冲突解决](#1.3.2 哈希计算与冲突解决)
[1.3.3 渐进式Rehash过程](#1.3.3 渐进式Rehash过程)
[1.3.4 扩容与收缩触发条件](#1.3.4 扩容与收缩触发条件)
[1.4 压缩列表(ZipList)原理](#1.4 压缩列表(ZipList)原理)
[1.4.1 ZipList整体结构](#1.4.1 ZipList整体结构)
[1.4.2 Entry编码格式](#1.4.2 Entry编码格式)
[1.4.3 连锁更新问题](#1.4.3 连锁更新问题)
[1.5 快速列表(QuickList)原理](#1.5 快速列表(QuickList)原理)
[1.5.1 设计动机](#1.5.1 设计动机)
[1.5.2 QuickList结构](#1.5.2 QuickList结构)
[1.5.3 配置参数](#1.5.3 配置参数)
[1.5.4 节点压缩](#1.5.4 节点压缩)
[1.6 跳表(SkipList)原理](#1.6 跳表(SkipList)原理)
[1.6.1 SkipList结构设计](#1.6.1 SkipList结构设计)
[1.6.2 层高生成算法](#1.6.2 层高生成算法)
[1.6.3 查找过程示例](#1.6.3 查找过程示例)
[1.6.4 SkipList与平衡树对比](#1.6.4 SkipList与平衡树对比)
[1.7 Redis对象系统](#1.7 Redis对象系统)
[1.7.1 RedisObject结构](#1.7.1 RedisObject结构)
[1.7.2 编码方式映射](#1.7.2 编码方式映射)
[1.7.3 内存优化策略](#1.7.3 内存优化策略)
[1.8 各数据类型内部实现](#1.8 各数据类型内部实现)
[1.8.1 字符串(String)实现](#1.8.1 字符串(String)实现)
[1.8.2 列表(List)实现演变](#1.8.2 列表(List)实现演变)
[1.8.3 集合(Set)实现策略](#1.8.3 集合(Set)实现策略)
[1.8.4 有序集合(ZSet)实现方案](#1.8.4 有序集合(ZSet)实现方案)
[1.8.5 哈希(Hash)实现机制](#1.8.5 哈希(Hash)实现机制)
[2.1 用户空间与内核空间](#2.1 用户空间与内核空间)
[2.1.1 内存空间划分](#2.1.1 内存空间划分)
[2.1.2 系统调用开销分析](#2.1.2 系统调用开销分析)
[2.2 五种IO模型对比](#2.2 五种IO模型对比)
[2.2.1 阻塞IO(Blocking IO)](#2.2.1 阻塞IO(Blocking IO))
[2.2.2 非阻塞IO(Non-blocking IO)](#2.2.2 非阻塞IO(Non-blocking IO))
[2.2.3 IO多路复用(IO Multiplexing)](#2.2.3 IO多路复用(IO Multiplexing))
[2.2.4 信号驱动IO(Signal Driven IO)](#2.2.4 信号驱动IO(Signal Driven IO))
[2.2.5 异步IO(Asynchronous IO)](#2.2.5 异步IO(Asynchronous IO))
[2.3 Redis单线程模型](#2.3 Redis单线程模型)
[2.3.1 Redis 6.0前单线程架构](#2.3.1 Redis 6.0前单线程架构)
[2.3.2 Redis多线程演进](#2.3.2 Redis多线程演进)
[2.3.3 Redis 6.0+混合模型](#2.3.3 Redis 6.0+混合模型)
[2.4 Redis事件循环机制](#2.4 Redis事件循环机制)
[2.4.1 事件处理器结构](#2.4.1 事件处理器结构)
[2.4.2 文件事件与时间事件](#2.4.2 文件事件与时间事件)
[2.4.3 事件处理流程](#2.4.3 事件处理流程)
[3.1 RESP协议详解](#3.1 RESP协议详解)
[3.1.1 RESP协议类型](#3.1.1 RESP协议类型)
[3.1.2 协议解析示例](#3.1.2 协议解析示例)
[3.1.3 自定义客户端实现](#3.1.3 自定义客户端实现)
[3.2 Redis内存回收机制](#3.2 Redis内存回收机制)
[3.2.1 过期键管理](#3.2.1 过期键管理)
[3.2.2 内存淘汰策略](#3.2.2 内存淘汰策略)
[3.2.3 内存淘汰执行流程](#3.2.3 内存淘汰执行流程)
[4.1 数据结构选择策略](#4.1 数据结构选择策略)
[4.2 内存优化建议](#4.2 内存优化建议)
[4.3 性能调优要点](#4.3 性能调优要点)
[4.4 监控与诊断](#4.4 监控与诊断)
[4.4.1 关键监控指标](#4.4.1 关键监控指标)
[4.4.2 性能问题诊断流程](#4.4.2 性能问题诊断流程)
[5.1 学习阶段划分](#5.1 学习阶段划分)
[5.2 实践建议](#5.2 实践建议)
本文深入剖析Redis高性能背后的核心原理,从数据结构到网络模型进行全面解析。首先详细介绍了Redis的6种核心数据结构实现:动态字符串SDS的预分配策略、整数集合IntSet的类型升级机制、字典Dict的渐进式Rehash、压缩列表ZipList的连锁更新问题、快速列表QuickList的平衡设计以及跳表SkipList的随机层高算法。其次深入讲解了Redis网络模型,包括五种IO模型对比、单线程架构优势、6.0版本的多线程演进,以及基于epoll的事件循环机制。最后阐述了Redis的内存管理策略,包括过期键删除、8种内存淘汰算法实现,并提供了性能优化建议和监控诊断方法。通过揭示这些底层机制,帮助开发者深入理解Redis的高效运作原理。
概述
Redis高性能的背后是其精心设计的数据结构和网络模型。本篇文章将深入剖析Redis的底层实现原理,从基本数据结构到网络通信模型,全面揭示Redis高效运行的秘密。
一、Redis数据结构原理
1.1 动态字符串(SDS)原理
1.1.1 传统C字符串的问题
C语言原生的字符串存在以下缺陷:
-
长度获取需遍历 :
strlen()需要O(n)时间 -
非二进制安全:不能存储包含'\0'的数据
-
缓冲区溢出风险:需手动管理内存
-
频繁内存分配:每次修改都需要重新分配
1.1.2 SDS结构设计
cpp
// Redis SDS 简化结构
struct sdshdr {
int len; // 已使用字节数
int free; // 空闲字节数
char buf[]; // 柔性数组,存储实际字符串
};
SDS内存布局示例:
cpp
SDS存储"Redis":
+-----+-----+-------------------------+
| len | free| R | e | d | i | s | \0 |
+-----+-----+-------------------------+
5 0 0 1 2 3 4 5 (索引)
1.1.3 SDS核心优势
-
O(1)获取长度:直接读取len字段
-
二进制安全:使用len判断结束,而非'\0'
-
杜绝缓冲区溢出:修改前检查空间
-
内存预分配策略:
cpp// 内存分配策略 if (新长度 < 1MB) { 新空间 = 2 * 新长度 + 1 } else { 新空间 = 新长度 + 1MB + 1 } -
惰性空间释放:缩短时不立即回收,留作备用
1.1.4 SDS编码类型
| 类型 | 描述 | 适用场景 |
|---|---|---|
| SDS_TYPE_5 | len用5位表示 | 极小字符串 |
| SDS_TYPE_8 | len用8位表示 | 短字符串 |
| SDS_TYPE_16 | len用16位表示 | 中等字符串 |
| SDS_TYPE_32 | len用32位表示 | 长字符串 |
| SDS_TYPE_64 | len用64位表示 | 超长字符串 |
1.2 整数集合(IntSet)原理
1.2.1 IntSet结构设计
cpp
typedef struct intset {
uint32_t encoding; // 编码方式
uint32_t length; // 元素数量
int8_t contents[]; // 存储数组(柔性数组)
} intset;
编码方式:
cpp
#define INTSET_ENC_INT16 (sizeof(int16_t)) // 2字节
#define INTSET_ENC_INT32 (sizeof(int32_t)) // 4字节
#define INTSET_ENC_INT64 (sizeof(int64_t)) // 8字节
1.2.2 类型升级机制
示例:添加大整数触发升级
cpp
初始状态:存储[1, 2, 3],encoding=INTSET_ENC_INT16
添加50000(超出int16范围)
1. 升级为INTSET_ENC_INT32
2. 倒序重新分配:3→2→1
3. 插入50000到末尾
4. 更新encoding和length
1.2.3 IntSet特性总结
-
有序存储:元素按升序排列
-
唯一性保证:自动去重
-
内存优化:自适应整数大小
-
二分查找:支持O(log n)查询
1.3 字典(Dict)原理
1.3.1 Dict三组件结构
cpp
// 哈希表节点
typedef struct dictEntry {
void *key; // 键
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v; // 值
struct dictEntry *next; // 链表下一个节点
} dictEntry;
// 哈希表
typedef struct dictht {
dictEntry **table; // 哈希表数组
unsigned long size; // 哈希表大小
unsigned long sizemask; // 大小掩码,用于计算索引值
unsigned long used; // 已有节点数量
} dictht;
// 字典
typedef struct dict {
dictType *type; // 类型特定函数
void *privdata; // 私有数据
dictht ht[2]; // 哈希表(2个,用于rehash)
long rehashidx; // rehash索引,-1表示未进行
int iterators; // 当前迭代器数量
} dict;
1.3.2 哈希计算与冲突解决
cpp
// 哈希值计算(简化版)
hash = dict->type->hashFunction(key);
index = hash & dict->ht[0].sizemask;
// 链表法解决冲突
// 新节点插入链表头部
1.3.3 渐进式Rehash过程
cpp
步骤:
1. 准备阶段:分配ht[1],设置rehashidx=0
2. 渐进迁移:每次操作迁移一个bucket
3. 完成阶段:ht[0]=ht[1],重置rehashidx=-1
迁移规则:
- 新增:直接写入ht[1]
- 查询/修改:ht[0]→ht[1]顺序查找
- 删除:两个表都要操作
1.3.4 扩容与收缩触发条件
cpp
// 扩容条件
if (负载因子 >= 1 && !(服务器正在执行BGSAVE或BGREWRITEAOF)) {
执行扩容();
} else if (负载因子 > 5) {
必须扩容();
}
// 收缩条件
if (负载因子 < 0.1) {
执行收缩();
}
负载因子 = used / size
1.4 压缩列表(ZipList)原理
1.4.1 ZipList整体结构
cpp
+--------+--------+--------+-------------+--------+
| zlbytes| zltail | zllen | entry1 | ... | entryN | zlend |
+--------+--------+--------+-------------+--------+
4字节 4字节 2字节 变长 变长 1字节
1.4.2 Entry编码格式
cpp
// Entry结构(变长)
+-------------------+------------+-----------+
| prev_entry_length | encoding | content |
+-------------------+------------+-----------+
1或5字节 1/2/5字节 变长
prev_entry_length规则:
cpp
if (前一节点长度 < 254) {
使用1字节存储
} else {
使用5字节:0xFE + 4字节长度
}
encoding编码规则:
cpp
// 字符串编码(开头二进制)
00xxxxxx // 长度<=63 (6位表示)
01xxxxxx xxxxxxxx // 长度<=16383 (14位表示)
10000000 xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx // 长度<=2^32-1
// 整数编码(开头11)
11000000 // int16_t
11010000 // int32_t
11100000 // int64_t
11110000 // 24位整数
11111110 // 8位整数
1111xxxx // 4位整数,xxxx范围0001-1101
1.4.3 连锁更新问题
场景分析:
cpp
初始状态:所有entry长度都在250-253字节之间
插入操作:在头部插入一个256字节的新entry
问题发生:
1. entry1的prev_entry_length从1字节变为5字节
2. entry1总长度从253变为257,触发entry2更新
3. 连锁反应直到遇到小entry
影响范围: 最坏情况O(n²)时间复杂度
1.5 快速列表(QuickList)原理
1.5.1 设计动机
-
ZipList问题:大内存申请困难,连续内存要求高
-
LinkedList问题:内存碎片多,指针开销大
-
折中方案:ZipList分片,链表连接
1.5.2 QuickList结构
cpp
typedef struct quicklist {
quicklistNode *head; // 头节点
quicklistNode *tail; // 尾节点
unsigned long count; // 所有ziplist的entry总数
unsigned long len; // quicklistNode节点数
int fill : 16; // 每个ziplist最大entry数
unsigned int compress : 16; // 压缩深度,0表示不压缩
} quicklist;
typedef struct quicklistNode {
struct quicklistNode *prev; // 前驱
struct quicklistNode *next; // 后继
unsigned char *zl; // 指向ziplist的指针
unsigned int sz; // ziplist字节大小
unsigned int count : 16; // ziplist中entry数量
unsigned int encoding : 2; // 编码方式:RAW=1, LZF=2
unsigned int container : 2; // 容器类型:NONE=1, ZIPLIST=2
unsigned int recompress : 1;// 是否被压缩过
unsigned int attempted_compress : 1; // 测试用
unsigned int extra : 10; // 预留位
} quicklistNode;
1.5.3 配置参数
cpp
# properties配置文件
# redis.conf配置
list-max-ziplist-size -2 # 每个ziplist最大64KB
list-compress-depth 0 # 不压缩
ziplist大小限制:
-
正值:最大entry数量
-
负值:最大内存大小
- -1: 4KB, -2: 8KB, -3: 16KB, -4: 32KB, -5: 64KB
1.5.4 节点压缩
cpp
// 压缩策略(LZF算法)
if (节点不在压缩深度范围内) {
保持不压缩;
} else {
使用LZF压缩;
}
// 压缩深度配置
// 0: 所有节点都不压缩
// 1: 头尾各1个节点不压缩
// 2: 头尾各2个节点不压缩
1.6 跳表(SkipList)原理
1.6.1 SkipList结构设计
cpp
typedef struct zskiplistNode {
sds ele; // 成员元素
double score; // 分值
struct zskiplistNode *backward; // 后退指针
struct zskiplistLevel {
struct zskiplistNode *forward; // 前进指针
unsigned long span; // 跨度
} level[]; // 层数组(柔性数组)
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail; // 头尾节点
unsigned long length; // 节点数量
int level; // 最大层数
} zskiplist;
1.6.2 层高生成算法
cpp
// 随机层高生成(幂次定律)
int zslRandomLevel(void) {
int level = 1;
while ((random() & 0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level < ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
// 概率:P=0.25,最大层数32
1.6.3 查找过程示例
cpp
查找score=7.5的节点:
1. 从最高层(3层)开始:header→L3(score=1,跨度=1)
2. L3下一个是NULL,下降一层
3. L2:header→L2(score=1)→L2(score=4,跨度=2)
4. L2下一个是score=9 > 7.5,下降一层
5. L1:当前节点score=4→L1(score=7,跨度=1)
6. L1下一个是score=9 > 7.5,找到score=7
1.6.4 SkipList与平衡树对比
| 特性 | SkipList | 红黑树/AVL树 |
|---|---|---|
| 实现复杂度 | 简单 | 复杂 |
| 范围查询 | 高效 | 需要中序遍历 |
| 内存占用 | 略高 | 略低 |
| 并发友好度 | 更高 | 较低 |
| 插入/删除 | O(log n) | O(log n) |
1.7 Redis对象系统
1.7.1 RedisObject结构
cpp
typedef struct redisObject {
unsigned type:4; // 类型(4位)
unsigned encoding:4; // 编码(4位)
unsigned lru:LRU_BITS; // LRU信息(24位)
int refcount; // 引用计数
void *ptr; // 指向实际数据的指针
} robj;
类型定义:
cpp
#define OBJ_STRING 0 // 字符串
#define OBJ_LIST 1 // 列表
#define OBJ_SET 2 // 集合
#define OBJ_ZSET 3 // 有序集合
#define OBJ_HASH 4 // 哈希
#define OBJ_MODULE 5 // 模块
#define OBJ_STREAM 6 // 流
1.7.2 编码方式映射
cpp
// 字符串编码
OBJ_ENCODING_INT // 整数编码
OBJ_ENCODING_EMBSTR // embstr编码
OBJ_ENCODING_RAW // raw编码
// 其他类型编码
OBJ_ENCODING_HT // 哈希表
OBJ_ENCODING_ZIPLIST // 压缩列表
OBJ_ENCODING_INTSET // 整数集合
OBJ_ENCODING_SKIPLIST // 跳表
OBJ_ENCODING_QUICKLIST // 快速列表
1.7.3 内存优化策略
字符串编码选择:
cpp
if (字符串可以表示为long long) {
使用OBJ_ENCODING_INT;
} else if (字符串长度 <= 44字节) {
使用OBJ_ENCODING_EMBSTR; // 连续内存
} else {
使用OBJ_ENCODING_RAW;
}
EMBSTR优势: 一次内存分配,连续空间,更好的缓存局部性
1.8 各数据类型内部实现
1.8.1 字符串(String)实现
cpp
// 编码转换逻辑
if (value是整数且在LONG_MAX范围内) {
encoding = OBJ_ENCODING_INT;
ptr直接存储整数值;
} else if (value长度 <= 44字节) {
encoding = OBJ_ENCODING_EMBSTR;
分配连续内存: sizeof(redisObject) + sizeof(sdshdr) + len + 1;
} else {
encoding = OBJ_ENCODING_RAW;
单独分配SDS内存;
}
1.8.2 列表(List)实现演变
cpp
// Redis 3.2之前
if (元素数量 < 512 && 每个元素 < 64字节) {
使用ZipList;
} else {
使用LinkedList;
}
// Redis 3.2之后
统一使用QuickList;
1.8.3 集合(Set)实现策略
cpp
if (所有元素都是整数 && 元素数量 <= set-max-intset-entries) {
使用IntSet;
} else {
使用HashTable (Dict);
// key=元素,value=NULL
}
1.8.4 有序集合(ZSet)实现方案
cpp
// 编码选择逻辑
if (元素数量 < zset-max-ziplist-entries &&
每个元素大小 < zset-max-ziplist-value) {
使用ZipList;
// 存储格式:element1, score1, element2, score2...
} else {
使用SkipList + Dict组合;
// SkipList: 用于范围查询和排序
// Dict: 用于O(1)查找score
}
1.8.5 哈希(Hash)实现机制
cpp
if (元素数量 < hash-max-ziplist-entries &&
每个元素大小 < hash-max-ziplist-value) {
使用ZipList;
// 存储格式:field1, value1, field2, value2...
} else {
使用HashTable (Dict);
}
二、Redis网络模型原理
2.1 用户空间与内核空间
2.1.1 内存空间划分
cpp
32位系统地址空间布局:
0x00000000 ┌─────────────────┐
│ 用户空间 │ 3GB
│ (Ring 3) │
0xC0000000 ├─────────────────┤
│ 内核空间 │ 1GB
│ (Ring 0) │
0xFFFFFFFF └─────────────────┘
2.1.2 系统调用开销分析
cpp
// 传统读写流程
用户空间 → 系统调用 → 内核空间 → 硬件设备
上下文切换 上下文切换
用户态→内核态 内核态→用户态
2.2 五种IO模型对比
2.2.1 阻塞IO(Blocking IO)
cpp
// 伪代码示例
while (true) {
fd = accept(server_fd); // 阻塞等待连接
n = read(fd, buffer, size); // 阻塞等待数据
process(buffer); // 处理数据
write(fd, response, len); // 写回响应
}
特点: 全程阻塞,简单但并发能力差
2.2.2 非阻塞IO(Non-blocking IO)
cpp
// 伪代码示例
set_nonblocking(server_fd); // 设置非阻塞
while (true) {
fd = accept(server_fd);
if (fd == EWOULDBLOCK) { // 没有连接
continue; // 忙等待
}
n = read(fd, buffer, size);
if (n == EWOULDBLOCK) { // 没有数据
continue;
}
// ... 处理数据
}
问题: CPU空转,资源浪费
2.2.3 IO多路复用(IO Multiplexing)
Select模型
cpp
// select使用示例
fd_set read_fds;
while (true) {
FD_ZERO(&read_fds);
FD_SET(server_fd, &read_fds);
max_fd = server_fd;
// 添加所有客户端fd
for (每个客户端) {
FD_SET(client_fd, &read_fds);
max_fd = MAX(max_fd, client_fd);
}
// 阻塞等待
select(max_fd + 1, &read_fds, NULL, NULL, NULL);
// 检查哪些fd就绪
if (FD_ISSET(server_fd, &read_fds)) {
// 有新连接
}
for (每个客户端) {
if (FD_ISSET(client_fd, &read_fds)) {
// 可读
}
}
}
Select缺点:
-
FD数量限制(1024)
-
每次需要拷贝整个fd_set
-
线性扫描O(n)复杂度
Poll模型
cpp
// poll改进
struct pollfd fds[MAX_CLIENTS];
while (true) {
poll(fds, nfds, timeout); // 无数量限制
// 仍需遍历所有fd
}
Epoll模型
cpp
// epoll核心API
int epoll_create(int size); // 创建epoll实例
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
// 工作流程
1. epoll_create() 创建红黑树和就绪链表
2. epoll_ctl() 添加/修改/删除监控的fd
3. epoll_wait() 等待事件就绪
4. 内核通过回调函数将就绪fd加入链表
5. 直接返回就绪的events数组
2.2.4 信号驱动IO(Signal Driven IO)
cpp
// 信号驱动示例
signal(SIGIO, sigio_handler); // 注册信号处理函数
fcntl(fd, F_SETOWN, getpid()); // 设置进程为所有者
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | FASYNC); // 启用异步
void sigio_handler(int sig) {
// 数据就绪,开始读取
read(fd, buffer, size);
process(buffer);
}
2.2.5 异步IO(Asynchronous IO)
cpp
// AIO示例(Linux)
struct aiocb cb;
cb.aio_fildes = fd;
cb.aio_buf = buffer;
cb.aio_nbytes = size;
cb.aio_offset = 0;
cb.aio_sigevent.sigev_notify = SIGEV_THREAD;
cb.aio_sigevent.sigev_notify_function = aio_completion_handler;
aio_read(&cb); // 非阻塞立即返回
// 内核完成所有工作后回调
void aio_completion_handler(sigval_t sigval) {
// 数据已准备好
process(buffer);
}
2.3 Redis单线程模型
2.3.1 Redis 6.0前单线程架构
cpp
客户端请求 → 事件分发器 → 文件事件处理器 → 命令处理 → 响应
(epoll/kqueue) (单线程) (单线程)
单线程优势分析:
-
避免锁竞争:无锁数据结构,减少上下文切换
-
避免死锁问题:无需考虑复杂的同步机制
-
降低复杂度:代码更简单,易于维护
-
CPU不是瓶颈:Redis性能瓶颈在内存和网络
2.3.2 Redis多线程演进
cpp
// Redis 4.0:后台线程处理耗时任务
背景线程执行:UNLINK、FLUSHALL ASYNC、FLUSHDB ASYNC
// Redis 6.0:网络IO多线程化
主线程:命令解析、执行
IO线程:网络读写(可配置数量)
2.3.3 Redis 6.0+混合模型
cpp
// 配置示例
io-threads 4 # 启用4个IO线程
io-threads-do-reads yes # IO线程处理读请求
// 工作流程
1. 主线程监听连接事件
2. 连接建立后,读操作交给IO线程
3. IO线程读取数据到缓冲区
4. 主线程执行命令
5. 写操作可交给IO线程
2.4 Redis事件循环机制
2.4.1 事件处理器结构
cpp
// 事件循环主函数
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
// 处理事件前的回调
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
// 处理事件
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
2.4.2 文件事件与时间事件
cpp
// 事件类型定义
#define AE_FILE_EVENTS 1 // 文件事件(网络IO)
#define AE_TIME_EVENTS 2 // 时间事件(定时任务)
#define AE_ALL_EVENTS (AE_FILE_EVENTS|AE_TIME_EVENTS)
#define AE_DONT_WAIT 4 // 不阻塞等待
// 时间事件结构
typedef struct aeTimeEvent {
long long id; // 时间事件ID
long when_sec; // 秒
long when_ms; // 毫秒
aeTimeProc *timeProc; // 时间事件处理器
aeEventFinalizerProc *finalizerProc;
void *clientData;
struct aeTimeEvent *next;
} aeTimeEvent;
2.4.3 事件处理流程
cpp
aeProcessEvents()流程:
1. 获取最近的时间事件
2. 计算等待时间(避免忙等待)
3. 调用aeApiPoll()等待事件
4. 处理文件事件
5. 处理时间事件
6. 返回处理的事件数量
三、Redis通信协议与内存管理
3.1 RESP协议详解
3.1.1 RESP协议类型
cpp
// RESP协议格式
+ 单行字符串(Simple Strings)
- 错误消息(Errors)
: 整数(Integers)
$ 多行字符串(Bulk Strings)
* 数组(Arrays)
3.1.2 协议解析示例
示例1:SET命令
bash
客户端发送:
*3\r\n # 数组,3个元素
$3\r\nSET\r\n # 字符串"SET",长度3
$4\r\nname\r\n # 字符串"name",长度4
$6\r\nredis\r\n # 字符串"redis",长度6
服务端响应:
+OK\r\n # 简单字符串"OK"
示例2:GET命令
bash
客户端发送:
*2\r\n # 数组,2个元素
$3\r\nGET\r\n # 字符串"GET",长度3
$4\r\nname\r\n # 字符串"name",长度4
服务端响应:
$5\r\nredis\r\n # 字符串"redis",长度5
示例3:错误响应
bash
-ERR unknown command 'foobar'\r\n
-WRONGTYPE Operation against a key holding the wrong kind of value\r\n
3.1.3 自定义客户端实现
java
public class RedisClient {
private Socket socket;
private PrintWriter writer;
private BufferedReader reader;
public Object sendCommand(String... args) throws IOException {
// 构建RESP协议格式
writer.print("*" + args.length + "\r\n");
for (String arg : args) {
writer.print("$" + arg.getBytes().length + "\r\n");
writer.print(arg + "\r\n");
}
writer.flush();
return parseResponse();
}
private Object parseResponse() throws IOException {
char prefix = (char) reader.read();
switch (prefix) {
case '+': return reader.readLine(); // 简单字符串
case '-': throw new RuntimeException(reader.readLine()); // 错误
case ':': return Long.parseLong(reader.readLine()); // 整数
case '$':
int len = Integer.parseInt(reader.readLine());
if (len == -1) return null;
if (len == 0) {
reader.readLine(); // 读取空行
return "";
}
char[] chars = new char[len];
reader.read(chars, 0, len);
reader.readLine(); // 读取CRLF
return new String(chars);
case '*':
return parseArray();
default:
throw new IOException("无效协议");
}
}
}
3.2 Redis内存回收机制
3.2.1 过期键管理
数据结构设计:
cpp
// Redis数据库结构
typedef struct redisDb {
dict *dict; // 键空间字典
dict *expires; // 过期字典 key→过期时间戳
// ... 其他字段
} redisDb;
过期键删除策略:
- 惰性删除(Lazy Expiration)
cpp
// 查找key时的检查
robj *lookupKey(redisDb *db, robj *key, int flags) {
dictEntry *de = dictFind(db->dict, key->ptr);
if (de) {
robj *val = dictGetVal(de);
// 检查是否过期
if (server.maxmemory_policy != MAXMEMORY_NO_EVICTION &&
expireIfNeeded(db, key)) {
return NULL; // 已过期,返回空
}
// ... 更新访问时间等
return val;
}
return NULL;
}
- 定期删除(Active Expiration)
cpp
// 定时任务执行删除
void activeExpireCycle(int type) {
// 两种模式:FAST和SLOW
if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
// 快速模式:最多执行1ms
timelimit = 1000; // 微秒
} else {
// 慢速模式:最多执行25ms
timelimit = 25000;
}
// 抽样删除逻辑
for (每个数据库) {
for (抽样20个key) {
if (key过期) {
删除key;
expired++;
}
if (达到时间限制) break;
if (过期比例 < 25%) break; // 优化
}
}
}
3.2.2 内存淘汰策略
淘汰策略分类:
cpp
// 8种淘汰策略
#define MAXMEMORY_VOLATILE_LRU 0 // 从设置了TTL的key中使用LRU
#define MAXMEMORY_VOLATILE_LFU 1 // 从设置了TTL的key中使用LFU
#define MAXMEMORY_VOLATILE_TTL 2 // 从设置了TTL的key中淘汰TTL最小的
#define MAXMEMORY_VOLATILE_RANDOM 3 // 从设置了TTL的key中随机淘汰
#define MAXMEMORY_ALLKEYS_LRU 4 // 从所有key中使用LRU
#define MAXMEMORY_ALLKEYS_LFU 5 // 从所有key中使用LFU
#define MAXMEMORY_ALLKEYS_RANDOM 6 // 从所有key中随机淘汰
#define MAXMEMORY_NO_EVICTION 7 // 不淘汰,写操作返回错误
LRU算法实现:
cpp
// Redis LRU实现(24位存储)
unsigned lru:LRU_BITS; // 24位存储时间戳
// 访问时更新
#define LRU_CLOCK() ((server.unixtime/MAXMEMORY_LRU_CLOCK_RESOLUTION) & 255)
#define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) // 最大值
// 计算空闲时间
unsigned long long estimateObjectIdleTime(robj *o) {
unsigned long long lruclock = LRU_CLOCK();
if (lruclock >= o->lru) {
return (lruclock - o->lru) * LRU_CLOCK_RESOLUTION;
} else {
// 处理时钟回绕
return (lruclock + (LRU_CLOCK_MAX - o->lru)) * LRU_CLOCK_RESOLUTION;
}
}
LFU算法实现:
cpp
// LFU实现:24位中,高16位为分钟级时间,低8位为计数器
#define LFU_INIT_VAL 5 // 初始计数器值
// 计数器增长逻辑
uint8_t LFULogIncr(uint8_t counter) {
if (counter == 255) return 255;
double r = (double)rand()/RAND_MAX;
double baseval = counter - LFU_INIT_VAL;
if (baseval < 0) baseval = 0;
double p = 1.0/(baseval*server.lfu_log_factor+1);
if (r < p) counter++;
return counter;
}
// 计数器衰减
unsigned long LFUDecrAndReturn(robj *o) {
unsigned long ldt = o->lru >> 8; // 上次递减时间
unsigned long counter = o->lru & 255; // 计数器
unsigned long num_periods = server.lfu_decay_time ?
LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
if (num_periods)
counter = (num_periods > counter) ? 0 : counter - num_periods;
return counter;
}
3.2.3 内存淘汰执行流程
cpp
// 内存淘汰触发逻辑
int processCommand(client *c) {
// ... 命令处理逻辑
// 检查内存使用
if (server.maxmemory) {
int retval = freeMemoryIfNeeded();
if (retval == C_ERR) {
addReply(c, shared.oomerr); // 内存不足错误
return C_OK;
}
}
// ... 执行命令
}
int freeMemoryIfNeeded(void) {
size_t mem_reported, mem_tofree;
mem_reported = zmalloc_used_memory();
if (mem_reported <= server.maxmemory) return C_OK;
mem_tofree = mem_reported - server.maxmemory;
// 根据策略执行淘汰
switch (server.maxmemory_policy) {
case MAXMEMORY_VOLATILE_LRU:
case MAXMEMORY_ALLKEYS_LRU:
evictionPoolPopulate(LRU);
break;
// ... 其他策略
}
// 从淘汰池中删除key
for (淘汰池中的key) {
if (mem_tofree <= 0) break;
deleteKey(key);
mem_freed = 估算释放内存;
mem_tofree -= mem_freed;
}
return C_OK;
}
四、Redis原理总结与性能优化
4.1 数据结构选择策略
| 数据类型 | 小数据量编码 | 大数据量编码 | 转换阈值 |
|---|---|---|---|
| String | INT/EMBSTR | RAW | 44字节 |
| List | QuickList | QuickList | 固定 |
| Set | IntSet | HashTable | set-max-intset-entries |
| ZSet | ZipList | SkipList+Dict | zset-max-ziplist-entries |
| Hash | ZipList | HashTable | hash-max-ziplist-entries |
4.2 内存优化建议
-
合理设置过期时间:避免永久key过多
-
使用适当的数据结构:根据场景选择最优结构
-
控制单个key大小:避免BigKey问题
-
启用内存淘汰策略:根据访问模式选择合适的策略
-
监控内存碎片:定期检查mem_fragmentation_ratio
4.3 性能调优要点
-
网络模型优化:
-
根据连接数调整TCP参数
-
合理配置epoll参数
-
考虑使用Redis 6.0+的IO多线程
-
-
持久化配置:
-
根据数据重要性选择RDB/AOF
-
调整保存频率和重写条件
-
使用无盘复制减少IO压力
-
-
内存管理:
-
设置合理的maxmemory
-
根据业务特点选择淘汰策略
-
监控内存使用趋势
-
4.4 监控与诊断
4.4.1 关键监控指标
bash
# 内存相关
INFO MEMORY
# 查看used_memory、mem_fragmentation_ratio等
# 淘汰策略效果
INFO STATS
# 查看evicted_keys、keyspace_hits/misses
# 慢查询
SLOWLOG GET 10
# 客户端连接
CLIENT LIST
4.4.2 性能问题诊断流程
bash
1. 检查内存使用:是否接近maxmemory
2. 检查网络延迟:客户端到Redis的RTT
3. 分析慢查询:哪些命令执行慢
4. 检查持久化:AOF重写是否频繁
5. 监控CPU使用:是否出现单核瓶颈
6. 查看连接数:是否达到maxclients限制
五、Redis原理学习路线建议
5.1 学习阶段划分
第一阶段:基础掌握
-
理解SDS、Dict、ZipList等基础结构
-
掌握五种数据类型的编码方式
-
了解Redis单线程模型
第二阶段:深入原理
-
研究SkipList、QuickList实现细节
-
理解Rehash、内存淘汰机制
-
掌握IO多路复用原理
第三阶段:源码分析
-
阅读关键数据结构源码
-
分析事件循环实现
-
理解命令执行流程
5.2 实践建议
-
动手实现:尝试用其他语言实现SDS、SkipList等
-
源码阅读:从简单模块开始,如SDS、ae事件循环
-
性能测试:对不同数据结构进行压力测试
-
问题排查:模拟内存溢出、慢查询等场景