← 学习门户 每日面试题 学习看板

2026-09-20

Redis 缓存与一致性 + 并发(引入线程池)

10 题 · 平均 3.5/5 · low 3.33 · medium 3.25 · high 4 · 第 1 轮

#1 Redis为什么这么快? low redis

题目

Redis为什么这么快?

我的回答

Redis 快跟慢本身是一个相对概念。Redis 所谓的快是跟谁对比?它对比的条件一般是跟过往的数据库,磁盘性的交互的,正常的、常规的数据库,MySQL、Oracle 这些去比,所以它快。它快的原因核心在于几个点吧。首先是存储介质跟计算介质获取方数据方式。存储介质,它的所有数据是在缓存中的,直接从缓存中拿,要比数据库去从磁盘中去获取要快得多。当然,数据库 MySQL 它也有缓存读,但是这是另外一个事。另外额外的,它快的原因就是它的数据获取方式。它自定义了大量自己很多自己使用方便的数据库结构,数据结构。这些数据结构方便它获取数据。而另外它核心的获取数据方式是 Key-Value,通过类,通过哈希的方式直接定位到缓存中的对应数据位置,所以它获取的数据基本上是特别特别快的。

点评

评分:⭐⭐⭐☆☆(3/5)
优点:开篇点出"快是相对概念",体现思考深度;抓住全内存存储 + Hash O(1) 两个核心
不足:遗漏最关键架构点——单线程 + I/O 多路复用;未提 RESP 协议、SDS、jemalloc;未对比 MySQL 慢的多维原因
补充:架构层(epoll 单线程)、数据层(SDS/listpack)、系统层(jemalloc、fork COW);对比 MySQL 慢在磁盘 I/O + 锁竞争 + SQL 解析 + 事务日志 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 Redis 高性能的架构层、数据结构层、系统层三维度理解,能否跳出"内存 vs 磁盘"这种表层对比。

**知识点拆解**:

1. **架构层:单线程 + I/O 多路复用**
- Redis 主线程单线程处理所有命令,避免锁竞争和上下文切换
- 依赖 epoll(Linux)/ kqueue(macOS)实现 I/O 多路复用
- 6.0 引入多线程处理网络 I/O,命令执行仍单线程

2. **数据层:全部驻留内存 + 高效数据结构**
- 数据全内存,避免磁盘 I/O
- Hash 底层是哈希表 O(1);小 hash 用 listpack
- SDS O(1) 取长度、二进制安全、避免大量 realloc

3. **系统层:内存分配器 + 无锁单线程**
- jemalloc 替代 ptmalloc,减少碎片
- RDB 用 fork + COW,主线程几乎不阻塞
- 无并发 → 无锁 → 无 CAS 开销

4. **网络层:RESP 协议简洁**
- 基于 TCP 的二进制协议,解析简单
- 支持 Pipeline / 二进制传输

**对比 MySQL 的"慢"来自哪里**:磁盘 I/O、多线程锁竞争、SQL 解析、事务日志落盘。

第二部分:考察方向与延伸递进

**考察方向**:
- 深度:单线程模型能否水平扩展?6.0 多线程边界
- 广度:与 Memcached / KV 存储性能模型对比
- 应用:大 key、慢命令导致单线程阻塞如何排查与规避

**延伸递进面试题**:

1. **单线程的 Redis 6.0 为什么引入多线程?**
- 解答要点:命令执行仍单线程,多线程只做 socket read/write 和协议解析
2. **Redis 什么场景下会变慢甚至阻塞?**
- 解答要点:大 key(DEL 大 hash)、KEYS、SORT、SMEMBERS、慢 Lua、fork 卡顿
3. **Redis 比 Memcached 快 / 慢在什么地方?**
- 解答要点:Redis 多数据结构、持久化、Lua、Pub/Sub 带来更多开销
4. **单线程模型如何保证高并发下不 OOM?**
- 解答要点:内存淘汰策略、maxmemory、Cluster 分片
5. **如果 Redis CPU 单核打满,你怎么优化?**
- 解答要点:慢查询日志 → 拆分大 key → Cluster 分片 → 6.0 多线程参数

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,Redis 快我觉得首先要区分清楚是"快在哪一层",我一般从三个维度讲。

架构层,Redis 默认是单线程模型处理所有命令,这样避免了多线程的锁竞争和上下文切换。同时它依赖 epoll 做 I/O 多路复用,一个线程能同时 hold 住上万连接。这也是 Redis 6.0 之前 CPU 单核就是瓶颈的原因,6.0 之后把 socket 读写和协议解析放到多线程,但命令执行依然保持单线程。

数据层,数据全部驻留内存,天然就比磁盘 I/O 的 MySQL 快。而且数据结构是自研的,Hash 底层用哈希表 O(1) 查找;字符串用 SDS,O(1) 取长度;小规模的 list/hash/zset 用 listpack 紧凑存储。

系统层,内存分配用的是 jemalloc,减少碎片;RDB 持久化用 fork 加 COW 写时复制,主线程几乎不阻塞。

相比之下 MySQL 慢的原因是多维的:主要是磁盘 I/O,再加上多线程并发带来锁竞争、SQL 解析优化、事务日志落盘,不只是内存 vs 磁盘的差别。

工程上我实际踩过两个坑:一是 DEL 一个大 hash 卡住单线程,最后换成 UNLINK;二是 Cluster 分片之前没做数据倾斜分析,单节点 QPS 打满。所以理解 Redis 快,本质是理解它做的一整套"少锁、少系统调用、少内存分配、少磁盘操作"的取舍。

#2 什么是缓存击穿、缓存穿透、缓存雪崩 low redis

题目

什么是缓存击穿、缓存穿透、缓存雪崩

我的回答

(原文见题目文件面试记录)

点评

评分:⭐⭐⭐⭐☆(4/5)
优点:三概念定义清晰;穿透→攻击手段定性到位;方案有层次(布隆过滤器→空值缓存→风控)
不足:击穿漏了"逻辑过期+异步刷新"方案;穿透未提布隆过滤器动态更新;雪崩漏了主从高可用、多级缓存
补充:击穿三档方案、穿透布隆过滤器动态更新、雪崩多层防御 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:三种问题的根因不同(单点失效 vs 无效数据 vs 批量失效),防御手段也必须差异化。

**知识点拆解**:

1. **缓存击穿(Cache Breakdown)—— 单热点 key 失效**
- 场景:某个热点 key 突然过期,瞬间成千上万请求全部穿透到 DB
- 根因:单点 + 并发 + 过期
- 方案分层:
- 互斥锁重建:SET key_lock NX PX 10000,拿到锁的线程查 DB 并回填
- 逻辑过期:值结构里存 expireTime,物理 TTL 设为永不/超长;发现过期后异步重建
- 永不过期 + 后台刷新:定时任务提前刷新热点数据

