Redis性能之王:从数据结构到集群架构的深度解密

一、Redis 核心数据结构与实现原理

  1. 字符串(String)
    • 原理:基于简单动态字符串(SDS),支持预分配空间和惰性释放,避免频繁内存分配。
    • 应用 :缓存、计数器(INCR)、分布式锁(SETNX)。

** SDS(Simple Dynamic String,简单动态字符串)**

定义

Redis 自定义的字符串数据结构,用于替代 C 原生字符串,是 Redis 最基础的数据类型(String 类型的底层实现)。

核心原理

  1. 结构设计

    SDS 的结构包含三个关键字段:

    c 复制代码
    struct sdshdr {
        int len;    // 已用长度(不包含结尾'\0')
        int free;   // 剩余可用空间
        char buf[]; // 实际存储的字符数组(兼容C字符串)
    };
  2. 核心优势

    • O(1) 复杂度获取长度 :通过 len 字段直接获取字符串长度,无需遍历(C 字符串需 strlen() 遍历)。
    • 杜绝缓冲区溢出:修改前自动检查空间,不足时扩容(C 字符串需手动管理)。
    • 减少内存重分配:采用预分配(扩容时多分配冗余空间)和惰性释放(缩短时不立即缩容)。
    • 二进制安全 :允许存储包含 '\0' 的数据(如图片、音频),C 字符串以 '\0' 为结尾标识,无法处理这类数据。

  1. 哈希(Hash)
    • 原理 :底层使用 ziplist(小数据)或 hashtable(大数据),ziplist通过连续内存压缩存储键值对。
    • 应用:存储对象(如用户信息)。

Redis 中 ziplisthashtable 是两种不同的底层数据结构,用于实现 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) 实现,核心结构为:

    c 复制代码
    typedef 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 字节)时。
  • 转换过程
    1. 删除原有的 ziplist
    2. 创建新的 hashtable
    3. 将所有键值对插入 hashtable

五、选择建议

  • 优先使用 ziplist:当数据量小且追求内存效率时。
  • 切换为 hashtable:当数据量大或需要高频读写时。
  • 监控配置 :根据业务场景调整 hash-max-ziplist-* 参数,平衡内存和性能。

通过理解二者的底层实现,可以更好地优化 Redis 的内存使用和性能表现。


  1. 列表(List)
    • 原理 :早期用 ziplist,现多用 quicklist(由多个 ziplist 组成的双向链表),平衡内存和性能。
    • 应用:消息队列、最新消息列表。

Redis 的 quicklist列表(List) 数据类型的底层实现,结合了 ziplist双向链表 的优势,在内存效率和操作性能之间取得平衡。其设计核心是解决纯 ziplist 或纯链表在特定场景下的缺陷。


