一句话结论

RDB 是快照备份(全量、快、可能丢数据),AOF 是命令日志(增量、安全、文件大)。生产环境两者结合使用。高可用靠主从复制 + Sentinel + Cluster三级递进。

持久化

RDB(Redis Database)

SAVE: 主线程执行 → 阻塞所有请求 ❌
BGSAVE: fork 子进程 → copy-on-write → 写入临时文件 → 替换旧 RDB ✅

优点: 文件小、恢复快
缺点: 两次快照之间的数据会丢失

RDB Fork COW(Copy-On-Write)详析

BGSAVE 的 Fork 过程:

  1. Redis 主进程调用 fork()
       ↓
  2. 操作系统创建子进程
     → 子进程共享父进程的 页表(Page Table)
     → 所有内存页标记为 Read-Only(写保护)
     → fork 速度很快(只复制页表,不复制物理内存)
       ↓
  3. 子进程:
     → 遍历所有内存数据
     → 序列化为 RDB 格式
     → 写入临时文件(temp-<pid>.rdb)
     → rename 原子替换旧 RDB 文件
     → 退出
       ↓
  4. 父进程(主线程继续处理请求):
     → 如果请求需要修改某页内存(如 SET key value)
     → 触发 Page Fault
     → OS 为该页创建一份副本(Copy-On-Write)
     → 父进程修改副本,子进程仍然读原页
