第八章:Redis底层原理深度详细解析

目录

概述

一、Redis数据结构原理

[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)实现机制)

二、Redis网络模型原理

[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))

Select模型

Poll模型

Epoll模型

[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 事件处理流程)

三、Redis通信协议与内存管理

[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 内存淘汰执行流程)

四、Redis原理总结与性能优化

[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 性能问题诊断流程)

五、Redis原理学习路线建议

[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语言原生的字符串存在以下缺陷:

  1. 长度获取需遍历strlen()需要O(n)时间

  2. 非二进制安全:不能存储包含'\0'的数据

  3. 缓冲区溢出风险:需手动管理内存

  4. 频繁内存分配:每次修改都需要重新分配

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核心优势
  1. O(1)获取长度:直接读取len字段

  2. 二进制安全:使用len判断结束,而非'\0'

  3. 杜绝缓冲区溢出:修改前检查空间

  4. 内存预分配策略

    cpp 复制代码
    // 内存分配策略
    if (新长度 < 1MB) {
        新空间 = 2 * 新长度 + 1
    } else {
        新空间 = 新长度 + 1MB + 1
    }
  5. 惰性空间释放:缩短时不立即回收,留作备用

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特性总结
  1. 有序存储:元素按升序排列

  2. 唯一性保证:自动去重

  3. 内存优化:自适应整数大小

  4. 二分查找:支持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 设计动机
  1. ZipList问题:大内存申请困难,连续内存要求高

  2. LinkedList问题:内存碎片多,指针开销大

  3. 折中方案: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缺点:

  1. FD数量限制(1024)

  2. 每次需要拷贝整个fd_set

  3. 线性扫描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)     (单线程)        (单线程)

单线程优势分析:

  1. 避免锁竞争:无锁数据结构,减少上下文切换

  2. 避免死锁问题:无需考虑复杂的同步机制

  3. 降低复杂度:代码更简单,易于维护

  4. 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;

过期键删除策略:

  1. 惰性删除(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;
}
  1. 定期删除(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 内存优化建议

  1. 合理设置过期时间:避免永久key过多

  2. 使用适当的数据结构:根据场景选择最优结构

  3. 控制单个key大小:避免BigKey问题

  4. 启用内存淘汰策略:根据访问模式选择合适的策略

  5. 监控内存碎片:定期检查mem_fragmentation_ratio

4.3 性能调优要点

  1. 网络模型优化

    • 根据连接数调整TCP参数

    • 合理配置epoll参数

    • 考虑使用Redis 6.0+的IO多线程

  2. 持久化配置

    • 根据数据重要性选择RDB/AOF

    • 调整保存频率和重写条件

    • 使用无盘复制减少IO压力

  3. 内存管理

    • 设置合理的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 实践建议

  1. 动手实现:尝试用其他语言实现SDS、SkipList等

  2. 源码阅读:从简单模块开始,如SDS、ae事件循环

  3. 性能测试:对不同数据结构进行压力测试

  4. 问题排查:模拟内存溢出、慢查询等场景

相关推荐
zzb15804 小时前
RAG from Scratch-优化-query
java·数据库·人工智能·后端·spring·mybatis
一只鹿鹿鹿4 小时前
信息安全等级保护安全建设防护解决方案(总体资料)
运维·开发语言·数据库·面试·职场和发展
堕2744 小时前
MySQL数据库《基础篇--数据库索引(2)》
数据库·mysql
wei_shuo4 小时前
数据库优化器进化论:金仓如何用智能下推把查询时间从秒级打到毫秒级
数据库·kingbase·金仓
雷工笔记4 小时前
Navicat Premium 17 软件安装记录
数据库
wenlonglanying5 小时前
Ubuntu 系统下安装 Nginx
数据库·nginx·ubuntu
数据库小组5 小时前
10 分钟搞定!Docker 一键部署 NineData 社区版
数据库·docker·容器·database·数据库管理工具·ninedata·迁移工具
爬山算法5 小时前
MongoDB(38)如何使用聚合进行投影?
数据库·mongodb
l1t5 小时前
Deep Seek总结的APSW 和 SQLite 的关系
数据库·sqlite
Pocker_Spades_A6 小时前
基于代价模型的连接条件下推:复杂SQL查询的性能优化实践
数据库·sql·性能优化