2. **缓存穿透(Cache Penetration)—— 无效 key 一直不缓存**
- 场景:请求一个数据库中根本不存在的 key,每次穿透到 DB
- 根因:数据不存在 → 无 key 可存 → 无缓存可查
- 方案分层:
- 空值缓存:DB 查不到就缓存 null,TTL 短;缺点:恶意请求可撑爆缓存
- 布隆过滤器:预加载所有有效 ID,误判率可控(推荐 1%),但新增数据要同步更新
- 入口校验:参数校验、鉴权、限流
- 风控兜底:同一 IP 高频无效请求 → 封禁

3. **缓存雪崩(Cache Avalanche)—— 批量失效 / Redis 挂**
- 场景:大批 key 同时过期,或 Redis 集群整体不可用
- 根因:批量 + 基础设施故障
- 方案分层:
- TTL 加随机抖动:TTL = base + random(0, delta)
- Redis 高可用:主从 + Sentinel / Cluster
- 多级缓存:本地缓存 + Redis + DB
- 限流熔断:Sentinel / Hystrix

**三者的组合防御**:参数校验 → 布隆过滤器 → 本地缓存 → Redis 查询(命中/互斥锁重建/空值缓存)→ 缓存重建(TTL 加随机)→ DB 查询(限流熔断兜底)

第二部分:考察方向与延伸递进

**考察方向**:
- 深度:布隆过滤器如何动态更新?逻辑过期的时钟漂移问题?
- 广度:与数据库限流、CDN、多级缓存体系如何协同
- 应用:电商秒杀、社交 Feed 流、内容详情页的实际方案选型

**延伸递进面试题**:

1. **布隆过滤器不支持删除怎么办?数据新增如何同步?**
- 解答要点:Count-Min Sketch 支持计数减一;新增数据写入时同步 hash 进过滤器;或用 RedissonBloom 支持动态扩容
2. **逻辑过期如何避免时钟漂移导致大量过期?**
- 解答要点:用 Redis 服务端时间;或改用 Redis TTL 但配合互斥锁
3. **多级缓存的一致性 问题怎么解决?**
- 解答要点:Cache-Aside + 发布订阅;本地缓存 TTL 极短(秒级);业务容忍短暂不一致
4. **秒杀场景如何设计缓存防御体系?**
- 解答要点:预热库存到 Redis + Lua 原子扣减 + 布隆过滤 + 消息削峰 + 空单降级
5. **Redis 集群整体宕机,你的系统如何优雅降级?**
- 解答要点:熔断打开 + 静态兜底数据 + 排队限流 + 前端提示

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,这三种问题本质是失效模式不同:击穿是单点、穿透是无效、雪崩是批量。

缓存击穿是热点 key 过期,并发全部穿透。我用三档方案:互斥锁重建(SETNX + 短超时,其他线程短轮询读缓存)适合中等并发;逻辑过期(值里存 expireTime,物理 TTL 永不过期)适合高并发且容忍读旧;永过期 + 后台定时刷新适合核心热点数据。选哪个取决于能否接受读旧数据。

缓存穿透是请求不存在的 key,我用分层防御:入口参数校验先拦非法 ID;布隆过滤器预加载所有有效 ID,误判率控制到 1% 以内;对布隆漏网的,缓存空值 30 秒兜底。布隆有静态限制,我们定期全量重建,新增数据走 Count-Min Sketch 或 RedissonBloom 动态扩容。极端情况走风控封 IP。

缓存雪崩是批量失效或 Redis 挂,我用组合方案:TTL 加随机抖动避免同时过期;主从 + Sentinel 保证 Redis 可用;多级缓存(Caffeine + Redis)作为兜底;DB 侧接限流熔断,Sentinel 规则兜底。上线前做容量压测,确定临界值。

实际工程里这三种问题常同时出现,比如电商秒杀:热点商品 key 过期(击穿)+ 恶意刷不存在的 SKU(穿透)+ Redis 节点宕机(雪崩)。所以防御体系要分层叠加,而不是单选一种方案。

#3 什么是线程池,如何实现的? low java-concurrency

题目

什么是线程池,如何实现的?

我的回答

(原文见题目文件面试记录)

点评

评分:⭐⭐⭐☆☆(3/5)
优点:池化思想正确;核心参数关系顺序讲对了
不足:参数不全(漏 keepAliveTime、threadFactory);拒绝策略未给类名;未提队列类型、Executors 禁用
补充:7 大核心参数;执行流程三关判断;4 种拒绝策略类名;Executors OOM 风险 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 ThreadPoolExecutor 的参数体系、执行流程、工程选型的全链路掌握。

**知识点拆解**:

1. **池化思想**:线程创建成本(内核态切换、栈内存、GC 压力);池化收益 = 复用 + 限流 + 隔离
2. **7 大核心参数**:corePoolSize / maximumPoolSize / keepAliveTime / unit / workQueue / threadFactory / handler
3. **执行流程**(三步判断):core → 队列未满 → max → 拒绝策略;队列优先级高于 max 扩容
4. **4 种拒绝策略**:AbortPolicy(默认抛异常)、DiscardPolicy(静默丢弃)、DiscardOldestPolicy(丢队列头)、CallerRunsPolicy(调用者执行,天然限流)
5. **4 种队列**:ArrayBlockingQueue(有界)、LinkedBlockingQueue(可无界 OOM 风险)、SynchronousQueue(不存储)、PriorityBlockingQueue(优先级)
6. **5 态**:RUNNING → SHUTDOWN → STOP → TIDYING → TERMINATED
7. **Executors 禁用**:FixedThreadPool/SingleThreadExecutor 用无界队列 OOM;CachedThreadPool 无 max 线程暴涨;ScheduledThreadPool 无界队列
8. **参数估算**:CPU 密集 N_cpu×1;IO 密集 N_cpu×2 或 N_cpu×(1+wait/compute)
9. **监控**:JMX MBean 暴露 queueSize/activeCount/completedTaskCount/rejectedCount

第二部分:考察方向与延伸递进

**考察方向**:
- 深度:execute() 源码执行路径,为什么队列优先级高于 max?
- 广度:与 ForkJoinPool / 虚拟线程对比
- 应用:线程暴涨排查、队列积压排查、动态调优

**延伸递进面试题**:

1. **ThreadPoolExecutor.execute 的源码流程?**
- 解答要点:三关判断(core → queue → max),CAS 修改 workerCount,addWorker 创建线程
2. **为什么队列满了才扩非核心线程?**
- 解答要点:避免频繁创建销毁线程,队列作为缓冲池;代价是延迟升高
3. **线程暴涨如何排查?**
- 解答要点:jstack 抓栈 → 看线程名 → 定位提交方 → 看 queue 是否无界
4. **ForkJoinPool 和 ThreadPoolExecutor 区别?**
- 解答要点:ForkJoin 用工作窃取,无界任务队列,适合并行任务;TP 适合独立任务
5. **JDK 21 虚拟线程能替代线程池吗?**
- 解答要点:虚拟线程 M:N 调度,阻塞时释放载体线程;CPU 密集仍需池;不适合 synchronized 大量竞争
6. **动态线程池如何落地?**
- 解答要点:阿里 Hippo4j / 美团动态线程池;配置中心推送 + JMX 回调 + 告警

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,线程池本质是池化思想,核心目标是复用线程 + 限流 + 隔离。