┌─────────────────────────────────────────────────────┐
│              Fork COW 内存变化示意                    │
│                                                      │
│  Fork 前:                                            │
│    父进程: [Page A] [Page B] [Page C] [Page D]       │
│                                                      │
│  Fork 后(共享):                                     │
│    父进程: [Page A] [Page B] [Page C] [Page D]       │
│              ↕共享↕  ↕共享↕  ↕共享↕  ↕共享↕         │
│    子进程: [Page A] [Page B] [Page C] [Page D]       │
│                                                      │
│  父进程修改 Page B:                                  │
│    父进程: [Page A] [Page B'] [Page C] [Page D]      │
│              ↕共享↕           ↕共享↕  ↕共享↕         │
│    子进程: [Page A] [Page B]  [Page C] [Page D]      │
│                                                      │
│  COW 开销 = 父进程写操作数量 × 页大小(通常 4KB)     │
└─────────────────────────────────────────────────────┘
COW 的内存风险:

  如果 BGSAVE 期间写的 Key 太多:
    → 大量页被 COW 复制
    → 内存占用可能接近 2 倍(原数据 + 副本)
    → 极端情况: fork 时内存 50% 使用,写操作大量后 OOM

  所以:
    → 生产环境 Redis 内存不要超过物理内存的 50%
    → 或预留足够内存给 COW(至少再留 30%)
    → 可使用 `info memory` 监控 mem_fragmentation_ratio
RDB 触发时机:

  自动触发(redis.conf):
    save 900 1      # 900 秒内 ≥1 次修改
    save 300 10     # 300 秒内 ≥10 次修改
    save 60 10000   # 60 秒内 ≥10000 次修改

  手动触发:
    BGSAVE  → 异步(推荐)
    SAVE    → 同步(阻塞!)
    SHUTDOWN → 如果没有 AOF,自动 BGSAVE 一次
    FLUSHALL → 如果配置了 SAVE 会自动触发(不推荐依赖)

AOF(Append Only File)

每次写命令追加到 AOF 文件末尾
AOF 重写: 合并冗余命令(如 6 次 incr → 1 次 SET)
  → fork 子进程 → 生成新 AOF → 替换旧 AOF

fsync 策略:
  always: 每次写都刷盘 → 最安全,最慢
  everysec: 每秒刷一次 → 丢 1 秒数据,性能好 ✅ 默认
  no: 交给 OS → 不可控

AOF 重写机制详解

AOF 重写不是基于旧 AOF 文件做压缩!

  重写流程:
  1. Redis 主进程 fork 子进程
  2. 子进程 根据当前内存中的数据状态 生成新 AOF 命令
     → 例: Redis 内存中 key=100(经过 6 次 incr)
     → 子进程直接生成: SET key 100(不是你记录的 6 条 incr)
  3. 重写期间,父进程继续接收新命令:
     → 新命令写入 AOF 缓冲区(旧 AOF 正常追加)
     → 同时写入 AOF 重写缓冲区(新 AOF 需要)
  4. 子进程完成重写 → 发信号给父进程
  5. 父进程将 AOF 重写缓冲区中的命令追加到新 AOF 文件
  6. 原子 rename 新 AOF 文件替换旧 AOF
AOF 重写触发条件(redis.conf):

  auto-aof-rewrite-percentage 100
    → 当前 AOF 文件大小 > 上次重写后大小的 100%(即增大了一倍)

  auto-aof-rewrite-min-size 64mb
    → 当前 AOF 文件至少要有 64MB 才触发重写
    → 防止 AOF 很小时频繁重写

  手动触发: BGREWRITEAOF
AOF 重写过程 ASCII 图示:

  子进程:
    ┌──────────────────────────────┐
    │ 遍历内存数据                  │
    │ key1 → SET key1 val1         │──→ 新 AOF 文件 (in progress)
    │ key2 → SET key2 val2         │
    │ mylist → RPUSH mylist ...    │
    └──────────────────────────────┘

  父进程同时:
    正常请求 → AOF 缓冲区 → 旧 AOF 文件
    正常请求 → 重写缓冲区 → 子进程完成后追加到新 AOF

  最终新 AOF 文件:
    SET key1 val1        ← 子进程生成的
    SET key2 val2        ← 子进程生成的
    RPUSH mylist a b     ← 子进程生成的
    INCR counter         ← 重写缓冲区追加
    SET newkey hello     ← 重写缓冲区追加

混合持久化(Redis 4.0+)

aof-use-rdb-preamble yes  # 开启混合持久化

AOF 文件格式:
  ┌──────────────────┬──────────────────────┐
  │  RDB 格式数据     │  AOF 格式增量命令     │
  │  (内存快照)       │  (RDB 之后的新命令)   │
  │  快速加载主体数据  │  补齐最后几秒的数据   │
  └──────────────────┬──────────────────────┘
                      ↑
                   一个文件

恢复流程:
  1. 先加载 RDB 部分 ← 速度快(O(N) 读二进制)
  2. 再重放 AOF 部分 ← 补齐 RDB 之后丢失的命令
  3. 既快又全 ← 兼顾 RDB 的速度和 AOF 的安全性
┌─────────────────────────────────────────────────────┐
│              三种持久化方案对比                       │
│                                                      │
│  ┌─────────┬──────────┬──────────┬────────────────┐ │
│  │         │ RDB      │ AOF      │ 混合持久化     │ │
│  ├─────────┼──────────┼──────────┼────────────────┤ │
│  │ 持久化   │ 快照     │ 命令日志 │ RDB快照+AOF增量│ │
│  │ 文件大小 │ 小       │ 大       │ 中             │ │
│  │ 恢复速度 │ 快       │ 慢       │ 快             │ │
│  │ 数据安全 │ 可能丢N秒│ everysec │ 接近不丢       │ │
│  │         │          │ 丢1秒    │                │ │
│  │ 对性能   │ fork瞬间 │ 写盘IO   │ 两者综合       │ │
│  │ 影响     │ 停机     │ 持续     │                │ │
│  │ 阻塞     │ fork      │ 几乎无   │ 几乎无        │ │
│  └─────────┴──────────┴──────────┴────────────────┘ │
│                                                      │
│  推荐: 生产环境开启混合持久化                          │
│    save 900 1  ← RDB 自动触发条件也配置                │
│    appendonly yes                                     │
│    aof-use-rdb-preamble yes                            │
│    appendfsync everysec                                │
└─────────────────────────────────────────────────────┘

高可用

主从复制

从节点: SLAVEOF master_ip port
  → 全量同步(第一次): master 生成 RDB → 发给 slave
  → 增量同步: master 写 replication buffer → 持续同步给 slave

异步复制: master 不等 slave 确认 → 可能丢数据

Sentinel(哨兵模式)

Sentinel 架构

┌─────────────────────────────────────────────────────┐
│                                                      │
│   ┌──────────────────────────────────────┐          │
│   │        Sentinel 集群 (≥3 个)          │          │
│   │   Sentinel-1  Sentinel-2  Sentinel-3  │          │
│   │       │           │           │       │          │
│   └───────┼───────────┼───────────┼───────┘          │
│           │           │           │                  │
│           ▼           ▼           ▼                  │
│   ┌──────────┐   ┌──────────┐   ┌──────────┐       │
│   │  Master  │◄──│ Slave-1  │◄──│ Slave-2  │       │
│   │  (读写)  │   │  (只读)   │   │  (只读)   │       │
│   └──────────┘   └──────────┘   └──────────┘       │
│      复制──►         复制──►                          │
└─────────────────────────────────────────────────────┘

Sentinel 选主流程

Master 宕机后的选主步骤:

  Step 1: 主观下线(SDOWN — Subjective Down)
    → Sentinel-1 PING Master → 超时(down-after-milliseconds)
    → Sentinel-1 认为 Master "主观下线"

  Step 2: 客观下线(ODOWN — Objective Down)
    → Sentinel-1 询问其他 Sentinel: "你们觉得 Master 挂了吗?"
    → 超过 quorum 个 Sentinel 确认 → Master 被标记为"客观下线"
    → quorum 通常 = N/2+1(如 3 个 Sentinel 时 quorum=2)

  Step 3: Leader Sentinel 选举
    → 每个 Sentinel 提议自己当 Leader
    → 使用 Raft 选举算法: 获得多数票 (majority=N/2+1) 才能当选
    → Leader 负责执行故障转移

  Step 4: 选择新 Master
    → Leader 在存活的 Slave 中选新主,优先级:
       1. slave-priority 配置值最小的(手动指定的优先级)
       2. 复制偏移量最大的(数据最新)
       3. runid 最小的(字典序,确定性选择)

  Step 5: 切换与通知
    → Leader 将选中的 Slave 提升为 Master
    → 其余 Slave 改为复制新 Master
    → 通知所有 Sentinel 和客户端(PUB/SUB 方式)
    → 旧 Master 恢复后自动降级为 Slave
┌─────────────────────────────────────────────────────┐
│          Sentinel 选主 ASCII 时间线                   │
│                                                      │
│  T0: Master 宕机                                     │
│       ↓                                              │
│  T1: Sentinel 发现 SDOWN (ping 超时)                 │
│       ↓ (等待 is-master-down-after-milliseconds)     │
│  T2: 确认 ODOWN (quorum 个 Sentinel 同意)            │
│       ↓                                              │
│  T3: Raft 选举 Leader                                │
│       ↓                                              │
│  T4: Leader 选择新 Master                             │
│       ↓                                              │
│  T5: SLAVEOF NO ONE → 新 Master 上线                 │
│       ↓                                              │
│  T6: SLAVEOF new-master → 其余 Slave 重新同步        │
│       ↓                                              │
│  T7: 客户端收到 +switch-master 通知                  │
│                                                      │
│  总耗时: 通常 15-30 秒                                │
│    = down-after-milliseconds (如 10s)                 │
│    + 选举时间 (~2-5s)                                 │
│    + 复制切换 (~2-5s)                                 │
└─────────────────────────────────────────────────────┘

Sentinel 脑裂问题

网络分区导致脑裂:

  ┌──────────────────┐        ┌──────────────────┐
  │  分区 1           │        │  分区 2           │
  │  Sentinel-1       │  ✗     │  Sentinel-2       │
  │  Sentinel-3       │  网络  │  Master (旧)      │
  │  Slave-1→新Master │  断开  │  客户端还在写     │
  └──────────────────┘        └──────────────────┘

  分区 1 选举了新 Master
  分区 2 的客户端还在写旧 Master
  → 数据不一致!

解决方案:
  min-slaves-to-write 1      # 至少 1 个 Slave 在线才允许写
  min-slaves-max-lag 10      # Slave 延迟不超过 10 秒
  → 旧 Master 发现 Slave 都失联 → 拒绝写入 → 避免脑裂丢数据

Cluster(集群模式)

16384 个哈希槽分布在不同节点
  → key 通过 CRC16(key) % 16384 → 槽 → 节点

特点: 去中心化(无代理层)、自动分片、故障转移
局限: 跨槽事务不支持

Cluster 槽位分配与数据分布

槽位分配示例(3 主 3 从集群):

  ┌─────────────────────────────────────────────────┐
  │                                                  │
  │  Master-1 ──── Slave-1                           │
  │  (slot 0-5460)                                   │
  │                                                  │
  │  Master-2 ──── Slave-2                           │
  │  (slot 5461-10922)                               │
  │                                                  │
  │  Master-3 ──── Slave-3                           │
  │  (slot 10923-16383)                              │
  │                                                  │
  └─────────────────────────────────────────────────┘

CRC16(key) % 16384 → 槽号 → 哪个 Master 负责 → 去那个节点读/写

Cluster 数据迁移(Slot 迁移)

当扩容加节点或缩容下线节点时,需要迁移槽位。

迁移流程(Slot Migrate):

  ┌────────────────────────────────────────────────────┐
  │ 源节点 (M1)          目标节点 (M2)                  │
  │                                                    │
  │ ← CLUSTER SETSLOT <slot> MIGRATING <M2-id>         │
  │    槽进入 "迁出" 状态                                │
  │                                                    │
  │                             ← CLUSTER SETSLOT <slot> IMPORTING <M1-id>
  │                               槽进入 "迁入" 状态     │
  │                                                    │
  │ ← CLUSTER GETKEYSINSLOT <slot> <count>             │
  │    获取槽中的 Key 列表                               │
  │                                                    │
  │ ← MIGRATE <M2-ip> <M2-port> <key> 0 <timeout>     │
  │    逐个迁移 Key(原子操作:源删+目标建)              │
  │    重复直到槽中 Key 全部迁完                         │
  │                                                    │
  │ ← CLUSTER SETSLOT <slot> NODE <M2-id>              │
  │    所有节点广播:槽归属变更                           │
  └────────────────────────────────────────────────────┘
迁移期间请求的路由规则:

  客户端请求迁出节点 (M1) 上的 Key:
    → Key 还在 M1 → 正常处理
    → Key 已迁到 M2 → M1 返回 MOVED 错误 → 客户端重定向到 M2

  客户端请求迁入节点 (M2) 上的 Key:
    → Key 已迁入 → 正常处理
    → Key 还在 M1 → M2 返回 ASK 错误 → 客户端向 M2 发送 ASKING → 再查 M2
      (注意:ASK 是临时重定向,MOVED 是永久重定向)
ASK vs MOVED:

  MOVED: "这个 Key 不在我这,你去 XXX"
    → 客户端应永久更新槽位映射
    → 下次直接去 XXX

  ASK: "这个槽在迁移中,Key 刚好在我这,但你得先 ASKING 一下"
    → 临时重定向,不更新客户端槽位映射
    → 下次可能还在原节点

Cluster Gossip 协议

Cluster 节点间使用 Gossip 协议通信:

  每个节点定期(每秒 10 次)随机选 N 个其他节点:
    → PING: 发送自己知道的节点信息
    → PONG: 收到 PING 的回复,附上自己的信息
    → MEET: 加新节点进集群时使用

  Gossip 消息体:
    - 节点自身信息(ID、IP、Port、Role、Flags)
    - 一些随机的其他节点信息(去中心化传播)

  通过这种"八卦传播",最终所有节点都知道整个集群的状态

Cluster 故障转移

Cluster 故障检测:

  1. 节点间互相 PING/PONG
  2. 如果 Node-A PING Node-B 超时(cluster-node-timeout 默认 15s)
     → Node-A 标记 Node-B 为 PFAIL(疑似故障)
  3. Node-A 通过 Gossip 传播 "Node-B 可能挂了"
  4. 如果超过半数 Master 都觉得 Node-B PFAIL
     → Node-B 被标记为 FAIL(确认故障)
  5. Node-B 的 Slave 发起选举(类似 Sentinel 的 Raft)
  6. 获得多数票的 Slave 提升为新 Master
  7. 接管 Node-B 的槽位

高可用方案对比

┌────────────────┬────────────┬──────────────┬──────────────┐
│                │ 主从复制    │ Sentinel     │ Cluster      │
├────────────────┼────────────┼──────────────┼──────────────┤
│ 数据分片       │ ❌ 无      │ ❌ 无        │ ✅ 自动分片  │
│ 自动故障转移   │ ❌ 手动    │ ✅ 自动      │ ✅ 自动      │
│ 容量扩展       │ ❌ 垂直    │ ❌ 垂直      │ ✅ 水平      │
│ 客户端复杂度   │ 低         │ 中           │ 高           │
│ 跨 Key 操作    │ ✅ 支持    │ ✅ 支持      │ ❌ 限制      │
│ 部署复杂度     │ 低         │ 中           │ 高           │
│ 最小节点数     │ 2          │ 5(2主+3哨)   │ 6(3主+3从)   │
│ 典型场景       │ 开发/测试   │ 中小规模生产  │ 大规模生产   │
│ 数据容量上限   │ 单机内存    │ 单机内存     │ 理论无限     │
└────────────────┴────────────┴──────────────┴──────────────┘

过期删除策略

惰性删除 (Lazy Expiration)

┌─────────────────────────────────────────────────────┐
│  每次访问 Key 时:                                     │
│    if (key 设置了 TTL && 已过期) {                     │
│      删除 Key;                                        │
│      返回 null;                                       │
│    }                                                  │
│  优点: CPU 最友好(只在访问时检查)                    │
│  缺点: 如果 Key 一直没被访问,就永远不会被删除          │
└─────────────────────────────────────────────────────┘

定期删除 (Active Expiration)

┌─────────────────────────────────────────────────────┐
│  serverCron 每秒执行 10 次 (每 100ms)                  │
│                                                      │
│  每次执行:                                            │
│    1. 从设置了 TTL 的 Key 中随机抽取 20 个            │
│    2. 删除其中已过期的                                │
│    3. 如果本次过期比例 > 25% → 再抽 20 个继续        │
│       (控制单次执行时间上限,算法有超时保护)          │
│                                                      │
│  优点: 主动清理过期 Key,防止内存泄漏                  │
│  缺点: 过期比例高时 CPU 占用上升                       │
└─────────────────────────────────────────────────────┘
过期删除对比:

  ┌─────────┬──────────────┬──────────────┐
  │         │ 优点         │ 缺点         │
  ├─────────┼──────────────┼──────────────┤
  │ 定时删除 │ 内存友好     │ CPU 不友好   │
  │         │ (及时清理)    │ (需定时轮训) │
  │ 惰性删除 │ CPU 友好     │ 内存不友好   │
  │         │ (不额外开销)  │ (过期不删)   │
  │ 定期删除 │ 折中方案     │ 需调频率     │
  └─────────┴──────────────┴──────────────┘

内存淘汰策略

全部策略一览

策略

范围

行为

适用场景

noeviction

-

满时直接报错

从不(默认,必须改)

allkeys-lru

全部 Key

淘汰最近最少使用

通用缓存(推荐)

volatile-lru

有过期时间

淘汰 LRU

部分 Key 可淘汰

allkeys-random

全部 Key

随机淘汰

很少用

volatile-random

有过期时间

随机淘汰

很少用

volatile-ttl

有过期时间

淘汰 TTL 最小的

期望先到期先删

allkeys-lfu

全部 Key

淘汰最不常用

热点 Key 保护(4.0+)

volatile-lfu

有过期时间

淘汰 LFU

部分 Key LFU(4.0+)

默认 noeviction——生产环境务必改!

LRU vs LFU 实现差异

Redis LRU 实现(近似 LRU)

Redis 不使用精确 LRU(需要双向链表维护精确顺序,内存开销大),而是使用近似 LRU:

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; // 24 bits, 存储访问时间戳(秒)
    int refcount;
    void *ptr;
} robj;
近似 LRU 的工作方式:

  1. 每个 Key 的 redisObject 中保存一个 lru 字段(24 位)
     → 记录该 Key 的最后一次访问时间(秒级时间戳)
  
  2. 当内存满需要淘汰时:
     → 随机抽取 N 个 Key(maxmemory-samples 控制,默认 5)
     → 比较这些 Key 的 lru 字段(即最后访问时间)
     → 淘汰 lru 最小的(即最久没被访问的)
  
  3. 这不是全局精确 LRU,但效果接近、成本极低
     → N 越大,越接近精确 LRU(默认 5 够用)
     → 可通过 maxmemory-samples 10 调大以更精确

  24 位时间戳 → 约 194 天回环一次 → 生产可用

