ARCHIVE / INITIALIZING000%
正在载入档案界面SYS.07
Interview Prep

Redis

拆开 Redis 的事件循环、底层编码、RDB/AOF、复制、Sentinel、Cluster、Cache-Aside 与分布式锁。

Redis

将Redis的性能简单归因于“单线程”并不充分。需要分别分析核心命令执行、网络I/O、对象编码、持久化、复制、故障转移和缓存一致性。

Redis 执行、持久化与高可用全景图

Redis的性能来源与阻塞点

Redis核心命令通常由Event Loop串行执行,单条命令因此天然拥有原子执行边界,也省去共享数据结构上的大量锁竞争。部分版本支持多线程网络I/O,但命令执行是否串行要按照版本和配置说明,不能背成“Redis永远只有一条线程”。

快来自一组共同条件:

  • 数据主要在内存,随机访问延迟低;
  • I/O Multiplexing用少量线程照看大量连接;
  • 紧凑编码提高Cache Locality并减少内存;
  • 命令实现大多有明确复杂度,避免不必要阻塞;
  • 渐进式Rehash把扩容工作摊到多次操作。

Event Loop串行也意味着大Key、慢Lua、KEYS和大范围集合运算会阻塞后续命令。Fork、写时复制、AOF Rewrite、过期风暴、网络带宽和Swap同样可能成为性能瓶颈。

数据类型与底层编码

Redis对外类型和内部Encoding不是一一对应:

类型常见编码思路转换原因
Stringint / embstr / raw数字、小字符串、大字符串占用不同
ListQuicklist组织Listpack兼顾紧凑存储与两端操作
HashListpack → Hashtable小对象省内存,大对象稳定查找
SetIntset → Hashtable纯整数小集合更紧凑
Sorted SetListpack → Skiplist + Dict同时满足按member查分数和按score排序

阈值会随版本与配置变化。更可靠的理解是:数据规模较小时使用紧凑编码,规模增长后切换为具有稳定复杂度的数据结构。Redis Object还包含类型、编码、LRU/LFU相关信息和引用计数等元数据。

过期与淘汰的区别

TTL到期表示Key逻辑上不应再被返回,Redis通过惰性删除与定期抽样删除组合清理;Maxmemory Eviction则是在内存达到上限时按照策略选择牺牲品。一个管时间语义,一个管容量压力。

企业配置需要明确:

  • 是否允许无TTL的Key被淘汰;
  • 使用LRU、LFU、Random还是TTL倾向策略;
  • 业务能否承受缓存击穿后的回源洪峰;
  • 删除大对象是否使用异步free路径;
  • Hot Key与Big Key如何发现和拆分。

RDB、AOF 与混合持久化

RDB生成某一时点的数据快照,恢复快、文件紧凑,但两次快照之间存在数据窗口;AOF记录写命令,配合appendfsync always/everysec/no在持久性与延迟之间取舍,文件增长后需要Rewrite;混合持久化常用RDB格式作为AOF重写基底,再追加增量命令,兼顾恢复速度与数据窗口。

Fork创建子进程时会共享父进程Page,后续写入触发Copy-on-Write。数据集较大且写流量较高时,持久化后台任务仍会增加前台进程的内存占用和页复制开销。

持久化决定节点重启后可以恢复哪些数据;主节点故障后的自动切换则由复制和高可用机制负责,两者解决的问题不同。

Replication、Sentinel 与 Cluster

Replica通过全量同步或PSYNC部分重同步追赶Master。Replication ID、Offset和Backlog共同判断断线副本能否从历史缓冲继续;落后超过Backlog覆盖范围就要重新全量同步。

Redis复制通常是异步的,Master返回成功时Replica可能尚未收到数据。故障转移期间仍可能丢失最近写入,因此存在多个副本并不等同于RPO为零。

  • Sentinel:监控主从、主观/客观下线判断、选新主和通知客户端;解决的是不分片主从集群的故障转移;
  • Cluster:16384个Hash Slot分片到多个Master,客户端处理MOVED、ASK和拓扑刷新;解决容量与水平扩展,也带来迁槽、多Key跨槽和故障转移复杂度。

Cache-Aside的一致性边界

Redis Cache-Aside 读写路径与竞态

常见读路径为Cache Miss → 查询DB → 回填Cache;常见写路径为先更新DB → 再删除Cache。该模式能够降低不一致概率,但本身不提供Strict Consistency,仍需处理并发读写、删除失败和延迟窗口。

经典竞态:

R: Cache Miss
R: 从 DB 读到 v1
W: DB 更新为 v2
W: 删除 Cache
R: 把旧值 v1 回填 Cache

最终,已经删除的旧值被重新写入缓存。延迟双删可以缩小该窗口,但等待时间依赖真实请求耗时,不能构成严格一致性保证。更可靠的方案包括:

  • 缓存值带Version,拒绝旧Version覆盖;
  • DB提交后通过CDC/Outbox统一发失效事件;
  • 对强一致读绕过缓存或读取权威源;
  • Singleflight/互斥重建防止击穿;
  • TTL作为最终自愈边界,但不能提供实时一致性。

缓存穿透、击穿与雪崩

  • 穿透:请求的数据根本不存在,每次都绕过缓存打DB;可用参数校验、短TTL空值、Bloom Filter;
  • 击穿:单个热点Key过期,大量请求同时回源;可用互斥重建、逻辑过期、提前刷新;
  • 雪崩:大量Key同时失效或Redis整体不可用;可用TTL抖动、多级缓存、限流降级和隔离。

三者都可能表现为数据库回源流量上升,但触发条件和处理方法不同。

分布式锁与Fencing Token

SET key value NX PX ttl配合Lua比较Value后删除,可以避免误删其他客户端持有的锁。但客户端持锁后若暂停时间超过TTL,锁会被另一客户端获得;旧客户端恢复后仍可能向下游写入。锁服务中的租约已失效,但下游资源无法仅凭该租约识别迟到请求。

Fencing Token为每次成功获取锁分配单调递增编号,下游只接受比已处理Token更新的操作,从资源侧拒绝旧持有者的迟到请求。锁的租约必须与下游资源的写入保护关联,否则已经过期的锁持有者仍可能执行写入。

项目回答模板

Redis核心命令由Event Loop串行执行,网络I/O可按版本启用多线程。我们将RDB/AOF用于节点恢复,将Replication/Sentinel用于高可用,将Cluster用于分片,并分别设计三类机制。缓存采用Cache-Aside,数据库作为权威数据源;写入数据库后删除缓存,并通过CDC失效通知和Version防止旧值回填。热点Key使用Singleflight,TTL加入随机抖动。分布式锁仅用于协调,关键写入由下游Fencing Token保证顺序有效性。

高频追问

  1. Big Key为什么同时伤网络、Event Loop、复制和持久化?
  2. AOF everysec最坏可能丢多少,为什么不是绝对一秒?
  3. PSYNC什么时候退化成全量同步?
  4. Sentinel的主观下线和客观下线有什么区别?
  5. Cluster迁槽期间MOVED与ASK分别表示什么?
  6. 为什么Redlock也不能替代下游Fencing?