一、设计背景

  1. ziplist 的局限性

    • 内存连续:插入/删除需移动数据,时间复杂度 O(N)。
    • 连锁更新prevlen 字段变长编码可能导致多个节点重新分配内存(极端场景)。
    • 大容量瓶颈 :单一 ziplist 存储大量数据时,操作效率急剧下降。
  2. 链表的缺陷

    • 内存碎片:节点分散存储,内存利用率低。
    • 指针开销:每个节点需存储前后指针(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 个节点外的所有节点(依此类推)。
  • 平衡插入/删除性能

    • 头部/尾部操作 :直接操作首尾节点的 ziplist,时间复杂度 O(1)。
    • 中间插入/删除 :定位到具体 quicklistNode 后操作其 ziplist,若 ziplist 超出大小限制,则分裂为两个节点。

三、优势与性能分析

场景 quicklist 表现
内存利用率 高于纯链表(ziplist 紧凑存储),低于单一 ziplist(分片导致少量冗余)。
插入/删除头部/尾部 O(1) 时间复杂度(直接操作首尾节点的 ziplist)。
随机访问 O(n) 时间复杂度(需遍历节点),但实际性能优于纯链表(ziplist 局部连续访问)。
大范围操作 性能优于单一 ziplist(避免大规模内存拷贝)。

四、配置调优

  1. list-max-ziplist-size

    • 控制每个 ziplist 的最大容量(单位:字节或元素数)。
    • 示例:
      • list-max-ziplist-size -2:每个 ziplist 不超过 8 KB(默认值)。
      • list-max-ziplist-size 512:每个 ziplist 最多存储 512 个元素。
  2. list-compress-depth

    • 控制压缩深度,权衡 CPU 与内存。
    • 示例:
      • list-compress-depth 0:关闭压缩(默认)。
      • list-compress-depth 1:压缩中间节点,保留首尾节点不压缩(适合频繁操作两端场景)。

五、源码级设计细节

  1. 避免连锁更新
    quicklistziplist 分片后,单个 ziplist 长度有限,降低了连锁更新的概率。

  2. 压缩与解压策略

    • 写时压缩 :数据修改时,对目标 quicklistNode 解压 -> 修改 -> 按需重新压缩。
    • 读时解压:访问被压缩的节点时,自动解压后返回数据。

六、应用场景

  1. 消息队列
    • 高频 LPUSH/RPOP 操作时,直接操作首尾节点的 ziplist,性能接近 O(1)。
  2. 最新消息列表
    • 存储最近的 N 条消息,利用 LTRIM 快速截断旧数据。
  3. 大列表存储
    • 存储百万级元素的列表时,避免单一 ziplist 的内存重分配问题。

通过分片、压缩和双向链表的结合,quicklist 在保证操作性能的同时,显著提升了内存利用率,是 Redis 列表类型的理想底层实现。


  1. 集合(Set)

    • 原理 :基于 intset(整数集合)或 hashtable,自动根据元素类型切换。
    • 应用 :标签系统、共同关注(SINTER)。
  2. 有序集合(Sorted Set)

    • 原理ziplist(小数据)或 跳跃表(SkipList) + 字典,跳跃表支持快速范围查询,字典用于 O(1) 查找分数。
    • 应用:排行榜、延迟队列。
  3. 其他高级类型

    • 位图(Bitmap) :基于字符串的位操作,如统计活跃用户(BITCOUNT)。
    • HyperLogLog:概率算法,固定 12KB 内存估算上亿唯一值(误差 0.81%)。
    • GEO:基于有序集合的 GeoHash 编码,存储地理位置。

二、持久化机制

  1. RDB(快照)

    • 原理fork 子进程生成数据快照(二进制文件),主进程继续处理请求。
    • 优点:恢复快,适合备份。
    • 缺点:可能丢失最后一次快照后的数据。
  2. AOF(追加日志)

    • 原理 :记录所有写命令(文本格式),支持 fsync 策略(always/everysec/no)。
    • 重写机制 :通过 BGREWRITEAOF 生成紧凑的新 AOF 文件,避免日志膨胀。

AOF(Append-Only File)持久化 fsync 策略 决定了这些命令何时从操作系统缓冲区同步到磁盘,直接影响数据安全性和性能。:


一、fsync 策略的三种模式

通过 appendfsync 配置项设置,可选值为 alwayseverysecno

1. appendfsync always(最安全,性能最低)

  • 原理 :每次写入 AOF 缓冲后,立即调用 fsync() 强制将数据刷到磁盘。
  • 数据安全性:理论上最多丢失一个命令的数据(除非磁盘或内核缓冲区故障)。
  • 性能影响:频繁的磁盘 I/O 操作(尤其是机械硬盘),吞吐量显著下降。

2. appendfsync everysec(默认配置,平衡选择)

  • 原理
    1. 主线程将命令写入 AOF 缓冲区
    2. 后台线程每隔 1 秒 调用 fsync() 将缓冲区数据刷到磁盘。
  • 数据安全性 :最多丢失 1 秒内的写操作(若故障发生在两次 fsync 之间)。
  • 性能影响:吞吐量接近无持久化模式,适合大多数场景。

3. appendfsync no(最不安全,性能最高)

  • 原理 :数据写入 AOF 缓冲区后,由操作系统决定何时刷盘(通常依赖内核的 flush 机制)。
  • 数据安全性:可能丢失大量数据(取决于操作系统刷盘策略,通常最长 30 秒)。
  • 性能影响 :无 fsync 开销,吞吐量最高,但风险极大。

二、底层机制解析

  1. 写入流程
    • 客户端写命令 → Redis 服务端 → AOF 缓冲区 → 根据策略调用 fsync() → 磁盘。
  2. fsync() 的作用
    • 强制将内核缓冲区(Page Cache)中的数据写入磁盘,确保持久化。
    • 相比 write() 仅写入内核缓冲区,fsync() 是数据安全的最后屏障。

三、策略对比与选型建议

策略 数据安全性 吞吐量 适用场景
always 最高(近似零丢失) 最低 金融、支付等对数据一致性要求极高的场景。
everysec 较高(秒级丢失) 中等 通用场景(默认推荐)。
no 最低(依赖操作系统) 最高 允许数据丢失的缓存场景或测试环境。

四、配置示例

  1. 配置文件(redis.conf)

    bash 复制代码
    appendonly yes           # 启用 AOF
    appendfilename "appendonly.aof"
    appendfsync everysec     # 设置 fsync 策略
  2. 运行时动态修改

    bash 复制代码
    CONFIG SET appendfsync always

五、优化注意事项

  1. 结合混合持久化 (Redis 4.0+)
    • 启用 aof-use-rdb-preamble yes,AOF 文件包含 RDB 格式的全量数据 + AOF 增量命令,提升重启恢复速度。
  2. AOF 重写
    • 定期执行 BGREWRITEAOF 压缩 AOF 文件,避免文件过大。
  3. 磁盘性能
    • 使用 SSD 硬盘可显著降低 fsync 的性能损耗。

六、故障恢复逻辑

  • Redis 重启时,会优先加载 AOF 文件(因其数据更完整),通过 重放所有命令 恢复内存数据。
  • 若 AOF 文件损坏,可用 redis-check-aof --fix 工具修复。

通过合理选择 fsync 策略,可以在数据安全性和性能之间找到最佳平衡点。默认的 everysec 是大多数场景的理想选择,而极端场景可按需调整。

  1. 混合持久化(Redis 4.0+)
    • 原理:RDB 全量 + AOF 增量,重启时先加载 RDB,再重放 AOF 增量命令。
    • 优势:兼顾恢复速度和数据安全性。

BGREWRITEAOF 是一个异步命令,用于 重写 AOF 文件(Append-Only File),目的是压缩 AOF 文件体积、优化持久化性能,同时确保数据完整性:


一、为什么需要 BGREWRITEAOF?

  1. AOF 文件膨胀问题

    • AOF 文件记录所有写操作命令(如多次 SET key),随着时间推移会变得臃肿。
    • 例如:对同一个 key 修改 100 次,AOF 会记录 100 条命令,但实际只需保留最终状态。
  2. 恢复效率低下

    • 过大的 AOF 文件在 Redis 重启时重放速度慢,影响服务可用性。

二、BGREWRITEAOF 的作用

  1. 生成紧凑的新 AOF 文件

    • 基于当前内存数据快照,生成一个仅包含 重建数据集所需最小命令集 的新 AOF 文件。
    • 示例
      旧 AOF 文件:包含 SET key 1SET key 2SET key 3
      新 AOF 文件:仅保留 SET key 3(直接记录最终值)。
  2. 解决文件冗余

    • 删除无效命令(如已过期键的写入操作)。
    • 合并多条命令(如多个 SADD 合并为一条)。

三、实现原理

  1. 触发方式

    • 自动触发 :根据配置 auto-aof-rewrite-percentageauto-aof-rewrite-min-size(如 AOF 文件大小增长超过 100% 且大于 64MB)。
    • 手动触发 :执行 BGREWRITEAOF 命令。
  2. 执行流程

    • 步骤 1fork 子进程(利用 Copy-On-Write 机制),避免阻塞主进程。
    • 步骤 2:子进程遍历内存数据,生成新 AOF 文件的临时内容(格式兼容 AOF)。
    • 步骤 3:新文件生成后,替换旧文件(原子操作)。
    • 步骤 4 :主进程将重写期间的新写入命令追加到新 AOF 文件(通过 AOF 重写缓冲区 保证数据一致性)。

四、关键特性

特性 说明
异步非阻塞 子进程执行重写,主进程继续处理请求(仅 fork 阶段短暂阻塞)。
数据安全 重写期间的新命令会写入 AOF 缓冲区和重写缓冲区,确保无数据丢失。
兼容性 新 AOF 文件格式与旧版本兼容,支持 Redis 版本升级。
资源消耗 fork 子进程可能消耗较多内存(尤其在数据集较大时)。

五、配置调优

  1. 自动重写阈值

    bash 复制代码
    auto-aof-rewrite-percentage 100  # 当前 AOF 文件大小比上次重写后增长 100%
    auto-aof-rewrite-min-size 64mb   # AOF 文件最小重写体积
  2. 混合持久化(Redis 4.0+)

    启用 aof-use-rdb-preamble yes,新 AOF 文件头部为 RDB 格式(全量数据),后续为增量 AOF 命令,兼顾恢复速度和数据安全。


六、注意事项

  1. 磁盘空间
    • 确保磁盘剩余空间足够(需容纳旧 AOF 文件 + 新 AOF 文件)。
  2. 性能影响
    • 避免在写入高峰触发重写(可能因 fork 延迟导致短暂阻塞)。
  3. 监控命令
    • 使用 INFO Persistence 查看 aof_rewrite_in_progressaof_last_rewrite_time_sec 监控重写状态。

七、与 BGSAVE 的优先级

  • 若同时执行 BGSAVE(生成 RDB)和 BGREWRITEAOF
    • Redis 2.6 及以前BGREWRITEAOF 会延迟到 BGSAVE 完成后执行。
    • Redis 2.8+:允许两者同时运行(但实际执行仍有资源竞争)。

三、内存管理与淘汰策略

  1. 内存分配

    • jemalloc:默认内存分配器,减少内存碎片。
    • 对象共享 :如 0~9999 的整数对象复用。
  2. 淘汰策略(8种)

    • LRU/LFU:近似算法(随机采样 + 淘汰最久/最少使用)。
    • TTL:淘汰过期时间最近的数据。
    • 配置示例maxmemory-policy volatile-lru

8 种淘汰策略及其实现原理:

一、淘汰策略分类

Redis 的淘汰策略通过 maxmemory-policy 配置,分为两类:

  1. 针对所有键allkeys-*):无论是否设置过期时间,都可能被淘汰。
  2. 仅针对过期键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 位),适合大规模数据。
  • 缺点:精度较低(采样可能遗漏真实 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 均匀淘汰,避免局部热点影响。

六、性能与监控

  1. 监控命令

    bash 复制代码
    INFO memory  # 查看内存使用及淘汰次数(evicted_keys)
  2. 调优方向

    • 增加 maxmemory-samples 提高 LRU/LFU 精度(代价:CPU 开销增加)。
    • 结合业务特性选择策略(如电商促销期间切为 allkeys-lfu)。

通过合理配置淘汰策略,可以在内存限制下最大化缓存命中率,平衡性能与数据保留需求。

  1. 内存碎片整理
    • 原理 :通过 MEMORY PURGE 或配置 activedefrag 自动整理碎片。

四、高可用与集群

  1. 主从复制

    • 全量同步 :从节点发送 PSYNC,主节点生成 RDB 发送。
    • 增量同步 :基于复制积压缓冲区(repl_backlog)发送命令流。
  2. 哨兵(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. 如何解决脑裂?

  • 场景模拟

    1. 主节点(M)有 3 个从节点(S1, S2, S3),配置 min-slaves-to-write=2
    2. 网络分区导致 M 只连接 S1,S2 和 S3 被隔离。
    3. 此时 M 的健康从节点数量(1) < 配置值(2),触发写入拒绝。
    4. 客户端无法向 M 写入数据,防止数据不一致。
    5. 哨兵检测到 M 失联,选举 S2 或 S3 为新主节点。
    6. 新主节点满足 min-slaves-to-write 后恢复写入。
  • 结果

    • 旧主节点(M)因无法满足从节点数量要求,停止写入。
    • 只有新主节点能正常写入,确保数据一致性。

三、底层实现原理

1. 健康从节点判定

  • 连接状态:从节点必须与主节点保持连接。
  • 复制延迟 :从节点的 repl_offset 与主节点的 master_repl_offset 差值需 ≤ min-slaves-max-lag

2. 写入拒绝逻辑

  • 主节点在每次处理写命令前,检查以下条件:

    c 复制代码
    if (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. 适用场景

  • 高一致性场景:金融交易、订单系统等要求强一致性的业务。
  • 多可用区部署:跨机房部署时,确保主节点在足够从节点存活时才能写入。

五、注意事项

  1. 从节点数量要求 :需部署足够从节点(至少 ≥ min-slaves-to-write)。
  2. 监控与告警
    • 监控主节点的 connected_slaves 和从节点延迟。
    • 配置哨兵自动故障转移,避免人工介入延迟。
  3. 客户端兼容性 :客户端需处理 -NOREPLICAS 错误(如重试、降级逻辑)。

六、对比其他方案

方案 优点 缺点
min-slaves-to-write 主动拒绝写入,数据强一致 可能降低写入可用性
Quorum 协议 分布式一致性保障 实现复杂,性能开销大
人工介入 灵活处理特殊场景 响应慢,易出错

通过合理配置 min-slaves-to-writemin-slaves-max-lag,可以在网络分区场景下有效避免脑裂问题,是 Redis 高可用架构中保障数据一致性的重要机制。


  1. Cluster 集群
    • 数据分片:16384 个槽(Slot),使用 CRC16 算法分片。
    • Gossip 协议:节点间通过 PING/PONG 交换状态信息。
    • 重定向机制 :客户端访问错误节点时,返回 MOVEDASK 指令。

Redis Cluster 是 通过 数据分片(Sharding)主从复制 实现高可用与水平扩展:


一、数据分片与槽(Slot)分配

1. 槽的概念

  • Redis Cluster 将整个数据集划分为 16384 个槽(Slot),每个键通过算法映射到特定槽。

  • 键到槽的映射

    python 复制代码
    slot = 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:向目标节点发送命令,声明其负责的槽。

    bash 复制代码
    CLUSTER ADDSLOTS <slot1> <slot2> ... <slotN>
  • 步骤 2 :目标节点更新本地 clusterState.slots[] 数组,标记槽归属。

  • 步骤 3 :目标节点通过 Gossip 协议(PING/PONG 消息)将槽分配信息广播给其他节点。

  • 步骤 4:集群所有节点更新槽映射表,最终达成一致。

3. 注意事项

  • 无数据迁移 :仅修改槽归属元数据,不迁移原有数据(需确保槽未被占用或已清空)。
  • 风险:若槽已存在数据,直接分配会导致数据丢失或访问错误。

二、redis-cli --cluster reshard 自动化迁移

1. 适用场景

  • 扩容/缩容:新增或移除节点时重新平衡槽分布。
  • 负载均衡:调整槽分配以均匀分布数据。

2. 实现流程

  • 步骤 1:计算迁移计划

    工具根据目标节点数量、槽分布和权重,自动计算需迁移的槽及数量。

    bash 复制代码
    redis-cli --cluster reshard <host>:<port> --cluster-from <源节点ID> --cluster-to <目标节点ID> --cluster-slots <迁移槽数>
  • 步骤 2:标记迁移状态

    • 源节点标记槽为 MIGRATING(迁移中)。
    • 目标节点标记槽为 IMPORTING(接收中)。
  • 步骤 3:迁移数据

    1. 源节点遍历待迁移槽的所有键,逐个执行 MIGRATE 命令:

      bash 复制代码
      MIGRATE <目标节点IP> <目标节点端口> <key> 0 <timeout>
      • 原子性迁移:键数据会被序列化传输到目标节点,传输成功后源节点删除该键。
    2. 迁移过程中,客户端请求可能触发 ASK 重定向(临时将请求转发到目标节点)。

  • 步骤 4:更新槽归属

    迁移完成后,目标节点通过 CLUSTER SETSLOT <slot> NODE <目标节点ID> 声明槽归属,并通过 Gossip 协议同步到全集群。

  • 步骤 5:清理状态

    移除 MIGRATINGIMPORTING 标记,集群进入稳定状态。


三、关键技术原理

1. MIGRATE 命令的原子性

  • 过程
    1. 源节点序列化键数据并发送到目标节点。
    2. 目标节点反序列化数据并存入内存。
    3. 目标节点返回确认后,源节点删除该键。
  • 一致性保障:迁移过程中键在源和目标节点间短暂存在,但客户端通过重定向保证访问正确节点。

2. ASK 与 MOVED 重定向

  • MOVED 重定向:槽已永久迁移,客户端需更新槽映射缓存。
  • ASK 重定向 :槽迁移中临时重定向,客户端需先发送 ASKING 命令告知目标节点接受请求。

3. 集群状态同步

  • Gossip 协议:节点间通过 PING/PONG 消息交换槽分配信息,最终一致。
  • 配置纪元(Epoch):全局递增的版本号,解决网络分区导致的配置冲突。

四、示例:扩容时迁移槽

  1. 添加新主节点

    bash 复制代码
    redis-cli --cluster add-node 新节点IP:端口 集群任意节点IP:端口
  2. 执行重分片

    bash 复制代码
    redis-cli --cluster reshard 集群任意节点IP:端口
    • 交互式输入:迁移槽数、目标节点 ID、源节点 ID(或输入 all 从所有节点抽取槽)。
  3. 验证结果

    bash 复制代码
    redis-cli --cluster check 集群任意节点IP:端口

五、注意事项

  1. 性能影响

    • 迁移期间可能增加网络和 CPU 负载,建议低峰期操作。
    • 大 Key 迁移可能导致阻塞(单线程模型)。
  2. 客户端兼容性

    • 客户端需正确处理 MOVEDASK 重定向(如 Jedis、Lettuce 等 Smart Client 已支持)。
  3. 数据一致性

    • 迁移过程中对键的写操作可能丢失(需业务端重试或避免迁移期间写入)。

六、源码关键逻辑

  1. 槽状态标记

    c 复制代码
    // 标记槽为 MIGRATING
    clusterSendUpdate(clusterGetSlotOrReply(c,c->argv[1],&slot));
    server.cluster->migrating_slots_to[slot] = node;
    
    // 标记槽为 IMPORTING
    server.cluster->importing_slots_from[slot] = node;
  2. 数据迁移(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 命令)。

2. Smart Client

  • 优化客户端实现:缓存槽与节点的映射关系(slot -> node),减少重定向次数。

四、故障检测与恢复

1. 故障检测

  • 心跳超时:节点间通过定期 PING/PONG 检测存活状态。
  • 主观下线(PFAIL):某节点认为另一节点不可达时,标记为 PFAIL。
  • 客观下线(FAIL):当多数主节点确认某节点不可达,标记为 FAIL,触发故障转移。

2. 故障转移

  • 从节点选举
    1. 从节点发现主节点 FAIL 后,等待一段延迟时间(与复制偏移量相关)。
    2. 发起选举请求(FAILOVER_AUTH_REQUEST),由其他主节点投票。
    3. 获得多数票的从节点晋升为新主,接管原主槽位。
  • 配置纪元(Epoch):全局递增的版本号,用于解决冲突(如脑裂场景)。

五、数据迁移与平衡

1. 槽迁移

  • 命令示例

    bash 复制代码
    CLUSTER SETSLOT <slot> IMPORTING <source-node-id>
    CLUSTER SETSLOT <slot> MIGRATING <target-node-id>
  • 迁移过程

    1. 源节点标记槽为 MIGRATING,目标节点标记为 IMPORTING
    2. 使用 MIGRATE 命令将槽内键分批迁移到目标节点。
    3. 迁移完成后,更新集群槽分配信息。

2. 自动平衡

  • 工具支持(如 redis-cli --cluster rebalance)自动计算槽分配,使各节点负载均衡。

六、集群限制

  1. 跨槽操作限制

    • 不支持跨槽的多键操作(如 MGET 多个不同槽的键)。
    • Lua 脚本必须确保所有操作的键在同一个槽(可通过 Hash Tag 强制同一槽)。
  2. 事务限制

    • 仅支持同一节点上的事务(无法跨节点保证原子性)。

七、集群配置示例

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

八、核心源码结构

  1. 集群状态结构(server.h)

    c 复制代码
    typedef struct clusterState {
        clusterNode *myself;          // 当前节点信息
        dict *nodes;                  // 所有节点字典(key 为节点 ID)
        clusterNode *slots[CLUSTER_SLOTS]; // 槽与节点的映射
        uint64_t currentEpoch;        // 当前配置纪元
        // ...
    } clusterState;
  2. Gossip 消息处理

    • cluster.c 中实现 PING/PONG 消息的发送与解析,维护节点状态。

总结

Redis Cluster 通过 分片、Gossip 协议、故障转移 实现了高可用与水平扩展,其设计平衡了性能与一致性,适用于大规模分布式场景。理解其原理有助于优化集群配置与故障排查。


五、事务与并发控制

  1. 事务(MULTI/EXEC)

    • 原理 :命令入队,EXEC 时原子执行,但无回滚(语法错误全失败,运行时错误继续执行)。
    • Watch 命令:基于乐观锁,监控键是否被修改。
  2. Lua 脚本

    • 原子性:脚本执行期间阻塞其他命令,避免竞态条件。
    • 示例:实现分布式锁续期、限流。

六、性能优化

  1. Pipeline

    • 原理:批量发送命令,减少 RTT(Round-Trip Time)。
    • 限制:需控制批量大小,避免阻塞其他请求。
  2. 慢查询优化

    • 配置slowlog-log-slower-than 记录执行时间过长的命令。
    • 排查 :避免 KEYS *、大 Value、复杂聚合操作。
  3. 连接池

    • 作用:复用 TCP 连接,减少握手开销(如 Java 的 Lettuce 连接池)。

七、应用场景与实战

  1. 缓存穿透/雪崩/击穿

    • 穿透:布隆过滤器拦截无效查询。
    • 雪崩:随机过期时间 + 永不过期后台更新。
    • 击穿 :互斥锁(SETNX)或热点数据永不过期。
  2. 分布式锁

    • Redlock 算法:多节点加锁,半数成功才算获取锁。
    • Redisson 实现:看门狗线程自动续期。
  3. 秒杀系统

    • 预减库存:通过 Lua 脚本保证原子性扣减。
    • 限流 :令牌桶(INCR + EXPIRE)。

八、源码与底层机制

  1. 单线程模型

    • 事件循环 :基于 epoll/kqueue 的 I/O 多路复用,非阻塞处理请求。
    • 性能关键:纯内存操作 + 避免锁竞争。
  2. 数据结构实现

    • 跳跃表:多层链表加速范围查询(时间复杂度 O(logN))。
    • 字典(HashTable):渐进式 Rehash,避免一次性迁移导致阻塞。
  3. 网络协议

    • RESP 协议 :简单文本协议(如 *3\r\n$3\r\nSET\r\n$5\r\nkey1\r\n$5\r\nvalue\r\n)。

九、监控与运维

  1. 监控指标

    • 内存used_memorymem_fragmentation_ratio
    • 性能instantaneous_ops_per_seclatency
    • 持久化rdb_last_save_timeaof_current_size
  2. 运维工具

    • redis-cli--stat 实时监控、--bigkeys 分析大 Key。
    • RedisInsight:图形化监控与管理。

十、扩展与生态

  1. Redis Module

    • 功能扩展:如 RediSearch(全文搜索)、RedisJSON(JSON 支持)。
    • 自定义命令:通过 C 语言编写模块。
  2. 与云原生集成

    • Kubernetes:使用 Operator 管理 Redis 集群。
    • 云服务:阿里云 Tair(兼容 Redis,支持持久内存、多模型)。
相关推荐
搬砖的阿wei1 分钟前
从零开始学 Flask:构建你的第一个 Web 应用
前端·后端·python·flask
石去皿4 分钟前
力扣hot100 31-40记录
java·算法·leetcode
草巾冒小子12 分钟前
查看pip3 是否安装了Flask
后端·python·flask
脑子慢且灵20 分钟前
【蓝桥杯】 枚举和模拟练习题
java·开发语言·职场和发展·蓝桥杯·模拟·枚举
m0_6640470231 分钟前
基于Spring Boot+Layui构建企业级电子招投标系统源码
java·spring cloud·招投标系统源码·电子招标采购系统源码·企业电子招标采购系统源码
小郝 小郝41 分钟前
(C语言)指针运算 习题练习1.2(压轴难题)
java·开发语言·算法
放肆的驴1 小时前
EasyDBF Java读写DBF工具类(支持:深交所D-COM、上交所PROP)
java·后端
DavidSoCool1 小时前
Java使用Californium 实现CoAP协议交互代码案例
java·物联网
开开心心就好1 小时前
便携免安装,畅享近 30 种 PDF 文档处理功能
java·服务器·python·eclipse·pdf·word·excel