Redis LFU 实现(Redis 4.0+)

LFU(Least Frequently Used)不仅要看"多久没访问",还要看"访问频率"。引入 LFU 是因为 LRU 无法区分"偶尔被访问但频率低"和"持续高频率访问"的 Key。

LRU 的问题: 一个 Key 每 10 分钟被访问一次(频率低但能保持 LRU 新鲜度)
LFU 的价值: 识别真正的"热点 Key"

LFU 复用 redisObject 的 lru 字段(24 bits),但含义不同:

  ┌──────────┬──────────┐
  │ 16 bits  │ 8 bits   │
  │ counter  │ time     │
  │ 访问频率  │ 衰减时间 │
  └──────────┴──────────┘

  counter(16 bits — 访问频率计数器):
    - 不是简单计数(防溢出)
    - 使用概率性递增:
      counter = min(counter + 1/(counter*factor + 1), 255)
    - 访问频率越高,counter 增长越慢(对数增长)
    - 实际实现中 counter 范围 0-255
  
  time(8 bits — 衰减时间):
    - 记录最后一次衰减的时间(分钟级,取低 8 位)
    - 用于计算衰减周期
LFU 访问频率计算原理:

  counter 的增长:
    每次访问:p = 1 / (counter * lfu_log_factor + 1)
    有 p 的概率 counter+1
    
    例: lfu_log_factor=10, counter=5
        p = 1/(5*10+1) = 1/51 ≈ 2%
        → 访问 100 次,大约 +2(概率性增长)
    
  counter 的衰减:
    每 lfu_decay_time 分钟,counter 减 1
    默认 lfu_decay_time=1(每分钟减 1)
    
  淘汰时:
    比较 counter 值,淘汰最小的
    因为 counter 小的 = 访问频率低 + 最近衰减多
