Java 后端真实面试专题 · Redis 篇
Redis 和 MySQL 并列高频。每题三段: ① 标准答(讲透:是什么+为什么+怎么做+原理)→ ② 拓展(成体系带出关联点和必追问的,答一题等于答一片)→ ③ 怎么接到你自己的项目。
年限标签:
🟢 3年内🔴 3年+
1. 🟢 Redis 为什么这么快?单线程为什么不慢?
标准答:四个原因——
- 纯内存操作:数据在内存,读写就是纳秒级。
- 单线程模型:命令执行是单线程,避免了多线程的锁竞争和上下文切换开销。
- IO 多路复用:基于 epoll 的事件驱动,一个线程能高效处理大量连接。
- 高效数据结构:如 SDS、跳表、压缩列表、渐进式 rehash。
拓展:
- "单线程为什么不是瓶颈?"——Redis 的瓶颈在内存和网络 IO而不是 CPU,单线程省去锁开销反而更稳更简单。
- "Redis 6.0 不是多线程了吗?"——6.0 引入多线程只用于网络 IO 的读写和协议解析,命令执行仍是单线程,所以不用担心并发安全。
- "什么操作会阻塞单线程?"——
keys *、大 key 的del、复杂的sort,这些要避免(用scan替代 keys)。 - 一次命令的真实路径:请求先经过 TCP 读事件和协议解析,再进入事件循环执行命令,结果写回 socket;命令时间复杂度、网络往返次数和响应体大小都会直接反映到 P99 延迟。RDB/AOF 重写、懒释放等工作可由子进程或后台线程承担,并不等于“所有工作只有一个线程”。
- 不要把“内存快”当成无条件快:
O(n)的集合扫描、超大响应、频繁跨机访问都会让 CPU 或网卡成为瓶颈。生产上应结合slowlog、latency doctor、命令耗时和 big key 指标定位,而不只看平均 QPS。 - 常见追问:“为什么 pipeline 能提速?”——它减少每条命令的 RTT,但不会降低命令本身的复杂度;批次过大又会增加单次阻塞和客户端内存,应按响应大小分批。
往项目引 ⭐:"我项目把首页商品、热门排行这些读多写少的数据放 Redis,QPS 扛住了大促流量、DB 压力降了一大半。也正因为单线程,我们绝不在线上用 keys *,统计 key 都用 scan 渐进遍历。"
2. 🟢 Redis 有哪些数据类型?底层结构和适用场景?
标准答:五种基本类型——
- String:缓存、计数(
incr)、分布式锁。底层 SDS。 - Hash:存对象(如
user:1的各字段,可单独改一个字段)。底层 ziplist/hashtable。 - List:消息队列、最新列表。底层 quicklist。
- Set:去重、共同好友(
sinter交集)。底层 intset/hashtable。 - ZSet:排行榜、延迟队列。底层 ziplist/skiplist。
拓展:
- 进阶三个特殊类型——bitmap(签到、布隆过滤器)、HyperLogLog(UV 统计,省内存有误差)、GEO(附近的人)。能说出这三个是加分项。
- "ZSet 为什么用跳表不用红黑树?"——跳表实现简单、范围查询方便、并发友好。
- "Hash 什么时候从 ziplist 转 hashtable?"——元素超 128 个或值超 64 字节。
- 编码是实现细节,不是接口契约:Redis 7 以后很多“小对象”使用
listpack,旧版本常见ziplist;超过hash-max-listpack-*、zset-max-listpack-*等阈值会转成哈希表/跳表。阈值应结合对象大小和内存碎片压测,不要死记一个数字。 - 复杂度和访问模式要一起看:String 适合整体读写,Hash 适合按字段更新,List 两端操作快但按下标中间访问慢,Set 判断成员平均 O(1),ZSet 更新和范围查询通常是 O(logN)。需要分页、独立过期或独立删除时,通常拆成多个 key 比一个超大 Hash 更可控。
- 常见追问:“能不能用 List 做可靠 MQ?”——只能做轻量队列;缺少消费组、确认和回溯时要自行补偿,正式消息链路优先 Stream 或专业 MQ。
往项目引 ⭐:"我项目里每种类型都对着一个真实功能:ZSet 做实时热销榜(zincrby 累加、zrevrange 取 Top10)、Hash 存购物车、Set 做活动去重、bitmap 做签到——不是背概念,是真用过。"
3. 🟢 缓存穿透、击穿、雪崩分别是什么?怎么解决?(必问)
标准答:
- 穿透:查一个根本不存在的数据,缓存和 DB 都没有,每次都打到 DB(常被恶意攻击)。解决:① 缓存空值(设短过期);② 布隆过滤器拦截非法 key。
- 击穿:某个热点 key 过期的瞬间,大量请求同时打到 DB。解决:① 互斥锁,只让一个线程重建缓存;② 热点 key 逻辑过期/不过期。
- 雪崩:大量 key 同一时间过期,或 Redis 宕机,请求全压到 DB。解决:① 过期时间加随机值打散;② 多级缓存;③ Redis 高可用 + 限流降级兜底。
拓展:
- 三者区别一句话:穿透是"查不存在的",击穿是"单个热点失效",雪崩是"大面积同时失效"。
- "布隆过滤器原理?"——多个哈希函数映射到位数组,能判断"一定不存在"或"可能存在",有误判、但省空间、不能删除(用计数布隆可删)。
- 互斥锁重建要注意锁本身的超时,别把请求都堵死。
- 回源顺序要固定:先查本地缓存,再查 Redis,再查布隆过滤器/数据库;数据库查不到时写入带短 TTL 的空值,并限制空值大小。布隆过滤器只能挡住“确定不存在”的 key,不能替代数据库校验,也要在新增数据时同步更新。
- 逻辑过期不是删除过期:逻辑过期把数据和过期时间一起存,读到过期值时由一个线程异步刷新,其余请求暂时读旧值;它牺牲短时间新鲜度换可用性,库存、余额等强一致数据不能直接套用。
- 常见追问:“缓存重建失败怎么办?”——释放锁并保留短暂旧值/兜底值,记录重试次数和告警;锁等待必须有上限,不能让所有请求无限排队。
往项目引 ⭐:"我项目商品详情接口用了三招:布隆过滤器挡非法商品 id 防穿透、热点商品用逻辑过期防击穿、所有 key 过期时间加随机值防雪崩,大促期间缓存层零事故。"
4. 🟢 RDB 和 AOF 持久化的区别?怎么选?
标准答:
- RDB:定时把内存快照 dump 成一个二进制文件。优点文件小、恢复快;缺点是两次快照之间宕机会丢数据。
- AOF:记录每条写命令,追加到文件。优点更安全(可配每秒刷盘,最多丢 1 秒);缺点文件大、恢复慢。
- 选择:生产常用 RDB + AOF 混合持久化——AOF 保证少丢数据,重启时用 RDB 部分加速恢复。
拓展:
- "AOF 文件越来越大怎么办?"——AOF 重写(rewrite),用最简命令重建当前数据状态,压缩体积。
- "AOF 刷盘策略?"——always(每条都刷,最安全最慢)、everysec(每秒,默认,平衡)、no(交给操作系统)。
- "主从 + 持久化的关系?"——从库也可以开持久化做多份备份。
- RDB 的一致性边界:
BGSAVE通过 fork 的写时复制生成快照,fork 和复制页会带来瞬时内存峰值;AOF 重写也会 fork,不能只按文件大小评估容量。磁盘空间、fork 延迟和INFO persistence中的失败计数都要监控。 - 混合持久化的恢复顺序:AOF 文件前半段是 RDB 快照、后半段是增量命令,重启先加载快照再重放增量。它降低恢复时间,但不等于零数据丢失;
everysec仍可能丢最近约一秒,业务还需要数据库或消息补偿。 - 选型要看数据来源:纯缓存可把恢复速度和成本放前面;排行榜、限流计数等可重建数据与订单状态、会话数据的 RPO 不同,最好拆实例并单独配置持久化。
往项目引 ⭐:"我项目 Redis 主要做缓存、数据能从 DB 重建,所以对持久化要求不高、用 RDB 够了;但做分布式锁的那个实例开了 AOF,因为锁信息丢了影响业务。按数据重要性分别配置。"
5. 🟢 Redis 的 key 过期了是怎么删除的?内存满了怎么淘汰?
标准答:
- 过期删除:惰性删除(访问到 key 时才检查、过期就删)+ 定期删除(每隔一段时间随机抽一批检查删除)。两者结合,平衡 CPU 和内存。
- 内存淘汰:内存达到
maxmemory时按策略淘汰,8 种——对所有 key 的(allkeys-lru/lfu/random)、只对设了过期时间的(volatile-lru/lfu/ttl/random)、noeviction(不淘汰、写报错)。常用allkeys-lru。
拓展:
- "为什么不立即删除过期 key?"——给每个 key 开定时器太耗 CPU,所以用惰性+定期的折中。
- "惰性删除有什么问题?"——过期了但一直没被访问的 key 会占内存,靠内存淘汰兜底。
- "LRU 和 LFU 区别?"——LRU 看"最近有没有用",LFU 看"用得频不频繁",LFU 更适合有明显冷热的数据。
- 定期删除不是精确扫描:Redis 在时间预算内抽样检查带过期时间的 key,过期比例高时会继续抽样;因此“TTL 到点”与“内存中立刻消失”不是同一时刻。可用
MEMORY STATS、INFO memory和淘汰计数判断是否真的有压力。 - 淘汰策略要与 TTL 设计配套:
volatile-*只淘汰设置了过期时间的 key,若持久数据没有 TTL,可能最终无法写入;缓存实例通常选allkeys-lfu或allkeys-lru,并设置maxmemory留出复制、缓冲区和碎片空间。 - 删除大对象的边界:
UNLINK把释放内存移到后台线程,命令本身仍会遍历对象元数据;它不能解决大 key 带来的网络和数据建模问题。
往项目引 ⭐:"我项目纯缓存场景设 allkeys-lru,让冷数据被自动挤掉、热数据常驻,命中率稳定在 95% 以上。还排查过内存涨不下去——就是很多过期 key 没被访问没及时删,调整策略后好了。"
6. 🟢 怎么保证 Redis 缓存和 MySQL 的数据一致性?
标准答:最常用 Cache Aside(旁路缓存):
- 读:先查缓存,没有就查 DB 并回填缓存。
- 写:先更新 DB,再删除缓存(而不是更新缓存)。 删除而非更新是为了避免并发写时旧值覆盖新值;先更库后删缓存能减少不一致窗口。
拓展:
- "删缓存失败怎么办?"——延迟双删(更新前删一次、更新后延迟几百毫秒再删一次),或用 MQ/canal 监听 binlog 异步删缓存做兜底重试。
- "为什么不先删缓存再更库?"——并发下可能"删缓存→别的请求把旧值读回缓存→才更库",反而不一致。
- 强一致很难、代价高,一般做最终一致就够。
- 并发窗口要说清楚:读请求在“查库并回填”期间,写请求可能已经更新数据库并删除缓存,读请求随后把旧值写回去;延迟双删只能降低概率,不能提供严格线性一致。对价格、库存等关键字段可用版本号/CAS,或让 binlog 事件按 key 串行消费。
- 删除失败必须可观测:删除命令要重试并记录业务 key、版本和失败原因;补偿消费者要幂等,不能因为重试把新值误删。缓存 TTL 是最后一道兜底,不能拿 TTL 代替一致性方案。
- 读写策略可分级:允许秒级延迟的详情页采用 Cache Aside;余额、库存等强一致路径直接读库或采用带版本的缓存,避免为了命中率牺牲正确性。
往项目引 ⭐:"我项目商品改价用'更新 DB + 删缓存',再加 canal 监听 binlog 兜底删,保证用户最终看到新价格、不会长期读到旧价。要求高的地方加延迟双删。"
7. 🔴 Redis 分布式锁怎么实现?有哪些坑?
标准答:加锁用 SET key 唯一值 NX EX 30——NX 保证只有一个客户端能设置成功,EX 设过期防止持锁者宕机导致死锁,value 用唯一标识(如 UUID + 线程 id)。解锁用 Lua 脚本保证"判断是不是自己的锁 + 删除"两步原子。
拓展:
- 坑一:业务没执行完锁就过期了——别的线程拿到锁,出现并发。解决:用 Redisson 的看门狗自动续期。
- 坑二:解锁误删别人的锁——所以 value 要唯一、解锁要先判断。
- 坑三:主从切换锁丢失——主库加锁成功还没同步就宕机,从库上锁没了。极端一致用 RedLock(向多数节点加锁)。
- 直接
setnx+del不带唯一标识、不设过期是错误写法。 - 锁的语义要补上:Redis 锁只保证“某段时间内尽量只有一个持有者”,不自动保证业务结果正确。给下游写操作增加fencing token(单调递增令牌),资源端拒绝旧令牌,才能防止客户端暂停后恢复、继续使用已过期锁。
- 续期与释放都要有边界:看门狗续期线程失联、GC 停顿或网络分区时仍可能失锁;业务必须可重入/可重试,释放采用 Lua 原子校验
value。锁 TTL 应小于故障可接受窗口,不能无限续期掩盖慢任务。 - RedLock 不是万能强一致:多节点投票增加可用性但仍受时钟、网络和客户端暂停影响;涉及资金或唯一写入时优先用数据库约束、共识系统或带 fencing 的锁。
往项目引 ⭐:"我项目定时任务多实例部署,用 Redisson 分布式锁保证同一任务同一时刻只有一个实例跑,看门狗自动续期解决了'长任务锁提前过期'的问题,不用自己手写续期逻辑。"
8. 🟢 什么是热 key 和大 key?有什么危害,怎么解决?
标准答:
- 热 key:访问量极高的 key(如爆款商品、热搜),可能把单个 Redis 节点 CPU/网卡打满。解决:本地缓存(Caffeine)+ key 拆分到多个节点 + 多副本读。
- 大 key:value 很大的 key(如几万元素的 List、几 MB 的 String),操作慢、阻塞单线程、内存分布不均、删除时卡顿。解决:拆分(大 Hash 拆成多个小 Hash)、用
scan/unlink渐进删除而不是del。
拓展:
- "怎么发现大 key?"——
redis-cli --bigkeys、memory usage key、扫描分析。 - "为什么大 key 删除会阻塞?"——单线程删大对象耗时,Redis 4.0+ 用
unlink异步删除。 - 热 key 在集群下还会导致数据倾斜(某个分片特别忙)。
- 阈值要按业务定义:单个 String 的字节数、Hash/List/Set 的元素数和每秒访问次数都应设独立阈值,不能只用“超过 1 MB”一个标准。上线前用真实数据压测序列化、网络包和删除耗时。
- 热 key 的拆分要保持语义:可以把一个榜单拆成
key:{0..N}多副本,读时随机/按哈希取分片;写入需要广播或异步合并,不能只复制一份后任意修改导致排名不一致。 - 监控指标:命令耗时、单 key QPS、热点命中率、分片 CPU/网络、
evicted_keys和内存碎片率一起看,才能区分热 key、慢命令和数据倾斜。
往项目引 ⭐:"我项目大促前会用 --bigkeys 扫一遍大 key 提前拆分,给爆款商品做 Caffeine 本地缓存 + Redis 二级缓存,避免热 key 把某个分片打挂——这是大促保障的常规动作。"
9. 🔴 Redis 的主从、哨兵、Cluster 分别解决什么问题?
标准答:
- 主从复制:主写从读,实现读扩展和数据备份。但主挂了不能自动恢复。
- 哨兵(Sentinel):监控主从健康,主库宕机时自动故障转移(选一个从库升为主)、通知客户端新主地址。解决高可用,但不解决容量。
- Cluster(集群):数据按 16384 个哈希槽分布到多个主节点(每个主带从),既分片扩容又高可用。解决单机容量和写性能瓶颈。
拓展:
- "Cluster 怎么定位 key 在哪个节点?"——
CRC16(key) % 16384算出槽、槽分配给某个节点。 - "Cluster 支持多 key 操作吗?"——跨槽的多 key 操作受限,可用 hash tag(
{})把相关 key 放同一槽。 - 演进路线:单机 → 主从+哨兵(高可用)→ Cluster(容量也扛不住时)。
- 复制是异步的:主节点返回成功不代表从节点已经落盘,故障切换可能丢失最近写入;需要更强保证时配置
WAIT或把关键事实落在数据库/MQ。读从库还要接受复制延迟,不能用于刚写即读的强一致路径。 - Sentinel 的判断:主观下线是单个哨兵认为不可达,客观下线要达到 quorum;选主还会比较复制偏移量和优先级。客户端必须支持重连、刷新主地址,并对切换窗口做超时和重试。
- Cluster 的运维边界:扩容需要迁移槽位,迁移期间要处理
MOVED/ASK;多 key、事务和 Lua 需使用 hash tag 将相关 key 放在同一槽。
往项目引 ⭐:"我项目早期单机 + RDB 备份,量大后上哨兵保证高可用(演练过杀主节点,几秒切主),再后来数据量和写入都涨上来才迁到 Cluster 分片。是按瓶颈一步步演进的,不是一上来就上集群。"
10. 🔴 用 Redis 怎么实现接口限流?
标准答:几种方式——
- 固定窗口:
incr计数 + 第一次设过期,超阈值就拒绝。简单但有临界突刺。 - 滑动窗口:用 ZSet 存请求时间戳,按时间范围
zcount统计当前窗口请求数,更平滑。 - 令牌桶/漏桶:用 Lua 脚本保证原子,允许一定突发。
拓展:
- "固定窗口的临界问题?"——窗口边界前后各来一批,瞬时可能是阈值的两倍。
- 生产常用 Redis + Lua 实现令牌桶,或直接用 Sentinel/网关限流。
- Lua 保证"取令牌 + 判断 + 扣减"原子,避免并发超发。
- 限流维度要明确:按用户、设备、IP、接口和租户分别设 key,并把规则版本写入 key,避免改规则后旧计数污染新规则。返回
429时可带Retry-After,让客户端退避而不是立即重试。 - 令牌桶的状态通常包含
tokens与last_refill_time;Lua 按时间补充令牌、判断是否足够、扣减并设置 TTL,整个过程一次执行。时钟建议使用 RedisTIME或统一时间源,避免各应用机器时钟不一致。 - 限流不是背压的全部:还要限制并发数、下游连接池和队列长度;单纯提高阈值只会把压力转移到数据库。
往项目引 ⭐:"我项目短信验证码接口防刷,用 Redis 对手机号做滑动窗口限流——一分钟最多发一条,超了直接拦,用 Lua 保证并发下计数准确。"
11. 🟢 Redis 的 pipeline 是什么?和事务、Lua 的区别?
标准答:
- pipeline(管道):把多条命令一次性打包发送、一次拿回所有结果,减少网络往返(RTT)。只是批量发送,不保证原子、命令间不能依赖。
- 事务(MULTI/EXEC):命令入队后一起执行,但不支持回滚、中间不能拿结果做判断。
- Lua 脚本:在服务端原子执行,可以带逻辑判断(读-判断-写),是实现复杂原子操作的首选。
拓展:
- "pipeline 和事务能一起用吗?"——可以,pipeline 里包 MULTI/EXEC。
- 库存预扣、分布式锁解锁这种"读+判断+写"必须用 Lua。
- 批量操作优先 pipeline,别 for 循环单条发(每条一次 RTT 很慢)。
- 三者边界:pipeline 只优化客户端与服务端之间的交互,不提供原子性;MULTI/EXEC 保证队列内命令连续执行,但事务中不能根据中间结果分支;Lua 在服务端一次执行且能读写判断,脚本过长会阻塞其他命令。
- 失败处理:pipeline 的部分命令可能成功、部分失败,业务要按结果逐条处理;事务执行阶段的类型错误不会回滚;Lua 要设置脚本超时治理和版本管理,避免在线替换造成行为漂移。
- 批量大小要根据 RTT、响应体和 Redis 单线程时间预算压测,通常宁可多个中等批次,也不要一次塞几万条。
往项目引 ⭐:"我项目批量查 100 个商品库存,原来循环 100 次网络往返很慢,改成 pipeline 一次发回快了好几倍;秒杀预扣库存则用 Lua 脚本保证'判库存→扣库存'原子,杜绝超卖。"
12. 🔴 ZSet 的底层是什么?为什么排行榜用它?
标准答:ZSet 底层是 跳表(skiplist)+ 哈希表 的组合(元素少时用 ziplist 省内存):
- 跳表保证按 score 有序,支持范围查询和按排名取(
zrange)。 - 哈希表保证按成员 O(1) 查到它的 score(
zscore)。 两者配合,既能快速排名又能快速查分。
拓展:
- "跳表是什么?"——多层链表,上层是下层的"索引",查找类似二分、平均 O(log n)。
- "为什么不用红黑树?"——跳表实现简单、范围查询直接顺着底层链表走、并发更好控制。
- ziplist 转 skiplist 的阈值:元素超 128 或成员超 64 字节。
- 复杂度要说完整:
ZADD、ZREM和按成员查分平均 O(logN),按分数范围查询是 O(logN+M),其中 M 是返回元素数;ZRANGE返回大页数据时网络和序列化成本可能比索引查找更高。 - 同分排序规则:score 相同按成员的字典序排列;若业务要求“先到先得”,可把时间或序号编码进 score,或在成员中带唯一序号,不能假设插入顺序天然保留。
- 排行榜工程细节:分页要固定快照/游标,避免频繁大范围
ZREVRANGE;过期榜单用ZREMRANGEBYRANK/ZREMRANGEBYSCORE定期清理。
往项目引 ⭐:"我项目实时热销榜用 ZSet——下单时 zincrby 累加销量、展示时 zrevrange 0 9 取 Top10、查某商品排名用 zrevrank。正是看中跳表能高效排名又能查分。"
13. 🟢 缓存预热和缓存降级是什么?
标准答:
- 缓存预热:系统上线或大促前,提前把热点数据加载进缓存,避免冷启动时请求全打到 DB。可用启动任务或定时任务加载。
- 缓存降级:当缓存或下游不可用时,返回兜底数据/默认值/旧数据,保证核心流程可用,常配合熔断(Sentinel)。
拓展:
- 预热要选真正的热点(可根据历史访问统计)。
- 降级是有损服务——牺牲部分体验保整体可用。
- 还有"缓存击穿"场景下,预热 + 逻辑过期能避免热点重建风暴。
- 预热流程要可回滚:先在影子 key 写入并校验版本、再切换指针;避免启动时一次性
mget数百万条把 Redis 和数据库打满。预热失败要记录进度,支持断点续跑。 - 降级分层:核心接口可返回旧版本/只读摘要,非核心接口返回默认值或关闭;同时在响应中保留可观测标记,避免前端把降级数据误当成实时数据。
- 常见追问:“缓存预热和定时刷新有什么区别?”——预热解决冷启动,刷新解决数据新鲜度;刷新仍需版本校验和失败重试。
往项目引 ⭐:"我项目大促前用定时任务把 Top 商品预热进 Redis;Redis 万一挂了,详情接口降级返回本地缓存的基础信息,不让整个页面崩——预热保性能、降级保可用。"
14. 🔴 Redis 怎么实现延迟队列?和 MQ 比怎么选?
标准答:用 ZSet,score 存任务的到期时间戳,消费端轮询 zrangebyscore 取出到期的任务处理(取出后删除)。或直接用 Redisson 的 RDelayedQueue。
拓展:
- "和 RocketMQ 延迟消息比?"——Redis 方案轻量但要自己保证可靠性(消费失败重试、宕机恢复);MQ 方案更可靠、量大首选。
- 轮询要注意频率和并发取出的原子性(用 Lua 取+删)。
- 还可以用 Redis 的 keyspace 过期通知,但不保证及时和可靠。
- 取任务必须“认领”:用 Lua 原子地取出到期任务并移入 processing 集合,处理成功再删除;消费者宕机可根据 lease 超时把任务放回待处理集合。仅
ZRANGEBYSCORE后ZREM会在并发消费者下重复执行。 - 时间精度与负载:轮询间隔决定延迟和 CPU,建议使用阻塞/定时调度并批量取任务;任务 payload 不宜直接塞大对象,正文放数据库或对象存储、Redis 只保存引用。
- 何时不用 Redis:需要严格投递、消费位点、海量堆积或跨机房复制时,专业 MQ 的持久化和重试语义更合适。
往项目引 ⭐:"我项目订单 30 分钟未支付自动取消,早期用 Redis ZSet 做延迟队列(score 存取消时间),后来订单量大、对可靠性要求高,就迁到了 RocketMQ 延迟消息。"
15. 🟢 Redis 和 Memcached 的区别?为什么选 Redis?
标准答:
- Redis:支持丰富数据结构(String/Hash/List/Set/ZSet)、持久化、主从/哨兵/集群、事务/Lua、发布订阅。
- Memcached:只有简单 KV、纯内存不持久化、多线程。 现在新项目基本都选 Redis,因为功能强太多。
拓展:
- "Memcached 有什么优势?"——多线程在纯 KV、超大并发下吞吐略高,但功能太单一。
- Redis 的数据结构和持久化、高可用是决定性优势。
- 协议和运维差异:Memcached 的一致性哈希、纯缓存语义简单,适合已有成熟集群且只需要 KV 的场景;Redis 的 RESP、Lua、复制和集群能力更丰富,但单线程命令阻塞、内存碎片和持久化运维也要承担。
- 不要用功能数量替代选型:若只是极简 session 缓存且团队已有 Memcached 监控,迁移 Redis 未必有收益;反之需要排行榜、原子脚本或可靠计数时,Redis 的数据结构能显著减少外围组件。
往项目引 ⭐:"我项目所有缓存都用 Redis,正是因为要用 ZSet 排行榜、分布式锁、发布订阅这些 Memcached 给不了的能力,一个 Redis 顶 Memcached 加一堆中间件。"
16. 🔴 Redis 宕机了缓存全没了怎么办?怎么保证高可用?
标准答:
- 高可用部署:哨兵或 Cluster,主挂自动切换,避免单点。
- 持久化恢复:RDB/AOF,重启能恢复数据。
- 重建保护:缓存失效后用限流 + 互斥锁防止瞬间把 DB 打垮(防雪崩)。
- 多级缓存:本地缓存(Caffeine)兜底,Redis 挂了本地还能扛一会。
拓展:
- "切换期间的请求怎么办?"——本地缓存 + 限流降级扛过切换窗口(哨兵切换通常几秒)。
- 要演练故障转移,别等真宕机才发现配置有问题。
- 故障处理顺序:先保护数据库(限流、熔断、只读/静态兜底),再恢复 Redis 主从和客户端连接,最后分批预热;不要在 Redis 刚恢复时让所有实例同时全量回源。
- 持久化不能替代高可用:RDB/AOF 解决重启后的数据恢复,Sentinel/Cluster 解决节点故障和切换;两者都要做跨机房备份、恢复演练,并明确 RPO/RTO。
- 客户端要有退避:连接池耗尽时应快速失败或走本地缓存,指数退避重连,避免故障期间形成连接风暴。
往项目引 ⭐:"我项目做了本地缓存兜底 + Redis 哨兵,演练时把 Redis 主节点杀掉,哨兵几秒切主、本地缓存扛过切换窗口、限流防止 DB 被打垮,整体没出大问题——高可用是设计 + 演练出来的。"
17. 🔴 Redis 的发布订阅和 Stream 了解吗?
标准答:
- 发布订阅(pub/sub):消息广播,订阅者收实时消息,但不持久化,订阅者不在线就丢消息。适合实时通知。
- Stream(5.0 新增):真正的消息队列,支持持久化、消费组、ack 确认、消息回溯,比 pub/sub 可靠。
拓展:
- pub/sub 常用于"通知各节点清本地缓存"这种允许丢的场景。
- Stream 弥补了 List 做队列的不足(支持消费组、不会重复消费)。
- 但要做正经 MQ 还是用 RocketMQ/Kafka,Redis Stream 适合轻量场景。
- Stream 的关键语义:生产用
XADD生成递增 ID,消费组用XREADGROUP读取,处理成功后XACK;未确认消息会进入 PEL,可用XPENDING/XAUTOCLAIM排查和接管。它仍需设置最大长度、保留策略和重放权限。 - Pub/Sub 没有补偿窗口:订阅者断线期间发布的消息不会进入历史;若“清本地缓存”通知丢了,要在本地缓存设置短 TTL 或定时版本校验兜底。
往项目引 ⭐:"我项目用 pub/sub 做多级缓存的一致性——Redis 数据变了广播一条消息,各应用节点收到后清掉自己的本地缓存,保证本地缓存不会长期脏。"
18. 🟢 Redis 事务支持回滚吗?为什么?
标准答:不支持回滚。MULTI 把命令入队、EXEC 一次性执行,即使中间某条命令执行出错,其他命令照样执行、不会回滚。
拓展:
- "为什么不支持回滚?"——Redis 设计哲学是简单高效,命令出错通常是编程错误(用错类型),运行时错误回滚意义不大。
- 入队阶段的语法错误会导致整个事务不执行;执行阶段的错误只影响那条命令。
- 要原子的"读-判断-写"用 Lua 脚本,比事务更实用。
- WATCH 是乐观锁:
WATCH key后读取,其他客户端修改该 key 时EXEC返回空结果,客户端负责重试;它适合短事务,不能锁住数据库式的长业务流程。 - 事务与持久化的关系:
EXEC的命令执行连续,但是否已经刷盘取决于 AOF 配置;若需要跨资源原子性,Redis 事务不能替代数据库事务或 MQ 事务消息。
往项目引 ⭐:"我项目需要原子操作时基本不用 Redis 事务,而是用 Lua 脚本——比如库存预扣,脚本里判断 + 扣减一气呵成、原子且能带逻辑,比 MULTI/EXEC 好用。"
19. 🔴 缓存和数据库双写一致性,有哪些方案?各自取舍?
标准答:
- 先更库再删缓存(Cache Aside,推荐):简单,存在极小不一致窗口。
- 延迟双删:更新前删一次、更新后延迟再删一次,覆盖并发回填旧值的情况。
- 订阅 binlog(canal)异步删缓存:解耦、可靠、能重试,是较优方案。
- 先更新缓存再更库:基本不用,容易不一致。
拓展:
- 这些都是最终一致,强一致代价太高(要加分布式锁串行化,性能差)。
- 要不要追求强一致看业务——商品价格能接受秒级延迟,账户余额就不能。
- 典型竞态:读线程在数据库更新前读到旧值、随后写入缓存;写线程更新数据库后删除缓存,读线程可能又把旧值回填。因此删除事件应携带版本,消费者只删除不低于当前版本的缓存,并对失败消息做重试/死信。
- binlog 方案的边界:canal/CDC 只负责捕获变更,不自动保证缓存删除成功;要有消费位点、幂等、积压告警和全量校对任务。跨表事务还要定义按业务 key 的合并顺序。
- 一致性指标:记录缓存命中率、脏读检测比例、删除延迟和补偿成功率,面试时给出“允许多长时间的最终一致”比笼统说“保证一致”更可信。
往项目引 ⭐:"我项目主路径用'更库 + 删缓存',再用 canal 监听 binlog 异步补偿删缓存做兜底,保证最终一致。对一致性要求高的少数场景才加延迟双删,按业务分级处理。"
20. 🟢 项目里 Redis 都用在哪些场景?
标准答:常见用途——缓存(热点数据、减 DB 压力)、分布式锁、计数器(点赞/库存)、排行榜(ZSet)、会话共享(Session)、限流、消息/延迟队列、去重(Set/布隆)、签到(bitmap)、UV 统计(HyperLogLog)。
拓展:
- 选 Redis 还是别的——缓存和高并发读写用 Redis,正经消息队列用 MQ,复杂搜索用 ES。
- 不要把 Redis 当数据库用(虽然有持久化),它是缓存/高性能存储的定位。
- 回答建议按风险分组:读缓存/本地缓存属于性能优化;计数、限流、锁属于并发控制;Stream/发布订阅属于消息协作;持久化、哨兵、Cluster 属于可靠性。每组都要说数据来源、过期/清理策略、失败兜底和监控指标。
- 避免“万能 Redis”:事务主数据、超大文件、需要复杂查询的数据不应直接放 Redis;先说明一致性、容量、备份和成本,再决定是否使用。
往项目引 ⭐:"我项目里 Redis 承担了缓存、分布式锁、排行榜、限流、签到好几个角色——能用一句话把每个场景对应到具体功能,面试官就知道我是真的在项目里用,而不是背的清单。"
你能答到第几层?
- 三段都能答、还能往项目引:Redis 这块你稳了。
- 标准答 + 拓展能成体系答:知识够,差接到项目说出来。
- 标准答都磕巴:Redis 有主线(数据结构 → 持久化 → 过期淘汰 → 高可用 → 应用场景),跟着学一遍就通。
这是面试专题的「Redis 篇」,网站上还有并发、MySQL、Spring、微服务、项目场景等系统整理。 🌐 更多真实面试专题与资料:smallredtech.com 💬 想系统学 / 简历与辅导咨询,加微信:Ahongbb666(备注「面试题」)