一、Redis 核心数据结构与实现原理
- 字符串(String)
- 原理:基于简单动态字符串(SDS),支持预分配空间和惰性释放,避免频繁内存分配。
- 应用 :缓存、计数器(
INCR
)、分布式锁(SETNX
)。
** SDS(Simple Dynamic String,简单动态字符串)**
定义
Redis 自定义的字符串数据结构,用于替代 C 原生字符串,是 Redis 最基础的数据类型(String
类型的底层实现)。
核心原理
-
结构设计
SDS 的结构包含三个关键字段:
cstruct sdshdr { int len; // 已用长度(不包含结尾'\0') int free; // 剩余可用空间 char buf[]; // 实际存储的字符数组(兼容C字符串) };
-
核心优势
- O(1) 复杂度获取长度 :通过
len
字段直接获取字符串长度,无需遍历(C 字符串需strlen()
遍历)。 - 杜绝缓冲区溢出:修改前自动检查空间,不足时扩容(C 字符串需手动管理)。
- 减少内存重分配:采用预分配(扩容时多分配冗余空间)和惰性释放(缩短时不立即缩容)。
- 二进制安全 :允许存储包含
'\0'
的数据(如图片、音频),C 字符串以'\0'
为结尾标识,无法处理这类数据。
- O(1) 复杂度获取长度 :通过
- 哈希(Hash)
- 原理 :底层使用
ziplist
(小数据)或hashtable
(大数据),ziplist
通过连续内存压缩存储键值对。 - 应用:存储对象(如用户信息)。
- 原理 :底层使用
Redis 中 ziplist
和 hashtable
是两种不同的底层数据结构,用于实现 Hash 类型 和 Sorted Set 类型 。它们的核心区别在于 内存效率 和 操作性能 的权衡,以下是详细对比:
一、ziplist(压缩列表)
1. 实现原理
-
结构设计 :
ziplist
是一块 连续内存 的线性结构,由多个entry
紧凑排列组成,每个entry
存储键或值。整体布局:
plaintext| zlbytes | zltail | zllen | entry1 | entry2 | ... | entryN | zlend |
- zlbytes:总字节数
- zltail:最后一个 entry 的偏移量(支持反向遍历)
- zllen:entry 数量
- entry:动态编码的键值对
- zlend:结束标记(0xFF)
-
Entry 结构 :
每个
entry
包含以下信息:plaintext| prevlen | encoding | content |
- prevlen:前一个 entry 的长度(变长编码,用于反向遍历)
- encoding:内容的数据类型(如字符串、整数)和长度
- content:实际数据
2. 特点
- 内存紧凑:无指针开销,适合存储小数据。
- 顺序访问:查找需遍历,时间复杂度 O(N)。
- 插入/删除代价高:需重新分配内存并移动后续数据。
- 自动类型转换 :当元素数量或值大小超过阈值(如
hash-max-ziplist-entries
)时,转换为hashtable
。
3. 适用场景
- Hash 类型:键值对数量少且值较小时。
- Sorted Set 类型:元素数量少且分值范围小时。
二、hashtable(哈希表)
1. 实现原理
-
结构设计 :
Redis 的
hashtable
基于 字典(dict) 实现,核心结构为:ctypedef struct dict { dictType *type; // 类型特定函数(如哈希函数) dictht ht[2]; // 两个哈希表(用于渐进式 rehash) long rehashidx; // rehash 进度标记(-1 表示未进行) } dict; typedef struct dictht { dictEntry **table; // 哈希表数组(桶) unsigned long size; // 哈希表大小 unsigned long sizemask; // 掩码(计算索引值) unsigned long used; // 已使用节点数 } dictht; typedef struct dictEntry { void *key; // 键 union { // 值(可能是指针、整数等) void *val; uint64_t u64; int64_t s64; } v; struct dictEntry *next; // 链地址法解决哈希冲突 } dictEntry;
-
哈希冲突 :通过 链地址法(链表)解决冲突。
-
渐进式 Rehash :
当哈希表负载因子过高时,Redis 会逐步将数据从
ht[0]
迁移到ht[1]
,避免一次性迁移导致的阻塞。
2. 特点
- 快速查找:平均时间复杂度 O(1)。
- 内存开销大:需存储指针、预分配桶空间等。
- 支持动态扩容 :根据负载因子(
used/size
)触发扩容或缩容。
3. 适用场景
- Hash 类型:键值对数量多或值较大时。
- 所有键的存储(全局哈希表)。
三、核心区别
特性 | ziplist | hashtable |
---|---|---|
内存占用 | 极低(连续存储,无指针) | 较高(指针、预分配桶空间) |
查找性能 | O(N)(需遍历) | O(1)(哈希计算直接定位) |
插入/删除性能 | O(N)(需移动数据) | O(1)(链地址法快速操作) |
数据规模限制 | 小数据(受 hash-max-ziplist-* 配置限制) |
大数据(支持动态扩容) |
实现复杂度 | 高(需处理变长编码、内存重分配) | 低(标准哈希表实现) |
四、转换机制
- 触发条件 (以 Hash 类型为例):
- 当 Hash 的键值对数量超过
hash-max-ziplist-entries
(默认 512)时。 - 当任意值的长度超过
hash-max-ziplist-value
(默认 64 字节)时。
- 当 Hash 的键值对数量超过
- 转换过程 :
- 删除原有的
ziplist
。 - 创建新的
hashtable
。 - 将所有键值对插入
hashtable
。
- 删除原有的
五、选择建议
- 优先使用 ziplist:当数据量小且追求内存效率时。
- 切换为 hashtable:当数据量大或需要高频读写时。
- 监控配置 :根据业务场景调整
hash-max-ziplist-*
参数,平衡内存和性能。
通过理解二者的底层实现,可以更好地优化 Redis 的内存使用和性能表现。
- 列表(List)
- 原理 :早期用
ziplist
,现多用quicklist
(由多个ziplist
组成的双向链表),平衡内存和性能。 - 应用:消息队列、最新消息列表。
- 原理 :早期用
Redis 的 quicklist
是 列表(List) 数据类型的底层实现,结合了 ziplist
和 双向链表 的优势,在内存效率和操作性能之间取得平衡。其设计核心是解决纯 ziplist
或纯链表在特定场景下的缺陷。
一、设计背景
-
ziplist 的局限性
- 内存连续:插入/删除需移动数据,时间复杂度 O(N)。
- 连锁更新 :
prevlen
字段变长编码可能导致多个节点重新分配内存(极端场景)。 - 大容量瓶颈 :单一
ziplist
存储大量数据时,操作效率急剧下降。
-
链表的缺陷
- 内存碎片:节点分散存储,内存利用率低。
- 指针开销:每个节点需存储前后指针(64 位系统占 16 字节)。
二、quicklist 实现原理
1. 整体结构
quicklist
是一个由多个 quicklistNode
节点 组成的 双向链表 ,每个节点内部包含一个 ziplist
:
c
typedef struct quicklist {
quicklistNode *head; // 头节点
quicklistNode *tail; // 尾节点
unsigned long count; // 所有 ziplist 中的总元素数
unsigned long len; // quicklistNode 节点数
int fill : 16; // ziplist 大小限制(由 list-max-ziplist-size 配置)
unsigned int compress : 16; // 压缩深度(由 list-compress-depth 配置)
} quicklist;
typedef struct quicklistNode {
struct quicklistNode *prev; // 前驱节点
struct quicklistNode *next; // 后继节点
unsigned char *zl; // 指向 ziplist 的指针
unsigned int sz; // ziplist 的字节大小
unsigned int count : 16; // ziplist 中的元素个数
unsigned int encoding : 2; // 编码格式(RAW=1, LZF=2)
unsigned int container : 2; // 容器类型(NONE=1, ZIPLIST=2)
unsigned int recompress : 1; // 是否被压缩过(临时状态)
} quicklistNode;
2. 核心机制
-
分片存储
将长列表切分为多个短
ziplist
(称为quicklistNode
),避免单一ziplist
过大导致的性能问题。- 分片大小 :由
list-max-ziplist-size
配置(如-2
表示每个ziplist
不超过 8 KB)。
- 分片大小 :由
-
压缩优化
- LZF 压缩算法 :对中间节点的
ziplist
进行压缩(由list-compress-depth
配置)。list-compress-depth 0
:不压缩。list-compress-depth 1
:压缩首尾第 1 个节点外的所有节点。list-compress-depth 2
:压缩首尾前 2 个节点外的所有节点(依此类推)。
- LZF 压缩算法 :对中间节点的
-
平衡插入/删除性能
- 头部/尾部操作 :直接操作首尾节点的
ziplist
,时间复杂度 O(1)。 - 中间插入/删除 :定位到具体
quicklistNode
后操作其ziplist
,若ziplist
超出大小限制,则分裂为两个节点。
- 头部/尾部操作 :直接操作首尾节点的
三、优势与性能分析
场景 | quicklist 表现 |
---|---|
内存利用率 | 高于纯链表(ziplist 紧凑存储),低于单一 ziplist (分片导致少量冗余)。 |
插入/删除头部/尾部 | O(1) 时间复杂度(直接操作首尾节点的 ziplist )。 |
随机访问 | O(n) 时间复杂度(需遍历节点),但实际性能优于纯链表(ziplist 局部连续访问)。 |
大范围操作 | 性能优于单一 ziplist (避免大规模内存拷贝)。 |
四、配置调优
-
list-max-ziplist-size
- 控制每个
ziplist
的最大容量(单位:字节或元素数)。 - 示例:
list-max-ziplist-size -2
:每个ziplist
不超过 8 KB(默认值)。list-max-ziplist-size 512
:每个ziplist
最多存储 512 个元素。
- 控制每个
-
list-compress-depth
- 控制压缩深度,权衡 CPU 与内存。
- 示例:
list-compress-depth 0
:关闭压缩(默认)。list-compress-depth 1
:压缩中间节点,保留首尾节点不压缩(适合频繁操作两端场景)。
五、源码级设计细节
-
避免连锁更新
quicklist
的ziplist
分片后,单个ziplist
长度有限,降低了连锁更新的概率。 -
压缩与解压策略
- 写时压缩 :数据修改时,对目标
quicklistNode
解压 -> 修改 -> 按需重新压缩。 - 读时解压:访问被压缩的节点时,自动解压后返回数据。
- 写时压缩 :数据修改时,对目标
六、应用场景
- 消息队列
- 高频
LPUSH/RPOP
操作时,直接操作首尾节点的ziplist
,性能接近 O(1)。
- 高频
- 最新消息列表
- 存储最近的 N 条消息,利用
LTRIM
快速截断旧数据。
- 存储最近的 N 条消息,利用
- 大列表存储
- 存储百万级元素的列表时,避免单一
ziplist
的内存重分配问题。
- 存储百万级元素的列表时,避免单一
通过分片、压缩和双向链表的结合,quicklist
在保证操作性能的同时,显著提升了内存利用率,是 Redis 列表类型的理想底层实现。
-
集合(Set)
- 原理 :基于
intset
(整数集合)或hashtable
,自动根据元素类型切换。 - 应用 :标签系统、共同关注(
SINTER
)。
- 原理 :基于
-
有序集合(Sorted Set)
- 原理 :
ziplist
(小数据)或跳跃表(SkipList) + 字典
,跳跃表支持快速范围查询,字典用于 O(1) 查找分数。 - 应用:排行榜、延迟队列。
- 原理 :
-
其他高级类型
- 位图(Bitmap) :基于字符串的位操作,如统计活跃用户(
BITCOUNT
)。 - HyperLogLog:概率算法,固定 12KB 内存估算上亿唯一值(误差 0.81%)。
- GEO:基于有序集合的 GeoHash 编码,存储地理位置。
- 位图(Bitmap) :基于字符串的位操作,如统计活跃用户(
二、持久化机制
-
RDB(快照)
- 原理 :
fork
子进程生成数据快照(二进制文件),主进程继续处理请求。 - 优点:恢复快,适合备份。
- 缺点:可能丢失最后一次快照后的数据。
- 原理 :
-
AOF(追加日志)
- 原理 :记录所有写命令(文本格式),支持
fsync
策略(always
/everysec
/no
)。 - 重写机制 :通过
BGREWRITEAOF
生成紧凑的新 AOF 文件,避免日志膨胀。
- 原理 :记录所有写命令(文本格式),支持
AOF(Append-Only File)持久化 fsync
策略 决定了这些命令何时从操作系统缓冲区同步到磁盘,直接影响数据安全性和性能。:
一、fsync
策略的三种模式
通过 appendfsync
配置项设置,可选值为 always
、everysec
、no
:
1. appendfsync always
(最安全,性能最低)
- 原理 :每次写入 AOF 缓冲后,立即调用
fsync()
强制将数据刷到磁盘。 - 数据安全性:理论上最多丢失一个命令的数据(除非磁盘或内核缓冲区故障)。
- 性能影响:频繁的磁盘 I/O 操作(尤其是机械硬盘),吞吐量显著下降。
2. appendfsync everysec
(默认配置,平衡选择)
- 原理 :
- 主线程将命令写入 AOF 缓冲区。
- 后台线程每隔 1 秒 调用
fsync()
将缓冲区数据刷到磁盘。
- 数据安全性 :最多丢失 1 秒内的写操作(若故障发生在两次
fsync
之间)。 - 性能影响:吞吐量接近无持久化模式,适合大多数场景。
3. appendfsync no
(最不安全,性能最高)
- 原理 :数据写入 AOF 缓冲区后,由操作系统决定何时刷盘(通常依赖内核的
flush
机制)。 - 数据安全性:可能丢失大量数据(取决于操作系统刷盘策略,通常最长 30 秒)。
- 性能影响 :无
fsync
开销,吞吐量最高,但风险极大。
二、底层机制解析
- 写入流程
- 客户端写命令 → Redis 服务端 → AOF 缓冲区 → 根据策略调用
fsync()
→ 磁盘。
- 客户端写命令 → Redis 服务端 → AOF 缓冲区 → 根据策略调用
fsync()
的作用- 强制将内核缓冲区(Page Cache)中的数据写入磁盘,确保持久化。
- 相比
write()
仅写入内核缓冲区,fsync()
是数据安全的最后屏障。
三、策略对比与选型建议
策略 | 数据安全性 | 吞吐量 | 适用场景 |
---|---|---|---|
always |
最高(近似零丢失) | 最低 | 金融、支付等对数据一致性要求极高的场景。 |
everysec |
较高(秒级丢失) | 中等 | 通用场景(默认推荐)。 |
no |
最低(依赖操作系统) | 最高 | 允许数据丢失的缓存场景或测试环境。 |
四、配置示例
-
配置文件(redis.conf)
bashappendonly yes # 启用 AOF appendfilename "appendonly.aof" appendfsync everysec # 设置 fsync 策略
-
运行时动态修改
bashCONFIG SET appendfsync always
五、优化注意事项
- 结合混合持久化 (Redis 4.0+)
- 启用
aof-use-rdb-preamble yes
,AOF 文件包含 RDB 格式的全量数据 + AOF 增量命令,提升重启恢复速度。
- 启用
- AOF 重写
- 定期执行
BGREWRITEAOF
压缩 AOF 文件,避免文件过大。
- 定期执行
- 磁盘性能
- 使用 SSD 硬盘可显著降低
fsync
的性能损耗。
- 使用 SSD 硬盘可显著降低
六、故障恢复逻辑
- Redis 重启时,会优先加载 AOF 文件(因其数据更完整),通过 重放所有命令 恢复内存数据。
- 若 AOF 文件损坏,可用
redis-check-aof --fix
工具修复。
通过合理选择 fsync
策略,可以在数据安全性和性能之间找到最佳平衡点。默认的 everysec
是大多数场景的理想选择,而极端场景可按需调整。
- 混合持久化(Redis 4.0+)
- 原理:RDB 全量 + AOF 增量,重启时先加载 RDB,再重放 AOF 增量命令。
- 优势:兼顾恢复速度和数据安全性。
BGREWRITEAOF
是一个异步命令,用于 重写 AOF 文件(Append-Only File),目的是压缩 AOF 文件体积、优化持久化性能,同时确保数据完整性:
一、为什么需要 BGREWRITEAOF?
-
AOF 文件膨胀问题
- AOF 文件记录所有写操作命令(如多次
SET key
),随着时间推移会变得臃肿。 - 例如:对同一个
key
修改 100 次,AOF 会记录 100 条命令,但实际只需保留最终状态。
- AOF 文件记录所有写操作命令(如多次
-
恢复效率低下
- 过大的 AOF 文件在 Redis 重启时重放速度慢,影响服务可用性。
二、BGREWRITEAOF 的作用
-
生成紧凑的新 AOF 文件
- 基于当前内存数据快照,生成一个仅包含 重建数据集所需最小命令集 的新 AOF 文件。
- 示例 :
旧 AOF 文件:包含SET key 1
、SET key 2
、SET key 3
新 AOF 文件:仅保留SET key 3
(直接记录最终值)。
-
解决文件冗余
- 删除无效命令(如已过期键的写入操作)。
- 合并多条命令(如多个
SADD
合并为一条)。
三、实现原理
-
触发方式
- 自动触发 :根据配置
auto-aof-rewrite-percentage
和auto-aof-rewrite-min-size
(如 AOF 文件大小增长超过 100% 且大于 64MB)。 - 手动触发 :执行
BGREWRITEAOF
命令。
- 自动触发 :根据配置
-
执行流程
- 步骤 1 :
fork
子进程(利用 Copy-On-Write 机制),避免阻塞主进程。 - 步骤 2:子进程遍历内存数据,生成新 AOF 文件的临时内容(格式兼容 AOF)。
- 步骤 3:新文件生成后,替换旧文件(原子操作)。
- 步骤 4 :主进程将重写期间的新写入命令追加到新 AOF 文件(通过 AOF 重写缓冲区 保证数据一致性)。
- 步骤 1 :
四、关键特性
特性 | 说明 |
---|---|
异步非阻塞 | 子进程执行重写,主进程继续处理请求(仅 fork 阶段短暂阻塞)。 |
数据安全 | 重写期间的新命令会写入 AOF 缓冲区和重写缓冲区,确保无数据丢失。 |
兼容性 | 新 AOF 文件格式与旧版本兼容,支持 Redis 版本升级。 |
资源消耗 | fork 子进程可能消耗较多内存(尤其在数据集较大时)。 |
五、配置调优
-
自动重写阈值
bashauto-aof-rewrite-percentage 100 # 当前 AOF 文件大小比上次重写后增长 100% auto-aof-rewrite-min-size 64mb # AOF 文件最小重写体积
-
混合持久化(Redis 4.0+)
启用
aof-use-rdb-preamble yes
,新 AOF 文件头部为 RDB 格式(全量数据),后续为增量 AOF 命令,兼顾恢复速度和数据安全。
六、注意事项
- 磁盘空间
- 确保磁盘剩余空间足够(需容纳旧 AOF 文件 + 新 AOF 文件)。
- 性能影响
- 避免在写入高峰触发重写(可能因
fork
延迟导致短暂阻塞)。
- 避免在写入高峰触发重写(可能因
- 监控命令
- 使用
INFO Persistence
查看aof_rewrite_in_progress
和aof_last_rewrite_time_sec
监控重写状态。
- 使用
七、与 BGSAVE 的优先级
- 若同时执行
BGSAVE
(生成 RDB)和BGREWRITEAOF
:- Redis 2.6 及以前 :
BGREWRITEAOF
会延迟到BGSAVE
完成后执行。 - Redis 2.8+:允许两者同时运行(但实际执行仍有资源竞争)。
- Redis 2.6 及以前 :
三、内存管理与淘汰策略
-
内存分配
- jemalloc:默认内存分配器,减少内存碎片。
- 对象共享 :如
0~9999
的整数对象复用。
-
淘汰策略(8种)
- LRU/LFU:近似算法(随机采样 + 淘汰最久/最少使用)。
- TTL:淘汰过期时间最近的数据。
- 配置示例 :
maxmemory-policy volatile-lru
。
8 种淘汰策略及其实现原理:
一、淘汰策略分类
Redis 的淘汰策略通过 maxmemory-policy
配置,分为两类:
- 针对所有键 (
allkeys-*
):无论是否设置过期时间,都可能被淘汰。 - 仅针对过期键 (
volatile-*
):仅淘汰设置了过期时间的键。
二、8 种淘汰策略详解
策略 | 原理 | 适用场景 |
---|---|---|
noeviction |
不淘汰数据,直接拒绝所有写操作(返回错误)。 | 数据绝对不可丢失,宁愿牺牲写入。 |
allkeys-lru |
从所有键中淘汰 最近最少使用(LRU) 的键。 (近似算法,基于采样和空闲时间判断) | 通用缓存场景。 |
volatile-lru |
从设置了过期时间的键中淘汰 LRU 的键。 | 需区分持久数据和缓存数据。 |
allkeys-lfu |
从所有键中淘汰 最不频繁使用(LFU) 的键。 (基于访问频率和衰减机制) | 高频访问热点数据场景。 |
volatile-lfu |
从设置了过期时间的键中淘汰 LFU 的键。 | 需区分持久数据与低频缓存数据。 |
allkeys-random |
从所有键中随机淘汰键。 | 无明确访问模式,均匀分布场景。 |
volatile-random |
从设置了过期时间的键中随机淘汰键。 | 缓存数据随机淘汰。 |
volatile-ttl |
优先淘汰 剩余生存时间(TTL)最短 的键。 | 短期缓存数据(如验证码)。 |
三、核心实现原理
1. LRU(Least Recently Used)
- 近似算法 :Redis 采用 采样 + 空闲时间 估算 LRU。
- 每个键记录一个 24 位的
lru
字段(存储 Unix 时间戳的低 24 位)。 - 淘汰时随机采样
maxmemory-samples
个键(默认 5),淘汰空闲时间(当前时间 - lru)最大的键。
- 每个键记录一个 24 位的
- 优点:内存开销小(每个键仅需 24 位),适合大规模数据。
- 缺点:精度较低(采样可能遗漏真实 LRU 键)。
2. LFU(Least Frequently Used)
- 频率统计 :每个键记录一个 8 位的
lfu
字段(存储访问频率)。- 低频衰减 :每隔一段时间(由
lfu-decay-time
配置,默认 1 分钟)衰减计数器,避免旧访问记录长期影响。 - 计数器增长:访问时根据公式增加计数器(概率性避免快速饱和)。
- 低频衰减 :每隔一段时间(由
- 淘汰逻辑:淘汰频率最低的键(若频率相同,则淘汰 LRU 较小的键)。
3. TTL(Time To Live)
- 直接比较 :淘汰剩余生存时间(
TTL
)最小的键。 - 限制:仅对设置了过期时间的键生效。
4. Random(随机淘汰)
- 无模式依赖:随机选择键淘汰,无计算开销。
- 适用性:适合数据访问模式完全随机的场景。
四、配置示例
bash
# redis.conf
maxmemory 4gb # 最大内存限制
maxmemory-policy allkeys-lru # 使用所有键的 LRU 淘汰策略
maxmemory-samples 10 # 提高 LRU/LFU 采样精度(默认 5)
五、选型建议
场景 | 推荐策略 | 理由 |
---|---|---|
缓存系统 | allkeys-lru |
优先保留最近访问的热点数据。 |
高频热点数据 | allkeys-lfu |
精确淘汰低频访问数据,保留高频热点。 |
短期临时数据 | volatile-ttl |
自动清理最早过期的数据(如会话 Token)。 |
数据不可丢失 | noeviction |
确保写入失败而非数据丢失(需配合监控和扩容)。 |
无明确访问模式 | allkeys-random |
均匀淘汰,避免局部热点影响。 |
六、性能与监控
-
监控命令
bashINFO memory # 查看内存使用及淘汰次数(evicted_keys)
-
调优方向
- 增加
maxmemory-samples
提高 LRU/LFU 精度(代价:CPU 开销增加)。 - 结合业务特性选择策略(如电商促销期间切为
allkeys-lfu
)。
- 增加
通过合理配置淘汰策略,可以在内存限制下最大化缓存命中率,平衡性能与数据保留需求。
- 内存碎片整理
- 原理 :通过
MEMORY PURGE
或配置activedefrag
自动整理碎片。
- 原理 :通过
四、高可用与集群
-
主从复制
- 全量同步 :从节点发送
PSYNC
,主节点生成 RDB 发送。 - 增量同步 :基于复制积压缓冲区(
repl_backlog
)发送命令流。
- 全量同步 :从节点发送
-
哨兵(Sentinel)
- 原理:监控主节点,通过 Raft 协议选举 Leader 哨兵,执行故障转移。
- 脑裂问题 :通过
min-slaves-to-write
避免数据不一致。
Redis 的 min-slaves-to-write
配置项(Redis 5.0+ 中更名为 min-replicas-to-write
)用于解决 脑裂(Split-Brain)问题,其核心原理是通过限制主节点在特定条件下拒绝写入请求,确保数据一致性和安全性:
一、脑裂问题的背景
1. 什么是脑裂?
在网络分区(Network Partition)场景中,原主节点和从节点被隔离成两个独立集群:
- 旧主节点:因网络中断无法与哨兵(Sentinel)或其他从节点通信,但可能仍在接受客户端写入。
- 新主节点:哨兵选举出的新主节点,开始接受写入。
最终导致 两个主节点同时存在,客户端可能向两者写入数据,引发数据不一致。
2. 脑裂的危害
- 数据丢失:网络恢复后,旧主节点会作为从节点同步新主数据,其期间写入的数据将被覆盖。
- 数据冲突:若客户端同时写入新旧主节点,数据逻辑可能混乱。
二、min-slaves-to-write
的作用
1. 配置项定义
bash
min-slaves-to-write <num> # 主节点必须至少有 <num> 个从节点连接
min-slaves-max-lag <seconds> # 从节点复制延迟不超过 <seconds> 秒
- 触发条件 :当主节点连接的 健康从节点数量 <
min-slaves-to-write
或 从节点复制延迟 >min-slaves-max-lag
时。 - 主节点行为 :拒绝所有写入请求(返回错误),变为只读模式。
2. 如何解决脑裂?
-
场景模拟:
- 主节点(M)有 3 个从节点(S1, S2, S3),配置
min-slaves-to-write=2
。 - 网络分区导致 M 只连接 S1,S2 和 S3 被隔离。
- 此时 M 的健康从节点数量(1) < 配置值(2),触发写入拒绝。
- 客户端无法向 M 写入数据,防止数据不一致。
- 哨兵检测到 M 失联,选举 S2 或 S3 为新主节点。
- 新主节点满足
min-slaves-to-write
后恢复写入。
- 主节点(M)有 3 个从节点(S1, S2, S3),配置
-
结果:
- 旧主节点(M)因无法满足从节点数量要求,停止写入。
- 只有新主节点能正常写入,确保数据一致性。
三、底层实现原理
1. 健康从节点判定
- 连接状态:从节点必须与主节点保持连接。
- 复制延迟 :从节点的
repl_offset
与主节点的master_repl_offset
差值需 ≤min-slaves-max-lag
。
2. 写入拒绝逻辑
-
主节点在每次处理写命令前,检查以下条件:
cif (healthy_slaves_count < min_slaves_to_write || any_slave_lag > min_slaves_max_lag) { reject_write_request(); // 返回 -NOREPLICAS 错误 }
-
客户端响应 :返回错误
-NOREPLICAS Not enough good replicas to write.
3. 与哨兵故障转移的协同
- 当主节点因
min-slaves-to-write
拒绝写入时,哨兵会更快检测到异常(通过INFO
命令获取主节点状态),触发故障转移流程。
四、配置建议
1. 参数设置示例
bash
min-replicas-to-write 1 # 至少需 1 个健康从节点
min-replicas-max-lag 10 # 从节点复制延迟不超过 10 秒
- 平衡点 :
- 值越大,数据安全性越高,但写入可用性越低(需更多从节点在线)。
- 值过小可能无法有效防止脑裂。
2. 适用场景
- 高一致性场景:金融交易、订单系统等要求强一致性的业务。
- 多可用区部署:跨机房部署时,确保主节点在足够从节点存活时才能写入。
五、注意事项
- 从节点数量要求 :需部署足够从节点(至少 ≥
min-slaves-to-write
)。 - 监控与告警 :
- 监控主节点的
connected_slaves
和从节点延迟。 - 配置哨兵自动故障转移,避免人工介入延迟。
- 监控主节点的
- 客户端兼容性 :客户端需处理
-NOREPLICAS
错误(如重试、降级逻辑)。
六、对比其他方案
方案 | 优点 | 缺点 |
---|---|---|
min-slaves-to-write |
主动拒绝写入,数据强一致 | 可能降低写入可用性 |
Quorum 协议 | 分布式一致性保障 | 实现复杂,性能开销大 |
人工介入 | 灵活处理特殊场景 | 响应慢,易出错 |
通过合理配置 min-slaves-to-write
和 min-slaves-max-lag
,可以在网络分区场景下有效避免脑裂问题,是 Redis 高可用架构中保障数据一致性的重要机制。
- Cluster 集群
- 数据分片:16384 个槽(Slot),使用 CRC16 算法分片。
- Gossip 协议:节点间通过 PING/PONG 交换状态信息。
- 重定向机制 :客户端访问错误节点时,返回
MOVED
或ASK
指令。
Redis Cluster 是 通过 数据分片(Sharding) 和 主从复制 实现高可用与水平扩展:
一、数据分片与槽(Slot)分配
1. 槽的概念
-
Redis Cluster 将整个数据集划分为 16384 个槽(Slot),每个键通过算法映射到特定槽。
-
键到槽的映射:
pythonslot = CRC16(key) % 16384
- 使用 CRC16 算法计算键的哈希值,再对 16384 取模得到槽编号。
2. 槽的分配
- 集群启动时,槽会被分配到不同主节点(每个主节点负责一部分槽)。
- 动态调整 :支持通过
CLUSTER ADDSLOTS
或工具(如redis-cli --cluster reshard
)重新分配槽,实现数据迁移。
Redis 集群通过 槽(Slot)重分配 实现数据的动态迁移,无论是使用 CLUSTER ADDSLOTS
手动分配还是通过 redis-cli --cluster reshard
工具自动化迁移,其核心原理均基于 槽的标记、数据迁移和集群状态同步。以下是具体实现原理:
一、CLUSTER ADDSLOTS
手动分配槽
1. 适用场景
- 集群初始化:首次创建集群时分配槽到主节点。
- 小规模调整:手动调整少量槽的归属(不涉及数据迁移)。
2. 实现流程
-
步骤 1:向目标节点发送命令,声明其负责的槽。
bashCLUSTER ADDSLOTS <slot1> <slot2> ... <slotN>
-
步骤 2 :目标节点更新本地
clusterState.slots[]
数组,标记槽归属。 -
步骤 3 :目标节点通过 Gossip 协议(PING/PONG 消息)将槽分配信息广播给其他节点。
-
步骤 4:集群所有节点更新槽映射表,最终达成一致。
3. 注意事项
- 无数据迁移 :仅修改槽归属元数据,不迁移原有数据(需确保槽未被占用或已清空)。
- 风险:若槽已存在数据,直接分配会导致数据丢失或访问错误。
二、redis-cli --cluster reshard
自动化迁移
1. 适用场景
- 扩容/缩容:新增或移除节点时重新平衡槽分布。
- 负载均衡:调整槽分配以均匀分布数据。
2. 实现流程
-
步骤 1:计算迁移计划
工具根据目标节点数量、槽分布和权重,自动计算需迁移的槽及数量。
bashredis-cli --cluster reshard <host>:<port> --cluster-from <源节点ID> --cluster-to <目标节点ID> --cluster-slots <迁移槽数>
-
步骤 2:标记迁移状态
- 源节点标记槽为
MIGRATING
(迁移中)。 - 目标节点标记槽为
IMPORTING
(接收中)。
- 源节点标记槽为
-
步骤 3:迁移数据
-
源节点遍历待迁移槽的所有键,逐个执行
MIGRATE
命令:bashMIGRATE <目标节点IP> <目标节点端口> <key> 0 <timeout>
- 原子性迁移:键数据会被序列化传输到目标节点,传输成功后源节点删除该键。
-
迁移过程中,客户端请求可能触发
ASK
重定向(临时将请求转发到目标节点)。
-
-
步骤 4:更新槽归属
迁移完成后,目标节点通过
CLUSTER SETSLOT <slot> NODE <目标节点ID>
声明槽归属,并通过 Gossip 协议同步到全集群。 -
步骤 5:清理状态
移除
MIGRATING
和IMPORTING
标记,集群进入稳定状态。
三、关键技术原理
1. MIGRATE
命令的原子性
- 过程 :
- 源节点序列化键数据并发送到目标节点。
- 目标节点反序列化数据并存入内存。
- 目标节点返回确认后,源节点删除该键。
- 一致性保障:迁移过程中键在源和目标节点间短暂存在,但客户端通过重定向保证访问正确节点。
2. ASK 与 MOVED 重定向
MOVED
重定向:槽已永久迁移,客户端需更新槽映射缓存。ASK
重定向 :槽迁移中临时重定向,客户端需先发送ASKING
命令告知目标节点接受请求。
3. 集群状态同步
- Gossip 协议:节点间通过 PING/PONG 消息交换槽分配信息,最终一致。
- 配置纪元(Epoch):全局递增的版本号,解决网络分区导致的配置冲突。
四、示例:扩容时迁移槽
-
添加新主节点
bashredis-cli --cluster add-node 新节点IP:端口 集群任意节点IP:端口
-
执行重分片
bashredis-cli --cluster reshard 集群任意节点IP:端口
- 交互式输入:迁移槽数、目标节点 ID、源节点 ID(或输入
all
从所有节点抽取槽)。
- 交互式输入:迁移槽数、目标节点 ID、源节点 ID(或输入
-
验证结果
bashredis-cli --cluster check 集群任意节点IP:端口
五、注意事项
-
性能影响
- 迁移期间可能增加网络和 CPU 负载,建议低峰期操作。
- 大 Key 迁移可能导致阻塞(单线程模型)。
-
客户端兼容性
- 客户端需正确处理
MOVED
和ASK
重定向(如 Jedis、Lettuce 等 Smart Client 已支持)。
- 客户端需正确处理
-
数据一致性
- 迁移过程中对键的写操作可能丢失(需业务端重试或避免迁移期间写入)。
六、源码关键逻辑
-
槽状态标记
c// 标记槽为 MIGRATING clusterSendUpdate(clusterGetSlotOrReply(c,c->argv[1],&slot)); server.cluster->migrating_slots_to[slot] = node; // 标记槽为 IMPORTING server.cluster->importing_slots_from[slot] = node;
-
数据迁移(
migrateCommand
函数)- 序列化键数据并通过 Socket 发送到目标节点。
- 目标节点接收后调用
restoreCommand
写入数据。
通过上述机制,Redis Cluster 实现了动态的槽分配与数据迁移,支持弹性扩缩容,同时保证服务可用性与数据一致性。
二、节点角色与通信
1. 节点类型
- 主节点(Master):负责处理槽的读写请求,并参与故障转移投票。
- 从节点(Slave):复制主节点数据,主节点故障时通过选举晋升为新主。
2. Gossip 协议
- 节点间通信 :通过 PING/PONG 消息 交换集群状态信息(包括槽分配、节点存活状态等)。
- Meet 消息 :用于将新节点加入集群(例如执行
CLUSTER MEET <ip> <port>
)。
3. 集群状态维护
- 每个节点维护一份 集群配置信息 (
clusterState
),包含所有节点的槽分配、主从关系等。 - 最终一致性:通过 Gossip 协议逐步同步集群状态,可能存在短暂不一致。
三、请求路由与重定向
1. 客户端直连
- 客户端可直接连接任意节点,但需处理重定向逻辑:
- MOVED 重定向 :请求的键不属于当前节点槽范围,返回
MOVED <slot> <target-node-ip>:<port>
。 - ASK 重定向 :槽正在迁移中,临时重定向到目标节点(需先发送
ASKING
命令)。
- MOVED 重定向 :请求的键不属于当前节点槽范围,返回
2. Smart Client
- 优化客户端实现:缓存槽与节点的映射关系(
slot -> node
),减少重定向次数。
四、故障检测与恢复
1. 故障检测
- 心跳超时:节点间通过定期 PING/PONG 检测存活状态。
- 主观下线(PFAIL):某节点认为另一节点不可达时,标记为 PFAIL。
- 客观下线(FAIL):当多数主节点确认某节点不可达,标记为 FAIL,触发故障转移。
2. 故障转移
- 从节点选举 :
- 从节点发现主节点 FAIL 后,等待一段延迟时间(与复制偏移量相关)。
- 发起选举请求(
FAILOVER_AUTH_REQUEST
),由其他主节点投票。 - 获得多数票的从节点晋升为新主,接管原主槽位。
- 配置纪元(Epoch):全局递增的版本号,用于解决冲突(如脑裂场景)。
五、数据迁移与平衡
1. 槽迁移
-
命令示例:
bashCLUSTER SETSLOT <slot> IMPORTING <source-node-id> CLUSTER SETSLOT <slot> MIGRATING <target-node-id>
-
迁移过程:
- 源节点标记槽为
MIGRATING
,目标节点标记为IMPORTING
。 - 使用
MIGRATE
命令将槽内键分批迁移到目标节点。 - 迁移完成后,更新集群槽分配信息。
- 源节点标记槽为
2. 自动平衡
- 工具支持(如
redis-cli --cluster rebalance
)自动计算槽分配,使各节点负载均衡。
六、集群限制
-
跨槽操作限制
- 不支持跨槽的多键操作(如
MGET
多个不同槽的键)。 - Lua 脚本必须确保所有操作的键在同一个槽(可通过 Hash Tag 强制同一槽)。
- 不支持跨槽的多键操作(如
-
事务限制
- 仅支持同一节点上的事务(无法跨节点保证原子性)。
七、集群配置示例
1. 创建集群
bash
redis-cli --cluster create \
192.168.1.1:6379 192.168.1.2:6379 192.168.1.3:6379 \
--cluster-replicas 1
2. 添加节点
bash
# 添加主节点
redis-cli --cluster add-node 192.168.1.4:6379 192.168.1.1:6379
# 添加从节点
redis-cli --cluster add-node 192.168.1.5:6379 192.168.1.1:6379 --cluster-slave
八、核心源码结构
-
集群状态结构(server.h)
ctypedef struct clusterState { clusterNode *myself; // 当前节点信息 dict *nodes; // 所有节点字典(key 为节点 ID) clusterNode *slots[CLUSTER_SLOTS]; // 槽与节点的映射 uint64_t currentEpoch; // 当前配置纪元 // ... } clusterState;
-
Gossip 消息处理
- 在
cluster.c
中实现 PING/PONG 消息的发送与解析,维护节点状态。
- 在
总结
Redis Cluster 通过 分片、Gossip 协议、故障转移 实现了高可用与水平扩展,其设计平衡了性能与一致性,适用于大规模分布式场景。理解其原理有助于优化集群配置与故障排查。
五、事务与并发控制
-
事务(MULTI/EXEC)
- 原理 :命令入队,
EXEC
时原子执行,但无回滚(语法错误全失败,运行时错误继续执行)。 - Watch 命令:基于乐观锁,监控键是否被修改。
- 原理 :命令入队,
-
Lua 脚本
- 原子性:脚本执行期间阻塞其他命令,避免竞态条件。
- 示例:实现分布式锁续期、限流。
六、性能优化
-
Pipeline
- 原理:批量发送命令,减少 RTT(Round-Trip Time)。
- 限制:需控制批量大小,避免阻塞其他请求。
-
慢查询优化
- 配置 :
slowlog-log-slower-than
记录执行时间过长的命令。 - 排查 :避免
KEYS *
、大 Value、复杂聚合操作。
- 配置 :
-
连接池
- 作用:复用 TCP 连接,减少握手开销(如 Java 的 Lettuce 连接池)。
七、应用场景与实战
-
缓存穿透/雪崩/击穿
- 穿透:布隆过滤器拦截无效查询。
- 雪崩:随机过期时间 + 永不过期后台更新。
- 击穿 :互斥锁(
SETNX
)或热点数据永不过期。
-
分布式锁
- Redlock 算法:多节点加锁,半数成功才算获取锁。
- Redisson 实现:看门狗线程自动续期。
-
秒杀系统
- 预减库存:通过 Lua 脚本保证原子性扣减。
- 限流 :令牌桶(
INCR
+EXPIRE
)。
八、源码与底层机制
-
单线程模型
- 事件循环 :基于
epoll/kqueue
的 I/O 多路复用,非阻塞处理请求。 - 性能关键:纯内存操作 + 避免锁竞争。
- 事件循环 :基于
-
数据结构实现
- 跳跃表:多层链表加速范围查询(时间复杂度 O(logN))。
- 字典(HashTable):渐进式 Rehash,避免一次性迁移导致阻塞。
-
网络协议
- RESP 协议 :简单文本协议(如
*3\r\n$3\r\nSET\r\n$5\r\nkey1\r\n$5\r\nvalue\r\n
)。
- RESP 协议 :简单文本协议(如
九、监控与运维
-
监控指标
- 内存 :
used_memory
、mem_fragmentation_ratio
。 - 性能 :
instantaneous_ops_per_sec
、latency
。 - 持久化 :
rdb_last_save_time
、aof_current_size
。
- 内存 :
-
运维工具
- redis-cli :
--stat
实时监控、--bigkeys
分析大 Key。 - RedisInsight:图形化监控与管理。
- redis-cli :
十、扩展与生态
-
Redis Module
- 功能扩展:如 RediSearch(全文搜索)、RedisJSON(JSON 支持)。
- 自定义命令:通过 C 语言编写模块。
-
与云原生集成
- Kubernetes:使用 Operator 管理 Redis 集群。
- 云服务:阿里云 Tair(兼容 Redis,支持持久内存、多模型)。