LRU vs LFU 场景对比:

  ┌──────────────────────────────────────────────────┐
  │                                                    │
  │  假设缓存已满,内存需要淘汰                         │
  │                                                    │
  │  Key_A: 过去 5 分钟被访问了 1000 次(热搜商品)    │
  │  Key_B: 过去 1 秒被访问了 1 次(冷数据)           │
  │                                                    │
  │  LRU 视角: Key_B 更近 → 保留 B, 淘汰 A ❌          │
  │  LFU 视角: Key_A counter 高 → 保留 A, 淘汰 B ✅    │
  │                                                    │
  │  → LFU 更能保护"真正的热点"                        │
  └──────────────────────────────────────────────────┘
推荐配置:

  # 热点清晰的业务(如电商、社交)
  maxmemory-policy allkeys-lfu
  
  # LFU 精度调优
  lfu-log-factor 10     # 越大 counter 增长越慢(默认 10)
  lfu-decay-time 1      # 衰减周期(分钟)
  
  # 普通缓存(不需要区分冷热)
  maxmemory-policy allkeys-lru

多层追问面试题

题 1: RDB Fork COW

Q: BGSAVE fork 子进程的过程是怎么样的?为什么 fork 很快?

A:

  1. fork() 调用时,OS 创建子进程,子进程共享父进程的页表(Page Table),所有内存页标记为只读

  2. fork 只复制页表(几 MB),不复制实际物理内存(可能是几 GB),所以非常快

  3. 子进程遍历内存数据写入 RDB 文件,期间父进程需要写内存时触发 COW——OS 为该页创建副本

  4. COW 的开销取决于父进程在 BGSAVE 期间的写入量,写入越多 COW 副本越多