参数方面有 7 个:corePoolSize 常驻线程数、maximumPoolSize 最大线程数、keepAliveTime 非核心线程空闲回收时间、unit 时间单位、workQueue 阻塞队列、threadFactory 线程工厂(一定要给线程命名,方便排查)、handler 拒绝策略。

执行流程是三步判断:任务进来先看当前线程数是否小于 core,是就建核心线程;否则看队列是否满,不满入队;否则看是否小于 max,是就建非核心线程;都不行触发拒绝策略。注意队列优先级高于 max 扩容,这是很多人讲错的地方。

拒绝策略 4 种内置:AbortPolicy 抛异常(默认)、DiscardPolicy 静默丢弃、DiscardOldestPolicy 丢队列头重试、CallerRunsPolicy 调用者自己执行——这个我比较常用,天然限流让上游感受慢。

队列选择我一般看场景:CPU 密集用 SynchronousQueue 或无队列,max 拉高;IO 密集用 ArrayBlockingQueue 有界队列控内存;任务有优先级用 PriorityBlockingQueue。

我们项目里禁用 Executors,因为它内部用无界队列或无上限线程,容易 OOM。参数估算用 N_cpu × 2(IO 密集)做初值,再压测调优。动态调优接了配置中心,运行时改 corePoolSize / maximumPoolSize,配 JMX 监控 activeCount、queueSize、rejectedCount 告警。

我们踩过的坑是线程暴涨:某个下游变慢,队列积压,线程飙到几千,最后 JVM OOM。定位靠 threadFactory 命名的线程名 + jstack 抓栈。之后改成有界队列 + CallerRunsPolicy,队列满后直接压回上游,问题就解决了。

---

#4 Redis 中的 setnx 命令为什么是原子性的? medium redis

题目

Redis 中的 setnx 命令为什么是原子性的?

我的回答

(原文见题目文件面试记录)

点评

评分:⭐⭐⭐☆☆(3/5)
优点:核心逻辑对(单线程→串行→原子);主动想到 Lua 也是原子
不足:混淆 SETNX 和 SET NX EX;未展开机制(事件循环、epoll);未对比 MySQL 原子性来源
补充:原子性三层(命令/脚本/事务);Redis 6.0 多线程边界;SETNX vs SET NX EX 区分 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 Redis 原子性来源的架构级理解,能否区分"命令原子性"、"事务原子性"、"MySQL 事务原子性"的本质差异。

**知识点拆解**:

1. **SETNX 命令本身**:Set if Not eXists,单命令原子操作;带过期用 SET NX EX 复合命令
2. **原子性来源三层**:命令层(事件循环串行)、脚本层(Lua 一次执行)、事务层(MULTI/EXEC 无回滚)
3. **单线程模型机制**:aeMain 事件循环 → 从命令队列取 → processCommandAndResetClient 同步执行;I/O 多路复用解决并发连接,非并发执行
4. **Redis 6.0 多线程边界**:多线程做 socket read/write + 协议解析,命令执行仍主线程
5. **对比 MySQL**:Redis 靠架构(单线程),MySQL 靠协议(undo log + 锁 + MVCC)
6. **工程实践**:分布式锁用 SET NX EX + Lua 解锁 + Redisson watchdog 续期
7. **Redis 事务坑**:无回滚、无隔离、推荐 Lua 替代 MULTI

第二部分:考察方向与延伸递进

**考察方向**:
- 深度:Redis 6.0 多线程后原子性还成立吗?事件循环如何调度?
- 广度:与 MySQL / MongoDB / Kafka 事务原子性模型对比
- 应用:分布式锁、限流、计数器场景下的原子性使用

**延伸递进面试题**:

1. **Redis 6.0 多线程后原子性还成立吗?**
- 解答要点:多线程只做网络 I/O 和协议解析,命令执行仍在主线程串行
2. **MULTI/EXEC 事务和 Lua 脚本怎么选?**
- 解答要点:Lua 更灵活,推荐替代 MULTI;MULTI 用于简单批量
3. **如何用 SETNX 实现分布式锁?有哪些坑?**
- 解答要点:SET NX EX 原子;锁过期时间 > 业务耗时;加锁带唯一标识防误删;unlock 用 Lua;watchdog 续期
4. **Redis 的事务有隔离性吗?**
- 解答要点:无隔离,MULTI 后其他客户端命令仍可执行;无回滚
5. **Redis 为什么不用锁实现并发控制?**
- 解答要点:单线程天然无竞争,加锁反而增加复杂度

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,SETNX 是 Redis 内置的单条命令,意思是"Set if Not eXists"——不存在才设置。它的原子性不是靠锁实现的,而是靠 Redis 的架构本身。

核心机制:Redis 主线程运行事件循环,同一时刻只处理一个命令,命令在 processCommand 中同步执行,不会与下一条命令交错。所以单命令天然原子。I/O 多路复用(epoll)解决的是并发连接,不是并发执行——一个线程能 hold 上万连接,但每次只执行一个命令。

Redis 6.0 多线程边界要分清:多线程只做 socket 读写和协议解析,命令执行仍在主线程。所以原子性依然成立。

别混淆:SETNX key value 是单命令;带过期时间是 SET key value NX EX seconds,是复合原子命令。分布式锁必须用后者,避免死锁。

原子性有三层:命令层(单命令)、脚本层(Lua 一次性执行)、事务层(MULTI/EXEC 批量入队,EXEC 顺序执行)。但 MULTI 无回滚、无隔离,中间失败继续执行,所以生产环境推荐用 Lua 脚本替代 MULTI。

对比 MySQL:MySQL 事务原子性靠 undo log + 锁 + MVCC,是协议级保证,粒度是事务;Redis 是架构级保证,粒度是命令/脚本。所以 Redis 简单得多,但灵活性也差一些。

工程上我用 SETNX 做限流和分布式锁。锁推荐 SET NX EX 原子加锁 + Lua 脚本解锁(判断 value 是否是自己的锁)+ Redisson watchdog 自动续期,避免业务卡住导致锁过期。踩过的坑是把加锁和设置过期写成两步,中间崩溃造成死锁,后来统一改成 SET NX EX 一条命令解决。

#5 Redis 的过期策略是怎么样的? medium redis

题目

Redis 的过期策略是怎么样的?

我的回答

(原文见题目文件面试记录)

点评

评分:⭐⭐☆☆☆(2/5)
优点:抓住惰性过期 + 周期性扫描两个关键
不足:概念错误(说有独立线程扫描,实际是主线程 serverCron);遗漏采样细节(20 个 key、25% 阈值、25ms 上限);未解释设计权衡;混入内存淘汰策略
补充:惰性删除 + 定期删除组合;serverCron 主线程定时任务;采样 20 个过期 key 过期比例 >25% 继续;单次最多 25ms ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 Redis 过期键删除机制的深入理解,尤其要区分"过期策略"(Expiration Policy)和"内存淘汰策略"(Eviction Policy)。

**知识点拆解**:

