Redis 缓存与一致性 + 并发(引入线程池)
10 题 · 平均 3.5/5 · low 3.33 · medium 3.25 · high 4 · 第 1 轮
**核心考点定位**:考察对 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 多线程参数
面试官您好,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 快,本质是理解它做的一整套"少锁、少系统调用、少内存分配、少磁盘操作"的取舍。
**核心考点定位**:三种问题的根因不同(单点失效 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 集群整体宕机,你的系统如何优雅降级?**
- 解答要点:熔断打开 + 静态兜底数据 + 排队限流 + 前端提示
面试官您好,这三种问题本质是失效模式不同:击穿是单点、穿透是无效、雪崩是批量。
缓存击穿是热点 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 节点宕机(雪崩)。所以防御体系要分层叠加,而不是单选一种方案。
**核心考点定位**:考察对 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 回调 + 告警
面试官您好,线程池本质是池化思想,核心目标是复用线程 + 限流 + 隔离。
参数方面有 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,队列满后直接压回上游,问题就解决了。
---
**核心考点定位**:考察对 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 为什么不用锁实现并发控制?**
- 解答要点:单线程天然无竞争,加锁反而增加复杂度
面试官您好,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 一条命令解决。
**核心考点定位**:考察对 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 支持按数据库独立触发
面试官您好,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,前者是过期删除,后者是内存淘汰,两个数字要分开看。
**核心考点定位**:考察对 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 衰减优化;淘汰逻辑与过期逻辑解耦
面试官您好,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 设计要配套看,不能只改一边。
**核心考点定位**:考察对不一致根因的枚举能力 + 解决方案的层次选型能力,而非只答一个"延迟双删"。
**知识点拆解**:
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 最终落库 + 对账补偿
面试官您好,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 强一致方案。
**核心考点定位**:考察对 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
面试官您好,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 单线程是基于"命令轻 + 网络密集"的最优架构选择,简单、稳定、可预测;瓶颈用分片扩展,而不是破坏单线程模型。
---
**核心考点定位**:考察线程池调优的方法论(而非背公式)+ 工程落地能力(监控、动态调优、隔离)。
**知识点拆解**:
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 延迟
面试官您好,线程池调优我一般按 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,队列满压回上游,问题就解决了。
**核心考点定位**:考察系统设计的结构化思维——能否列出所有关键维度 + 每个维度给出量化目标 + 讲清 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 极短 + 容忍短暂不一致
面试官您好,设计缓存我一般从 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,后来加了容量告警 + 自动扩缩容。