追问 1: fork 的时候内存使用量是怎么变化的?

  • fork 瞬间:内存几乎不增加(只多了页表)

  • BGSAVE 期间:每次写操作可能触发 COW 复制 4KB 页

  • 极端情况:如果写入量非常大,内存可能翻倍

追问 2: 如何减少 COW 的内存开销?

  • 关闭 THP(Transparent Huge Pages):避免复制 2MB 大页而不是 4KB 小页

  • 减少 BGSAVE 期间的写入操作(如业务低峰期做 BGSAVE)

  • 监控 info memory 的 mem_fragmentation_ratio

题 2: AOF 重写与混合持久化

Q: AOF 重写为什么能缩小文件?

A: AOF 重写不是压缩旧 AOF 文件,而是根据当前内存数据状态重新生成最小命令集:

  • 多次 incr → 一条 SET

  • 多次 lpush + lpop → 可能只保留剩余元素

  • 过期 Key 的写命令直接丢弃

  • 合并连续操作

追问 1: 混合持久化的 AOF 文件格式是什么?

AOF 文件前半部分是 RDB 格式的二进制快照,后半部分是 AOF 文本命令。加载时先快速还原 RDB 部分,再重放 AOF 部分补齐增量,兼顾速度和安全性。

追问 2: 如果 BGSAVE 和 BGREWRITEAOF 同时触发怎么办?