1. **术语先分清**:过期策略(如何处理到达 TTL 的 key,两种机制组合)vs 内存淘汰策略(内存达 maxmemory 时如何腾空间,8 种策略)
2. **过期策略两大机制**:
- 惰性删除:访问 key 时(lookupKey 阶段)判断 expire < now,过期立即删;优点是零开销,缺点是冷门 key 永占内存
- 定期删除:主线程 serverCron(每秒 10 次)触发 activeExpireCycle,每次从 expires 字典随机抽 20 个有过期时间的 key,过期比例 ≤25% 结束本轮,>25% 继续扫,单次最多 25ms
3. **关键澄清**:不是独立线程,是主事件循环里的定时任务;采样 + 时间上限保证不阻塞
4. **设计权衡**:全扫会阻塞命令,纯惰性冷门 key 堆积;Redis 方案是 CPU 与内存的折中
5. **实现细节**:expires 字典与 db 主字典分离;EXPIRE/PEXPIRE/SET EX 写 expires;命令执行前后都可能触发 activeExpireCycle
6. **对比其他方案**:主动删除(高 CPU 低内存)、惰性删除(零 CPU 高内存)、Redis 组合(中 CPU 中内存)
7. **工程实践**:监控 expired_keys/evicted_keys;TTL 加随机抖动防雪崩;冷门 key 缩短 TTL
8. **常见误区**:过期 key 不一定立即删除;Redis 没有专门的过期线程;设置 EXPIRE 不一定按时删

第二部分:考察方向与延伸递进

**考察方向**:
- 深度:activeExpireCycle 源码逻辑?采样参数如何调优?
- 广度:与 Memcached 过期策略对比;与 Kafka 消息过期对比
- 应用:过期 key 堆积排查、雪崩预警、maxmemory-policy 与过期策略协同

**延伸递进面试题**:

1. **activeExpireCycle 的具体逻辑?**
- 解答要点:采样 20 个过期 key,过期比例 >25% 继续,单次最多 25ms;serverCron 每秒 10 次 + 命令前后触发
2. **如何判断过期 key 堆积?**
- 解答要点:INFO keyspace 看 expired_keys;MEMORY USAGE 看内存;DBSIZE 与真实存活数对比
3. **过期 key 会立即删除吗?**
- 解答要点:不保证,惰性 + 定期组合,采样有遗漏
4. **过期策略和内存淘汰策略的关系?**
- 解答要点:过期是 TTL 驱动的删除,淘汰是内存驱动的选择性删除;两者独立但互补
5. **大量 key 同时到期会怎样?**
- 解答要点:惊群,命令阻塞;TTL 加随机抖动预防
6. **Redis 7.0 对过期清理有什么优化?**
- 解答要点:过期清理 CPU 时间控制更精细;activeExpireCycle 支持按数据库独立触发

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,Redis 的过期策略其实是两个机制的组合:惰性删除 + 定期删除,别和内存淘汰策略混淆,那是另一个话题。

惰性删除是在 key 被访问时(lookupKey 阶段)判断 expire < now,过期就立即删。好处是零开销,坏处是冷门 key 永远不会被访问,内存一直占着。

定期删除是主线程的 serverCron 定时任务,每秒约 10 次触发 activeExpireCycle,每次从过期字典随机抽 20 个 key,统计这批里实际过期的比例:过期比例 ≤ 25% 就结束本轮(说明整体过期不多),> 25% 就继续扫,直到 CPU 时间超过 25ms 为止。这样既不阻塞主线程,又能控制过期 key 的堆积。

这里有个常见误区:很多人以为 Redis 有专门的过期线程,其实没有——单线程模型下,过期扫描是主事件循环里的定时任务,通过采样和时间限制来保证不阻塞命令。

为什么不用主动全扫?因为 Redis 单线程,全扫会阻塞所有命令。为什么不纯惰性?因为冷门 key 会堆积内存。所以是 CPU 与内存的折中。

实现细节上,过期信息存在独立的 expires 字典里,和 db 主字典分离。业务上我做过两个坑:一是把大量 key 的 TTL 设成一样(都是 24h),到点集体过期,触发缓存雪崩,后来改成 TTL + random(0, 3600) 随机抖动;二是发现 expired_keys 一直不涨但内存还在增,查下来是大量冷门 key 从没被访问,惰性删除没触发,缩短 TTL 就好了。

监控看 INFO keyspace 的 expired_keys 和 evicted_keys,前者是过期删除,后者是内存淘汰,两个数字要分开看。

#6 Redis 的内存淘汰策略是怎么样的? medium redis

题目

Redis 的内存淘汰策略是怎么样的?

我的回答

(原文见题目文件面试记录)

点评

评分:⭐⭐⭐⭐☆(4/5)
优点:框架正确(过期时间维度 × 算法维度);知道默认 noeviction;区分读写触发
不足:未说出策略具体名称;未提 LRU 是近似、LFU 是 4.0+;未给工程建议
补充:8 种策略完整列表;LRU 采样 maxmemory-samples;LFU 15-bit 计数器 + 时间衰减;选型建议 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 maxmemory-policy 8 种策略的正交矩阵理解,以及 LRU/LFU 底层实现。

**知识点拆解**:

1. **术语区分**:过期策略(TTL 到期删除 key,与内存无关)vs 内存淘汰策略(内存达 maxmemory 时腾空间,只在写操作触发)
2. **8 种策略矩阵**(两个正交维度):
- 范围维度:allkeys-*(无差别淘汰)/ volatile-*(只在有 EXPIRE 的 key 里淘汰)
- 算法维度:noeviction(默认,写失败)/ random / lru / lfu(4.0+)/ ttl(剩余 TTL 最小)
- 组合成 8 种:allkeys-lru、allkeys-lfu、allkeys-random、volatile-lru、volatile-lfu、volatile-random、volatile-ttl、noeviction
3. **LRU 是「近似 LRU」**:维护链表 + Hash 开销大;采样淘汰,随机抽 maxmemory-samples(默认 5)个候选选最老,误差约 1%
4. **LFU 是「近似 LFU」——Redis 4.0+**:15-bit 计数器记录访问频率,默认 10 秒衰减一次;优势是防新数据挤走真正热点
5. **noeviction 的坑**:默认策略,内存满后所有写返回 OOM 错误;生产必须显式配置
6. **实现机制**:每次写命令前检查 used_memory > maxmemory;淘汰与过期是两个独立机制
7. **工程实践**:缓存场景 allkeys-lru(通用);热点长尾 allkeys-lfu;保留数据 volatile-lru
8. **常见误区**:LRU 是严格 LRU(错,是采样);LFU 3.0 就有(错,4.0);淘汰和过期是一回事(错);默认 allkeys-lru(错,是 noeviction)

第二部分:考察方向与延伸递进

**考察方向**:
- 深度:activeExpireCycle 采样逻辑?maxmemory-samples 调优?
- 广度:与 Memcached(无淘汰策略,只做 eviction allkeys-lru)对比
- 应用:热点分布长尾场景、缓存雪崩预警、evicted_keys 告警

**延伸递进面试题**:

