一句话结论
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——生产环境务必改!
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:
fork()调用时,OS 创建子进程,子进程共享父进程的页表(Page Table),所有内存页标记为只读fork 只复制页表(几 MB),不复制实际物理内存(可能是几 GB),所以非常快
子进程遍历内存数据写入 RDB 文件,期间父进程需要写内存时触发 COW——OS 为该页创建副本
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 占用飙升、请求延迟增加。应对措施:
过期时间加随机值(核心方案)
关闭 THP 减少卡顿
分时段过期
追问 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。