Redis 会串行执行。先启动的完成后,第二个再执行。不会同时有两个子进程在做磁盘 I/O。可通过 info persistence 查看 rdb_bgsave_in_progress 和 aof_rewrite_in_progress。

题 3: Sentinel vs Cluster

Q: Sentinel 和 Cluster 的核心区别是什么?

A:

  • Sentinel:只做高可用(故障转移),不做分片。一台 Master 存全部数据,Slave 用于容灾和读扩展。

  • Cluster:做高可用 + 分片。数据按 slot 分布在多台 Master 上,每台只存一部分数据。

追问 1: Sentinel 的 quorum 和 majority 有什么区别?

  • quorum:判定 Master 客观下线(ODOWN)需要的最小 Sentinel 数,可配置

  • majority:选举 Leader 和执行故障转移需要获得超过半数 Sentinel 的投票,不可配置(N/2+1)

  • 两者可以不同:如 5 个 Sentinel, quorum=2,则 2 个 Sentinel 即可判定 ODOWN,但需要 3 个才能选出 Leader 执行切换

追问 2: Cluster 中如果客户端请求了错误的节点怎么办?

节点返回 MOVED 错误(包含正确的节点地址),客户端收到后:

  • 更新本地的 slot→node 映射表

  • 重定向到正确节点

  • Smart Client(如 go-redis)自动处理这个过程