1. **LRU 的采样淘汰为什么不是严格 LRU?**
- 解答要点:维护链表 + Hash 开销大;采样误差可控(默认 5 个误差 ~1%)
2. **LFU 的时间衰减如何实现?**
- 解答要点:15-bit 计数器,10 秒衰减周期,lfu-log-freq-factor 调参
3. **volatile-lru 和 allkeys-lru 怎么选?**
- 解答要点:有 TTL 的 key 占比高用 volatile-lru;混合场景用 allkeys-lru
4. **evicted_keys 突增如何排查?**
- 解答要点:INFO memory 看 used_memory vs maxmemory;INFO stats 看 evicted_keys 速率;确认是策略主动淘汰还是内存爆表
5. **Redis 7.0 对淘汰策略有什么优化?**
- 解答要点:LFU 衰减优化;淘汰逻辑与过期逻辑解耦

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,Redis 内存淘汰策略是两个维度的正交组合:一个是范围(全部 key 还是只有有过期时间的 key),一个是算法(无 / 随机 / LRU / LFU / TTL)。加起来共 8 种。

范围维度:allkeys-* 无差别淘汰;volatile-* 只在设置了 EXPIRE 的 key 里淘汰。

算法维度:noeviction(默认)内存满直接写失败,返回 OOM 错误;random 随机删;lru 近似 LRU,采样 5 个候选选最老;lfu 近似 LFU,Redis 4.0+,基于 15-bit 计数器 + 时间衰减;ttl 优先淘汰剩余 TTL 最小的。

两个维度组合成 8 种:allkeys-lru、allkeys-lfu、allkeys-random、volatile-lru、volatile-lfu、volatile-random、volatile-ttl、noeviction。

LRU 是近似的,不是严格 LRU。严格 LRU 要维护双向链表和 Hash,开销大。Redis 用采样淘汰,随机抽 5 个候选,误差约 1%。可以调 maxmemory-samples 提升精度。

LFU 是 Redis 4.0 新增,适合热点分布长尾的场景。用 15-bit 计数器记录访问频率,默认 10 秒衰减一次,防止新数据挤走真正的热点。

noeviction 是默认策略,内存满直接返回错误。生产环境必须显式配置,不能让它扛。

我们项目用 allkeys-lru,因为业务里大部分 key 都有 TTL,且热点分布比较均匀。监控 INFO stats 的 evicted_keys,一旦突增就排查是不是 maxmemory 设小了或者数据量暴涨。

踩过一个坑:某次上线后 evicted_keys 每分钟涨几千,查下来是新版本把某些大 key 的 TTL 从 24h 改成了永不过期,导致 volatile-* 策略失效,触发全量淘汰。改回 TTL 就好了。所以淘汰策略 + TTL 设计要配套看,不能只改一边。

#7 什么情况下会出现数据库和缓存不一致的问题? medium redis

题目

什么情况下会出现数据库和缓存不一致的问题?

我的回答

(原文见题目文件面试记录)

点评

评分:⭐⭐⭐⭐☆(4/5)
优点:核心根因(非原子性+并发+局部失败)抓得准;主动提到延迟双删、binlog 消费、主从延迟
不足:未系统列举 6 大场景;未解释删除优于更新;未提 Cache-Aside 模式名;强一致方案未展开
补充:6 大不一致场景;Cache-Aside 模式;延迟双删 500ms-1s;Binlog+MQ/CDC/事务性消息 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对不一致根因的枚举能力 + 解决方案的层次选型能力,而非只答一个"延迟双删"。

**知识点拆解**:

1. **不一致的 6 大场景**:
- 更新 DB 后删除缓存失败:脏数据驻留到 TTL
- 删除缓存后更新 DB 失败:下次读回填旧值
- 并发 race:读线程读旧值 → 写线程更新 DB 删缓存 → 读线程把旧值写回缓存
- 先删缓存再更新 DB:删除后并发读回填旧值
- 先更新缓存再更新 DB:DB 更新失败,缓存是新值 DB 是旧值
- 主从延迟:主库写完从库未同步,读走从库拿旧值

2. **三大经典模式对比**:
- Cache-Aside(推荐):先读缓存 miss 读 DB 回填;更新 DB → 删缓存
- Read-Through:缓存 miss 由缓存层回填;更新缓存 → 缓存层写 DB
- Write-Through:同步写缓存 + DB
- Write-Behind:异步批量写 DB

3. **为什么"删除缓存"优于"更新缓存"**:更新缓存在并发写时会有旧值覆盖新值的 race,删除让下次读自然回填最新值

4. **延迟双删正确姿势**:更新 DB → 删缓存 → 延迟 500ms-1s(覆盖主从同步周期)→ 再删一次;实现用线程池延迟任务或 MQ 延迟消息

5. **强一致性方案**(按侵入性从小到大):
- Binlog 订阅(Canal):消费 binlog → MQ → 删缓存,业务无侵入(最推荐)
- CDC 工具:Debezium / Flink CDC
- 事务性消息:RocketMQ 半消息
- 分布式事务:Seata AT/TCC,侵入大不推荐
- 锁住整个流程:性能差

6. **兜底策略**:TTL 是最终一致性的安全网;监控不一致率(定时抽样对比)

7. **工程实践清单**:
- 关键数据(订单、余额):Binlog + MQ 强一致
- 一般数据(商品、配置):Cache-Aside + 延迟双删 + TTL
- 只读数据:Read-Through + 长 TTL
- 高频写场景:Write-Through

第二部分:考察方向与延伸递进

**考察方向**:
- 深度:Canal 如何保证不丢消息?事务性消息的回查机制?
- 广度:与 Kafka Exactly-Once、Seata TCC、分布式锁对比
- 应用:订单、库存、账户余额等场景的方案选型

**延伸递进面试题**:

1. **Canal 如何保证不丢消息?**
- 解答要点:offset 落盘 + 断点续传 + MQ 至少一次投递
2. **延迟双删的 N 值怎么定?**
- 解答要点:主从延迟监控 P99 + 业务读放大系数;经验 500ms-1s
3. **读写分离下如何避免读到旧值?**
- 解答要点:强制走主库读(有写会话);session sticky;读前等待 binlog 同步
4. **Seata AT 模式原理?为什么侵入小?**
- 解答要点:undo_log + 全局锁 + 一阶段本地事务二阶段补偿
5. **如何监控缓存不一致率?**
- 解答要点:定时抽样对比 DB 与缓存;埋点上报不一致次数;阈值告警
6. **库存扣减场景如何保证一致性?**
- 解答要点:Redis Lua 原子扣减 + MQ 事务消息 + DB 最终落库 + 对账补偿

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,DB 与缓存不一致的根因是非原子性 + 并发,具体有 6 类场景:

场景 1:更新 DB 后删缓存失败,脏数据驻留到 TTL。场景 2:删缓存后更新 DB 失败,下次读回填旧值。场景 3:并发 race——读线程读到旧值,写线程更新 DB 删缓存,读线程把旧值写回缓存,脏数据长期驻留。场景 4:先删缓存再更新 DB,删除后并发读回填旧值。场景 5:先更新缓存再更新 DB,DB 失败则缓存是新值 DB 是旧值。场景 6:主从延迟,主库写完从库未同步,读走从库拿旧值。

