第八章: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. 问题排查:模拟内存溢出、慢查询等场景

相关推荐
电商API&Tina2 小时前
电商数据采集 API 接口 全维度解析(技术 + 商业 + 合规)
java·大数据·开发语言·数据库·人工智能·json
liwulin05062 小时前
【JSON】使用com.fasterxml.jackson解析json字符串
java·数据库·json
志凌海纳SmartX3 小时前
金融行业IT基础设施转型实践|450+机构部署轻量云,支持核心生产与信创业务
大数据·数据库·金融
VIP_CQCRE3 小时前
hCaptcha 验证码图像识别 API 对接教程
数据库
Mr_Xuhhh3 小时前
MySQL索引深度解析:从原理到实践
数据库·sql·mysql
爱学习的阿磊3 小时前
Python入门:从零到一的第一个程序
jvm·数据库·python
naruto_lnq3 小时前
编写一个Python脚本自动下载壁纸
jvm·数据库·python
AllData公司负责人3 小时前
【亲测好用】实时开发平台能力演示
java·c语言·数据库
fengxin_rou3 小时前
Redis从零到精通第二篇:redis常见的命令
数据库·redis·缓存