题 4: Redis 的删除/淘汰

Q: 如果 Redis 的 1000 万个 Key 在同一秒过期,会发生什么?

A: 这就是"缓存雪崩"的过期层面原因。定期删除会检查到大量过期 Key(过期比例 > 25%),会一直循环执行定期删除,导致 CPU 占用飙升、请求延迟增加。应对措施:

  1. 过期时间加随机值(核心方案)

  2. 关闭 THP 减少卡顿

  3. 分时段过期

追问 1: Redis 的 LRU 是精确的吗?

不是。是近似 LRU:随机抽 N 个 Key(maxmemory-samples),淘汰其中最久未访问的。N 越大越精确(默认 5)。

追问 2: 为什么需要 LFU 而不是只用 LRU?

LRU 只看"最近有没有被访问",不看"访问频率"。热点 Key 可能因为某段时间没被访问而被淘汰,新的低频 Key 反而留下来。LFU 通过频率计数保护真正的热点。

项目中的应用

在 分布式电商交易系统 中:

# Redis 配置
save 900 1       # 15 分钟内 1 次修改 → BGSAVE
save 300 10      # 5 分钟内 10 次修改 → BGSAVE
appendonly yes   # 开启 AOF
appendfsync everysec  # 每秒刷盘
maxmemory-policy allkeys-lru  # 内存满时淘汰 LRU

速记

RDB=快照(fork子进程,快,丢数据)。AOF=命令日志(everysec,稳,文件大)。混合=4.0+两全。主从=异步复制。Sentinel=哨兵选主。Cluster=16384槽分布。过期=惰性+定期。淘汰=noeviction默认→生产改allkeys-lru。