模式选型:我们通用场景用 Cache-Aside(读时回填、写时删缓存),删除比更新安全——并发写时更新缓存会有旧值覆盖新值的 race,删除让下次读自然回填最新值。

延迟双删:更新 DB → 删缓存 → 延迟 500ms-1s(覆盖一次主从同步周期)→ 再删一次,清掉并发读回填的脏值。实现用线程池延迟任务或 MQ 延迟消息。

强一致场景(订单、余额):用 Binlog 订阅(Canal)消费 binlog → MQ → 删缓存,业务无侵入;或者用 RocketMQ 事务性消息,DB 事务与消息发送原子;Seata AT/TCC 侵入大不推荐。

兜底:TTL 是最终一致性的安全网,业务可容忍的脏数据时间窗(订单 5min、商品详情 1h)。配合不一致率监控:定时抽样对比 DB 与缓存,超阈值告警。

实际项目我做过一个坑:一开始只用了"更新 DB + 删缓存",压测发现偶发脏数据。查下来是主从延迟导致读从库拿旧值。加了延迟双删(延迟 1s)后问题解决。后来核心订单场景改成了 Canal + MQ 强一致方案。

#8 Redis 为什么被设计成是单线程的? high redis

题目

Redis 为什么被设计成是单线程的?

我的回答

(原文见题目文件面试记录)

点评

评分:⭐⭐⭐⭐⭐(5/5)👏
优点:核心洞察到位(IO 密集而非 CPU 密集);成本-收益分析清晰;"越简单越稳定"哲学;主动提到 6.0 演进
不足:未提 epoll/kqueue 技术支撑;未提无锁收益;未对比 MySQL 为什么需要多线程
补充:I/O 多路复用是单线程高并发唯一基础;无锁/无 CAS/无上下文切换;6.0 命令执行仍单线程 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 Redis 单线程设计的架构权衡深度理解,能否跳出"单线程 = 慢"的直觉误区。

**知识点拆解**:

1. **核心逻辑——瓶颈在哪?**
- Redis 是内存 KV 存储,命令执行极其轻量:网络接收 → 解析 → 内存查找 O(1) → 网络返回
- 瓶颈在网络 I/O,不在 CPU;单核 CPU 就能处理数万 QPS
- 对比 MySQL:SQL 解析/优化器/执行器(CPU 密集)+ 事务隔离 + 锁竞争 + 磁盘 I/O → 必须多线程

2. **单线程的收益**:无锁(无 CAS/死锁/内存屏障)、无上下文切换、天然原子性、稳定性、可预测性、GC 无关

3. **技术支撑——I/O 多路复用**:epoll/kqueue 事件循环,一个线程 hold 上万连接,时间复杂度 O(1)

4. **Redis 6.0 多线程边界**:
- Socket 读写、协议解析:多线程(可配 io-threads 2-4)
- 命令执行:主线程(processCommandAndResetClient)
- 持久化 fork:子进程 + COW
- AOF fsync:子线程
- 为什么命令执行不多线程?引入锁开销抵消收益;命令本身轻;单线程保证原子性

5. **单线程的瓶颈与扩展**:瓶颈是单核 CPU 打满;正确解法是 Cluster 分片,不要"让单节点变多线程"

6. **何时绕过单线程瓶颈**:Cluster 分片 / 6.0 多线程 / 慢命令优化(UNLINK/SCAN)/ Lua 拆分

7. **与其他 KV 存储对比**:Redis(单线程 + epoll)vs Memcached(一连接一线程)vs RocksDB(多线程 LSM)vs etcd(单线程 Raft)

8. **工程实践**:避免慢命令(KEYS→SCAN,大 DEL→UNLINK);拆分大 key;Cluster 分片;监控 SLOWLOG;6.0 参数 io-threads 2-4

第二部分:考察方向与延伸递进

**考察方向**:
- 深度:aeMain 事件循环源码?为什么 6.0 命令执行不多线程?
- 广度:与 Netty / Nginx / Kafka 的 I/O 模型对比
- 应用:单核 CPU 打满如何排查?大 key 阻塞如何规避?

**延伸递进面试题**:

1. **Redis 6.0 引入多线程为什么命令执行仍是单线程?**
- 解答要点:引入锁的开销抵消收益;命令本身轻;单线程保证原子性
2. **Redis 的事件循环(aeMain)如何调度?**
- 解答要点:epoll 监听 I/O 事件 → 时间事件 serverCron → 命令队列处理;命令执行同步阻塞
3. **单核 CPU 打满如何排查?**
- 解答要点:SLOWLOG 看慢命令 → INFO commandstats 看命令耗时分布 → 拆大 key / Cluster 分片
4. **Redis 的慢命令有哪些?**
- 解答要点:KEYS、SORT、SRANDMEMBER(大集合)、大 HGETALL/SMEMBERS、LPOP 大列表、Lua 脚本
5. **为什么 Memcached 用多线程而 Redis 不用?**
- 解答要点:Memcached 早期设计,一连接一线程;Redis 用 epoll 单线程更优
6. **如果让你设计一个类似 Redis 的系统,会怎么选?**
- 解答要点:命令轻 → 单线程 + I/O 多路复用;命令重 → 多线程 + 锁;磁盘密集 → 异步 IO / io_uring

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,Redis 单线程的设计本质是架构权衡,核心判断是瓶颈在哪。

Redis 是内存 KV 存储,命令执行极其轻量——一次 GET 就是网络接收 → 解析 → 内存 O(1) 查找 → 网络返回,瓶颈在网络 I/O,不在 CPU。单核 CPU 就能处理数万 QPS,多核收益很低。

而多线程带来的成本很高:锁竞争、上下文切换、并发 bug、延迟抖动。单线程天然无锁、无死锁、无内存屏障、无 GC 暂停,稳定性极高,代码也简单。

技术支撑是关键——Redis 用 epoll/kqueue 做 I/O 多路复用,一个线程通过事件循环就能 hold 住上万连接,事件就绪才处理,时间复杂度 O(1)。这是单线程高并发的唯一基础。

Redis 6.0 引入多线程的边界要分清:多线程只做 socket 读写 + 协议解析,命令执行 processCommandAndResetClient 仍是主线程。因为命令本身轻,多线程收益低,引入锁开销抵消收益,而且单线程保证原子性语义不变。

对比 MySQL:MySQL 有 SQL 解析、优化器、执行器(CPU 密集)+ 事务隔离 + 锁竞争 + 磁盘 I/O(并发等待),所以必须多线程。Redis 命令简单,单线程足够。

单线程的瓶颈是单核 CPU 打满,正确解法是 Cluster 分片水平扩展,而不是让单节点变多线程。工程上我做过两个坑:一是 KEYS 命令阻塞主线程,改用 SCAN;二是大 key DEL 卡住,换 UNLINK 异步删除。

所以总结:Redis 单线程是基于"命令轻 + 网络密集"的最优架构选择,简单、稳定、可预测;瓶颈用分片扩展,而不是破坏单线程模型。
---

#9 如何进行线程池调优? high system-design

