问题分析

这道题考察 Redis 在高并发场景下的稳定性保障(缓存异常)以及底层实现原理(数据结构)。面试官关注的不仅是概念的定义,更是解决方案的落地场景和底层设计的权衡。

核心解答

口语回答

关于缓存异常: 在高并发下,我们主要关注三个问题。首先是缓存穿透,即查询根本不存在的数据,导致请求直达数据库,通常用布隆过滤器拦截。其次是缓存击穿,指热点 Key 突然过期,大量请求瞬间打垮数据库,可用互斥锁逻辑过期策略解决。最后是缓存雪崩,即大量 Key 同时失效或 Redis 宕机,通过随机过期时间高可用集群来预防。

关于数据结构: Redis 常用的五种类型中,String 底层是 SDS,适合计数和缓存对象;List 是双向链表,常用于消息队列;Hash 适合存储对象属性;Set 用于去重和社交场景;ZSet 底层采用跳表,专用于排行榜。

Key Takeaways

场景核心特征推荐解决方案
穿透 (Penetration)数据不存在,查空布隆过滤器、缓存空对象
击穿 (Breakdown)热点 Key 过期,并发量大互斥锁 (Mutex)、逻辑过期
雪崩 (Avalanche)大量 Key 同时过期随机 TTL、集群高可用

深度剖析

1. 缓存防御体系详解

应对“查无此人”:缓存穿透 当用户查询一个数据库中不存在的 id(如 -1)时,缓存层和存储层都不会命中。为了避免每次请求都穿透到数据库,最有效的手段是引入布隆过滤器 (Bloom Filter)。它利用位图 (Bitmap) 和多个哈希函数快速判断 Key 是否存在。虽然存在微小的误判率(可能误判存在,但绝不会误判不存在),但能以极小的内存代价拦截绝大多数非法请求。另一种简单的兜底方案是缓存空对象,即写入一个 TTL 很短的空值,防止短时间内重复查询。

应对“热点失效”:缓存击穿 对于微博热搜这类扛着高并发的热点 Key,一旦过期,重建缓存的瞬间数据库压力会骤增。

  • 互斥锁 (Mutex) 方案追求强一致性。当发现缓存失效时,尝试获取分布式锁,拿到锁的线程去查 DB 重建缓存,其他线程等待。这虽然安全,但会限制吞吐量。
  • 逻辑过期 方案追求高可用性。Key 在 Redis 中永不过期,但 Value 里包含一个逻辑过期时间戳。查询时若发现已过期,直接返回旧值,同时异步启动一个线程去更新缓存。

应对“集体崩塌”:缓存雪崩 如果大量缓存设置了相同的过期时间(如电商大促零点),它们会在同一时刻集体失效。最简单的解法是在设置 TTL 时添加一个随机值(如 1-5 分钟),将失效时间分散开。此外,搭建 Redis Sentinel 或 Cluster 集群,避免单点故障导致的雪崩,也是架构层面的必要保障。

2. 数据结构的设计哲学

String 为什么要造轮子 (SDS)? Redis 没有直接使用 C 语言的字符串,而是构建了 SDS (Simple Dynamic String)。C 字符串获取长度需要遍历 (O(N)),且以空字符 \0 结尾,无法存储图片等二进制数据。SDS 显式维护了长度属性,实现了 O(1) 获取长度,并保证了二进制安全。此外,SDS 通过预分配内存空间,减少了字符串修改时的内存重分配次数,提升了写入性能。

ZSet 为什么选跳表 (Skiplist)? 在实现有序集合时,Redis 选择了跳表而非 B+ 树。跳表通过在链表之上增加多级索引,实现了平均 O(log N) 的查找效率。相比 B+ 树,跳表实现更简单,占用的内存更少。更重要的是,在进行插入和删除操作时,跳表只需要修改局部指针,而不需要像平衡树那样进行复杂的旋转或节点分裂,这在高并发写入场景下更具优势。

扩展知识

Redis 的线程模型演进 常说的“Redis 是单线程的”,主要指其命令执行阶段是单线程的,这避免了上下文切换和锁竞争,配合纯内存操作和 I/O 多路复用,造就了高性能。但从 Redis 6.0 开始,为了解决网络 I/O 的瓶颈,引入了多线程处理网络读写,但核心的命令执行依然保持单线程,因此我们无需担心多线程带来的并发安全问题。