题目

如何进行线程池调优?

我的回答

(原文见题目文件面试记录)

点评

评分:⭐⭐⭐⭐⭐(5/5)👏
优点:5 步调优方法论完整;QPS×执行时间公式工程化;给了具体数字例子;场景化拒绝策略;ThreadFactory 命名素养高
不足:未提动态调优(配置中心+JMX);未提监控指标;未提业务隔离(Bulkhead);未提 Executors 禁用
补充:动态调优 Hippo4j;监控 queueSize/rejectedCount;Bulkhead 舱壁模式 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察线程池调优的方法论(而非背公式)+ 工程落地能力(监控、动态调优、隔离)。

**知识点拆解**:

1. **调优方法论 5 步走**:
- ① 看清任务类型 → 定 core
- ② 估算 QPS × 执行时间 → 定 queue 长度
- ③ max = core × 1.5-2 → 抗波动
- ④ keepAliveTime = 峰值时长 × 系数 → 抗波峰
- ⑤ 拒绝策略 = 业务容忍度 → 抛异常/静默/CallerRuns

2. **core 参数估算**:CPU 密集 N_cpu+1(加 1 利用上下文切换间隙);IO 密集 N_cpu × (1 + waitTime/computeTime),比 2N 精细

3. **queue 长度估算**:公式 QPS × 单任务执行时间(稳态积压);延迟目标 1-2 秒;有界队列推荐 ArrayBlockingQueue;拒绝池用 SynchronousQueue

4. **max 参数**:一般 core × 1.5-2;注意队列优先于 max 扩容

5. **keepAliveTime**:根据业务峰值持续时间定;短促峰值 30s-1min,持续峰值 5min-10min

6. **拒绝策略场景化**:AbortPolicy(业务可重试)/ DiscardPolicy(日志埋点)/ DiscardOldestPolicy(老任务可丢)/ CallerRunsPolicy(限流上游,推荐)/ 自定义

7. **ThreadFactory 必须命名**:排查靠线程名定位提交方;jstack 抓栈一眼看出归属

8. **禁用 Executors**:FixedThreadPool/SingleThreadExecutor 用 LinkedBlockingQueue 无界 OOM;CachedThreadPool 线程暴涨;ScheduledThreadPool 无界队列

9. **动态调优**:运行时改参数不用重启(setCorePoolSize 等);配置中心集成 Nacos/Apollo + JMX 回调;开源方案 Hippo4j / 美团动态线程池

10. **业务隔离 Bulkhead 舱壁模式**:不同业务不同线程池,避免一个业务打满拖垮全部

11. **监控指标**:poolSize / activeCount / queueSize / completedTaskCount / rejectedCount / taskRunTime;接入 Micrometer + Prometheus

12. **常见坑与排查**:线程暴涨(无界队列+max 无限)/ 队列积压(core 太少或下游慢)/ 频繁拒绝(队列满+max 满)/ OOM(无界队列堆积)/ 延迟高(队列太长)

第二部分:考察方向与延伸递进

**考察方向**:
- 深度:ThreadPoolExecutor.execute 源码执行路径?队列优先级为何高于 max?
- 广度:与 ForkJoinPool / 虚拟线程(JDK 21)对比
- 应用:动态调优落地、业务隔离、压测调优

**延伸递进面试题**:

1. **线程池执行流程(三关判断)?**
- 解答要点:core → 队列未满 → max → 拒绝策略;队列优先级高于 max 扩容
2. **为什么队列满了才扩非核心线程?**
- 解答要点:避免频繁创建销毁线程,队列作为缓冲池;代价是延迟升高
3. **线程暴涨如何排查?**
- 解答要点:jstack 抓栈 → 看线程名 → 定位提交方 → 看 queue 是否无界
4. **ForkJoinPool 和 ThreadPoolExecutor 区别?**
- 解答要点:ForkJoin 用工作窃取,无界任务队列,适合并行任务;TP 适合独立任务
5. **JDK 21 虚拟线程能替代线程池吗?**
- 解答要点:虚拟线程 M:N 调度,阻塞时释放载体线程;CPU 密集仍需池;不适合 synchronized 大量竞争
6. **动态线程池如何落地?**
- 解答要点:Hippo4j / 美团动态线程池;配置中心推送 + JMX 回调 + 告警
7. **如何设计压测方案评估线程池参数?**
- 解答要点:JMeter / wrk 压测;看 queueSize、activeCount、rejectedCount、P99 延迟

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,线程池调优我一般按 5 步走,先看清任务类型,再逐步定参数。

第一步定 core:CPU 密集用 N_cpu + 1(加 1 利用上下文切换间隙);IO 密集用 N_cpu × (1 + waitTime/computeTime),比笼统的 2N 更精细。8 核 CPU 如果 wait=900ms compute=100ms,core ≈ 80。

第二步定 queue:按 QPS × 单任务执行时间估算稳态积压。比如 100 QPS、任务 100ms,队列积压约 10;延迟目标 1-2 秒,队列长度控制在这个范围。默认用 ArrayBlockingQueue 有界队列,避免无界 OOM。

第三步定 max:一般 core × 1.5-2,抗波动但不浪费资源。注意队列优先级高于 max 扩容,很多人讲错。

第四步定 keepAliveTime:根据业务峰值持续时间定。秒级峰值设 30s-1min,分钟级峰值设 5min-10min。

第五步选拒绝策略:业务可重试选 AbortPolicy;日志埋点选 DiscardPolicy;需要限流上游选 CallerRunsPolicy(这个我最常用,天然限流让上游感受慢)。

ThreadFactory 必须命名,jstack 抓栈时一眼看出归属。禁用 Executors——FixedThreadPool 用无界队列 OOM,CachedThreadPool 无 max 线程暴涨。

动态调优:接 Nacos/Apollo 配置中心 + JMX 回调,运行时改参数不用重启。我们用 Hippo4j,配置中心推送 + MBean 暴露 + 告警闭环。

监控指标:poolSize / activeCount / queueSize / completedTaskCount / rejectedCount,接入 Micrometer + Prometheus 告警。

业务隔离:不同业务用不同线程池(Bulkhead 舱壁模式),避免一个业务打满拖垮全部。关键链路独立池,非关键链路单独池可容忍降级。

压测调优:先估再压测,别只靠公式。用 JMeter 压测看 P99 延迟、queueSize、rejectedCount,逐步调参。

我们踩过的坑:某次下游变慢导致队列积压、线程飙到几千、JVM OOM。定位靠 ThreadFactory 命名 + jstack。之后改成有界队列 + CallerRunsPolicy,队列满压回上游,问题就解决了。

#10 如果设计一个缓存,需要考虑哪些方面? high practice

题目

如果设计一个缓存,需要考虑哪些方面?

我的回答

(原文见题目文件面试记录)

点评

评分:⭐⭐☆☆☆(2/5)
优点:抓了 4 个方向(效率、稳定性、持久化、扩展);顺序合理
不足:广度严重不足(只答 4 个维度);每维度停留在"名词"层面;漏 8+ 关键维度(淘汰、过期、并发、网络、内存、安全、监控、多级缓存、一致性、API、容量规划、限流);缺乏量化思维;没有取舍权衡
补充:12 大维度框架(见题目文件完整讲解) ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察系统设计的结构化思维——能否列出所有关键维度 + 每个维度给出量化目标 + 讲清 trade-off。

**知识点拆解——12 大维度框架**:

1. **性能模型(先定目标)**:目标 QPS(如 10w)、P99 延迟(< 5ms)、内存占用、带宽;用 redis-benchmark 建基准;上线前压测找临界点

2. **数据结构(选对工具)**:String(KV/计数/锁/Session)、Hash(对象)、List(队列)、Set(去重/共同好友)、ZSet(排行榜/延迟队列);小数据用 ziplist/listpack,大数据自动升级

3. **淘汰与过期**:过期用惰性 + 定期删除(serverCron 采样);淘汰选 LRU(近似采样)、LFU(Redis 4.0+,防热点长尾);通用 allkeys-lru;热点长尾 allkeys-lfu;数据不能丢 noeviction

4. **持久化(RPO/RTO 权衡)**:RDB 快照恢复快但丢 15min 数据;AOF 追加日志 everysec 更安全但写慢;混合持久化(Redis 4.0+)RDB + 增量 AOF 是最佳折中

5. **高可用**:主从复制读扩展 + 故障转移;Sentinel 哨兵自动故障转移,3 个哨兵仲裁;Cluster 原生分片 16384 哈希槽;脑裂防护 min-replicas-to-write + 多数派仲裁

6. **扩展(分片策略)**:哈希槽(Redis Cluster 16384 槽,MOVED 重定向);一致性哈希(自定义分片,减少数据迁移);在线重平衡(redis-cli --cluster reshard);平滑扩容先加节点再迁 slot 再验证再摘老节点

7. **一致性(CAP 选型)**:Redis 是 AP 系统分区容忍 + 可用性牺牲一致性;写已写写主库立即返回读可能读到旧值;读多数 READMULTI 多副本读;最终一致靠异步复制 + 业务容忍窗口

8. **并发控制**:单线程模型命令串行天然原子;Lua 脚本批量操作原子化;事务 MULTI/EXEC 无回滚慎用;分布式锁 Redlock / Redisson(6.0+ 推荐)

9. **网络模型**:I/O 多路复用 epoll/kqueue 单线程处理万级连接;RESP 协议轻量二进制;连接复用(Lettuce 用 Netty 复用);Pipeline 批量命令减少 RTT

10. **安全**:认证密码 + ACL(Redis 6.0+ 细粒度权限);传输加密 TLS;网络隔离内网访问防火墙白名单;审计命令日志敏感操作告警

11. **监控(可观测性)**:核心指标 QPS、延迟 P50/P99、内存 used_memory、连接数;业务指标 evicted_keys、expired_keys、rejected_connections;慢查询 SLOWLOG(默认阈值 10ms);告警 Prometheus + Grafana

12. **业务集成**:穿透/击穿/雪崩用布隆过滤器 + 互斥锁 + TTL 抖动;多级缓存本地 Caffeine + 分布式 Redis + DB;限流熔断 Sentinel / Hystrix;与 DB 一致性 Cache-Aside + 延迟双删 + Canal + TTL 兜底;API 设计 CRUD、批量、事务、Pub/Sub、Pipeline

第二部分:考察方向与延伸递进

**考察方向**:
- 深度:每个维度的实现细节(如 LFU 计数器衰减、一致性哈希的虚拟节点)
- 广度:与 Memcached / etcd / TiKV / Hazelcast 对比选型
- 应用:具体场景(电商秒杀、社交 Feed、排行榜、Session)的架构设计

**延伸递进面试题**:

1. **如何估算缓存容量?**
- 解答要点:热点数据量 × 单条大小 × 副本数 × 安全系数(1.5-2);MEMORY USAGE 抽样
2. **一致性哈希如何解决数据倾斜?**
- 解答要点:虚拟节点(每物理节点 100-150 个虚拟节点)、MD5 散列均匀
3. **Redis Cluster 的脑裂如何防护?**
- 解答要点:min-replicas-to-write + 多数派仲裁 + 老主库下线验证
4. **Redis 如何做蓝绿部署?**
- 解答要点:新版本部署 → 双写或同步旧数据 → 灰度切流 → 全量切换 → 回滚预案
5. **如何设计一个支持强一致缓存?**
- 解答要点:Raft 协议(etcd 思路)+ 多副本同步 + 写多数;或业务层加锁
6. **缓存如何优雅降级?**
- 解答要点:熔断打开 → 走 DB(限流)→ 静态兜底数据 → 前端提示
7. **多级缓存的一致性如何解决?**
- 解答要点:Cache-Aside + 发布订阅 + 本地缓存 TTL 极短 + 容忍短暂不一致

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,设计缓存我一般从 12 个维度入手,每个都要给量化目标和权衡。

第一层:性能与存储。性能模型先定目标 QPS(如 10w)、P99 延迟(< 5ms)、内存预算;用 redis-benchmark 建基准。数据结构:String(KV/计数/锁)、Hash(对象)、List(队列)、ZSet(排行榜);小数据用 listpack 紧凑。淘汰与过期:过期用惰性 + 定期删除;淘汰选 allkeys-lru(通用)或 allkeys-lfu(热点长尾);TTL 加随机抖动防雪崩。

第二层:可靠性与扩展。持久化:RDB 恢复快但丢 15min,AOF everysec 更安全但写慢;混合持久化是最佳折中。高可用:主从 + Sentinel 自动故障转移;生产用 Cluster 16384 哈希槽分片。脑裂防护:min-replicas-to-write + 多数派仲裁。扩展:在线重平衡迁 slot;平滑扩容先加节点再迁数据。

第三层:一致性与并发。CAP 选型:Redis 是 AP 系统,牺牲一致性保可用。并发:单线程模型保证命令原子;Lua 脚本批量原子化;分布式锁用 Redisson。

第四层:网络与安全。网络:epoll I/O 多路复用 + RESP 协议 + 连接池 + Pipeline。安全:ACL 细粒度权限 + TLS + 内网隔离。

第五层:可观测性。监控:QPS、延迟 P99、used_memory、evicted_keys、SLOWLOG;Prometheus + Grafana 告警。

第六层:业务集成。三大问题:穿透用布隆过滤器、击穿用互斥锁、雪崩用 TTL 抖动。多级缓存:本地 Caffeine + Redis + DB。与 DB 一致性:Cache-Aside + 延迟双删 + Canal binlog + TTL 兜底。

实际项目我做过一个电商缓存设计:10w QPS、P99 < 3ms 目标;用 Redis Cluster 6 节点 3 主 3 从;淘汰策略 allkeys-lfu(热点商品长尾);混合持久化;Canal 消费 binlog 保证与 DB 最终一致;本地 Caffeine 5 秒兜底;压测验证临界点。踩过一个坑:早期没做容量规划,大促时内存爆表触发 OOM,后来加了容量告警 + 自动扩缩容。

📚 查看全部面试记录