Redis 高级场景速查手册
最后更新:2026-08-10
定位:Java 高级开发工程师面试 Redis 深度备战,口语化 + 真实案例 + 面试话术。 阅读建议:先通一遍建立知识体系,面试前重点背「面试怎么说」的话术。
目录
1. Redis 数据类型深度
1.1 八大类型全景图
Redis 远不止 String、List、Hash、Set、ZSet 这 5 种基础类型,从 3.2 开始陆续加了 Bitmap、HyperLogLog、GEO、Stream,面试时把这几样说出来,直接加分。
| 类型 | 典型场景 | 一句话总结 |
|---|---|---|
| String | 缓存、计数器、分布式锁 | 万能类型,啥都能存 |
| Hash | 用户信息、对象属性 | 存对象首选,字段可单独改 |
| List | 消息队列、时间线 | 有序但没索引,头尾快 |
| Set | 标签、共同好友、抽奖 | 去重 + 集合运算 |
| ZSet | 排行榜、延迟队列 | 带 score 排序,O(logN) |
| Bitmap | 签到、在线状态、布隆过滤器 | 位操作,极致省内存 |
| HyperLogLog | UV 统计 | 基数估算,误差 0.81%,只占 12KB |
| GEO | 附近的人、打车距离 | 地理位置,底层就是 ZSet |
| Stream | 消息队列(Kafka 平替) | 5.0 引入,支持消费组 |
1.2 底层编码结构
面试必问:Redis 为了省内存,同一个数据类型在不同数据量下,底层编码是不一样的。
| 数据类型 | 小数据量编码 | 大数据量编码 | 转换条件 |
|---|---|---|---|
| String | int → embstr → raw | raw | ≤44字节用 embstr,否则 raw |
| List | quicklist(ziplist 已废弃) | quicklist | list-max-ziplist-size 默认 -2(8KB) |
| Hash | listpack(4.0 前是 ziplist) | hashtable | hash-max-listpack-entries ≤128 且每个 value ≤64字节 |
| Set | intset → listpack | hashtable | intset:全是整数且 ≤512 个;listpack:≤128 个且 ≤64字节 |
| ZSet | listpack | skiplist + hashtable | zset-max-listpack-entries ≤128 且每个 ≤64字节 |
⚠️ 面试高频坑点:
4.0 版本
ziplist→listpack的迁移,ziplist 有连锁更新问题(一个节点变长导致后面的节点全部重新分配),listpack 通过限制 entry 最大长度解决了这个问题。Redis 7.0 里 listpack 进一步取代了 ziplist 在几乎所有场景的使用。
5.0 之后 List 的底层是 quicklist,本质是 ziplist(现在也换成了 listpack)的双向链表,兼顾头尾操作效率和内存利用。
底层结构口诀
1.3 内存编码转换阈值速查表
| 配置项 | 默认值 | 含义 |
|---|---|---|
hash-max-listpack-entries | 128 | Hash 的 field 数 ≤ 128 用 listpack |
hash-max-listpack-value | 64 | Hash 每个 value ≤ 64 字节 |
zset-max-listpack-entries | 128 | ZSet 元素数 ≤ 128 用 listpack |
zset-max-listpack-value | 64 | ZSet 每个 value ≤ 64 字节 |
set-max-intset-entries | 512 | Set 全是整数且 ≤ 512 个用 intset |
set-max-listpack-entries | 128 | Set 用 listpack 的元素上限(7.0+) |
list-max-ziplist-size | -2 | quicklist 每个节点的 listpack 大小,-2 代表 8KB |
💡 面试怎么说: "Redis 会根据数据量动态切换底层编码。比如 Hash 在字段少于 128 个且 value 都小于 64 字节时用 listpack,超过了就切到 hashtable。这种设计在节省内存的同时保证了大数据量下的操作性能。listpack 是 4.0 引入的,解决了 ziplist 的连锁更新问题——因为 listpack 限制了单个 entry 最大长度,不会因为一个节点的变长导致后面所有节点连锁重新分配。"
1.4 选型指南:场景 → 类型
| 业务场景 | 推荐类型 | 备注 |
|---|---|---|
| 用户会话 / Token | String | 简单 KV,设过期时间 |
| 用户对象(姓名+年龄+…) | Hash | 可单独改某个字段,省序列化开销 |
| 消息队列 | Stream / List | Stream 支持 ACK、消费组,生产首选 |
| 排行榜 | ZSet | score 排序 + 范围查询 |
| 延迟队列 | ZSet | score = 执行时间戳,轮询 ZRANGEBYSCORE |
| 每日签到 | Bitmap | SETBIT 一位一天,一年才 46 字节 |
| UV 统计 | HyperLogLog | 百万 UV 只占 12KB,误差可接受 |
| 附近的人 | GEO | 底层是 ZSet,GEORADIUS 查询 |
| 布隆过滤器 | Bitmap + 多哈希 | 或用 RedisBloom 模块 |
| 分布式锁 | String | SET key value NX EX |
| 计数器(点赞数) | String | INCR 原子操作 |
2. 缓存穿透 击穿 雪崩
2.1 三者区别一张表
| 维度 | 缓存穿透 | 缓存击穿 | 缓存雪崩 |
|---|---|---|---|
| 定义 | 查询一个根本不存在的数据,缓存和 DB 都没有 | 某个热点 Key 突然过期,大量并发打到 DB | 大量 Key 同时过期或 Redis 宕机 |
| 原因 | 恶意请求 / 不存在的 ID | 热点数据过期 | 批量设相同 TTL / Redis 故障 |
| 影响范围 | 单个 Key | 单个热点 Key | 全局 / 大范围 |
| DB 压力 | 持续但量小 | 短时间巨大 | 瞬间巨大 |
| 核心解决思路 | 挡住不存在的请求 | 保护热点 Key,串行化查 DB | 打散过期时间 + 兜底机制 |
2.2 缓存穿透解决方案
方案一:空值缓存
优点:简单粗暴,代码改动小 缺点:攻击者用随机 UUID 轰炸,缓存里堆满垃圾 Key,内存浪费
方案二:布隆过滤器
布隆过滤器核心原理:一个超长的 bit 数组 + 多个哈希函数。插入时把多个哈希位置 1,查询时所有位都为 1 才认为"可能存在"。
| 特性 | 说明 |
|---|---|
| 误判率 | 可能把"不存在"判断为"存在",但绝不会把"存在"判断为"不存在" |
| 不可删除 | 标准布隆过滤器不支持删除(会误删其他 Key 的位) |
| Redis 实现 | RedisBloom 模块(BF.ADD、BF.EXISTS) |
| 本地实现 | Google Guava BloomFilter |
💡 面试怎么说: "我们线上用的是布隆过滤器方案。用户 ID 注册时写入布隆过滤器,查询请求先过布隆过滤器,如果判断不存在就直接返回,不会打到 DB。布隆过滤器放在 Redis 里用 RedisBloom 模块,也可以用 Guava 在应用本地做一层。布隆过滤器的缺点是不能删除、有一定误判率,但对我们的场景够用了——我们允许极少量请求穿透到 DB,但不能让大量恶意请求把 DB 打挂。"
2.3 缓存击穿解决方案
方案一:互斥锁(分布式锁)
优点:数据一致性没问题 缺点:吞吐量下降,其他线程要等锁
方案二:逻辑过期
优点:不阻塞,高性能 缺点:短暂返回旧数据,一致性有妥协
| 方案 | 一致性 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 互斥锁 | 强一致 | 较差 | 低 | 对一致性要求高的场景 |
| 逻辑过期 | 弱一致 | 极好 | 中 | 热点数据,允许短暂不一致 |
2.4 缓存雪崩解决方案
手段一:随机 TTL
手段二:多级缓存
手段三:限流降级
2.5 生产案例:一次缓存雪崩的排查
背景:2025 年双 11 大促前压测,某电商系统 QPS 到 8000 时 DB CPU 飙到 95%,RT 从 50ms 涨到 2 秒。
排查链路:
💡 面试怎么说: "我们之前压测时遇到过一次缓存雪崩,根因是运维批量刷配置时把一批商品缓存 TTL 设成了相同值,1 小时后集中过期,请求全打到 DB 上。我们的排查思路是先看 DB 慢查询日志发现都是简单查询不该慢,再看 Redis 发现大量 Key 同时过期。紧急处理是限流保护 DB + 脚本刷缓存。后续做了三个优化:TTL 加随机偏移、引入本地 Caffeine 做多级缓存、上线热点 Key 探测。"
3. 分布式锁
3.1 演进路线
第一阶段:SETNX
问题:setnx 和 expire 之间进程挂了 → 死锁。
第二阶段:SET NX EX
问题:锁 30 秒过期,但业务执行了 40 秒 → 锁自动释放 → 线程 B 拿到锁 → 线程 A 执行完把 B 的锁删了。
解决:value 存 requestId,删锁时校验 + Lua 保证原子性:
第三阶段:Redisson(生产标配)
3.2 Redisson 核心原理
Watch Dog 自动续期
默认锁超时 30 秒(
lockWatchdogTimeout),每 10 秒续期一次。只要持锁线程还活着,锁就不会过期。
Lua 脚本加锁(简化版)
注意 Redisson 的锁用的是 Hash 结构,
field是线程标识,value是重入次数。所以 Redisson 的锁是可重入的。
3.3 Redisson 各种锁
| 锁类型 | 说明 | 适用场景 |
|---|---|---|
| 可重入锁(ReentrantLock) | 同一个线程可以多次加锁,计数递增 | 最常用,默认选择 |
| 公平锁(FairLock) | 按请求顺序排队获锁 | 防饥饿,但有性能损耗 |
| 联锁(MultiLock) | 多个锁打包成一个,全部加锁成功才算成功 | 跨资源操作 |
| 红锁(RedLock) | 多个独立 Redis 节点上分别加锁 | 对一致性要求极高的场景(有争议) |
| 读写锁(ReadWriteLock) | 读读共享、读写互斥 | 读多写少场景 |
3.4 RedLock 争议
RedLock 做法:在 N 个(通常 5 个)独立的 Redis 节点上加锁,超过半数(N/2+1)加锁成功才算成功。
Martin Kleppmann 的质疑(经典博文 "How to do distributed locking"):
时钟跳跃问题:Redis 用的是墙钟(wall clock),GC 停顿、网络延迟都可能导致时钟不准
没有 fencing token:无法检测过期锁
GC 停顿:客户端拿到锁后 GC 停顿,锁过期了都不知道
Antirez 的反驳:
RedLock 的设计目标就是在无可靠时钟的环境工作
如果真需要强一致分布式锁,应该用 ZooKeeper
业界共识:
| 方案 | 一致性 | 性能 | 推荐度 |
|---|---|---|---|
| 单节点 Redisson | AP(高可用) | 极高 | ⭐⭐⭐⭐⭐ 大部分场景够用 |
| RedLock | 介于 AP 和 CP 之间 | 较高 | ⭐⭐⭐ 极端场景才考虑 |
| ZooKeeper 锁 | CP(强一致) | 较低 | ⭐⭐⭐⭐ 对一致性要求高时 |
💡 面试怎么说: "生产上我们用的是 Redisson 单节点分布式锁,配合 Watch Dog 自动续期。RedLock 虽然理论上更安全,但实际引入的复杂度很高——需要多个独立 Redis 节点,还有 Martin Kleppmann 提出的时钟问题。如果业务真的需要强一致的分布式锁,我觉得不如直接用 ZooKeeper 或者 etcd。大部分互联网场景用 Redisson 就够了,关键是理解它的原理和边界。"
3.5 生产使用建议
锁的 TTL 设合理:虽然 Redisson 有 Watch Dog,但建议业务超时时间也设好,防止异常情况
加 try-finally:确保
unlock()一定执行避免锁粒度太大:锁订单就别锁用户,锁行就别锁表
锁竞争监控:加 metrics 监控锁等待时间、获取失败率
降级方案:锁获取失败时有兜底逻辑,别直接报错
4. Redis 持久化
4.1 RDB(快照)
原理:某个时间点的全量数据快照,保存为 .rdb 二进制文件。
触发方式
| 方式 | 说明 |
|---|---|
save | 阻塞主线程,生产禁用 |
bgsave | fork 子进程,不阻塞主线程 |
| 配置自动触发 | save 900 1(900秒内至少1次写入就触发) |
| 其他命令隐式触发 | shutdown、slaveof、debug reload |
fork + COW(Copy-On-Write)流程:
| 优点 | 缺点 |
|---|---|
| 文件紧凑,恢复速度快 | fork 时可能阻塞(大内存页表复制) |
| 适合备份和灾备 | 两次快照之间的数据可能丢失 |
| 对主进程性能影响小 | 大内存实例 fork 耗时长 |
⚠️ 关键指标:
lazyfree-lazy-eviction、lazyfree-lazy-expire这些配置要打开,避免主线程做删除操作时阻塞。
4.2 AOF(追加日志)
原理:每条写命令追加到 AOF 文件末尾。
写策略(fsync 策略)
| 策略 | 说明 | 数据安全 | 性能 |
|---|---|---|---|
always | 每条命令都 fsync | 最高,最多丢 1 条 | 最差 |
everysec(默认) | 每秒 fsync 一次 | 最多丢 1 秒数据 | 很好 |
no | 由 OS 决定何时 fsync | 不确定 | 最好 |
AOF Rewrite(重写)
自动触发配置:
auto-aof-rewrite-percentage 100+auto-aof-rewrite-min-size 64mb
4.3 混合持久化(4.0+)
| 方案 | 恢复速度 | 数据安全 | 文件体积 |
|---|---|---|---|
| 纯 RDB | ⭐⭐⭐⭐⭐ 极快 | ⭐⭐ 可能丢很多 | 小 |
| 纯 AOF | ⭐⭐ 慢(要重放命令) | ⭐⭐⭐⭐⭐ 最多丢 1 秒 | 大 |
| 混合持久化 | ⭐⭐⭐⭐ 快 | ⭐⭐⭐⭐⭐ 最多丢 1 秒 | 较小 |
💡 面试怎么说: "生产上我们用的是混合持久化,Redis 4.0 以后引入的。AOF 重写时前半段用 RDB 格式保存全量数据,后半段用 AOF 格式追加增量命令。这样恢复速度快(不用全量重放 AOF),数据也不容易丢。持久化策略用的
everysec,最多丢 1 秒数据。另外我们还会定期把 RDB 文件备份到 OSS,做灾备用。"
4.4 生产持久化配置建议
5. Redis 集群方案
5.1 主从复制
原理:
repl_backlog 的作用:
5.2 哨兵模式(Sentinel)
架构:1 主 N 从 + M 个 Sentinel 节点
核心功能:
监控:Sentinel 定期 PING 主从节点
自动故障转移:主节点挂了,Sentinel 投票选出新主
通知:故障转移后通知客户端新的主节点地址
故障转移流程:
从节点选择优先级:
5.3 Cluster 模式
架构:N 个主节点,每个主节点有从节点做故障转移。
核心设计:
Gossip 协议:
扩缩容:
5.4 三种方案对比
| 维度 | 主从复制 | 哨兵模式 | Cluster |
|---|---|---|---|
| 数据分布 | 全量数据在每个节点 | 全量数据在每个节点 | 分片存储 |
| 高可用 | ❌ 需要手动切换 | ✅ 自动故障转移 | ✅ 自动故障转移 |
| 水平扩展 | ❌ 不能 | ❌ 不能(单主瓶颈) | ✅ 天然支持 |
| 运维复杂度 | 低 | 中 | 高 |
| 客户端 | 普通客户端 | 需要 Sentinel 客户端 | 需要 Cluster 客户端 |
| 最大数据量 | 受单机内存限制 | 受单机内存限制 | 理论上无上限 |
| 适用场景 | 读多写少 + 灾备 | 中小规模高可用 | 大规模、高并发 |
💡 面试怎么说: "生产环境我推荐用 Cluster 模式。主从复制只能做读写分离和灾备,没有自动故障转移能力;哨兵模式解决了高可用问题,但单主节点的写入能力有上限,数据量受单机内存限制。Cluster 模式既能水平扩展又能自动故障转移,适合大规模高并发场景。当然运维复杂度会高一些,槽位迁移、客户端路由都要处理好。我们线上是 6 主 6 从的 Cluster,每个主节点大概 4~8GB 数据。"
6. Redis 内存管理
6.1 内存淘汰策略
当 Redis 内存使用达到 maxmemory 限制时,就需要淘汰数据。
| 策略 | 淘汰范围 | 淘汰算法 | 适用场景 |
|---|---|---|---|
| noeviction | 不淘汰 | - | 写入报错,适合纯缓存不适合的场景 |
| allkeys-lru | 所有 Key | LRU(最近最少使用) | 通用缓存,最常用 |
| volatile-lru | 有过期时间的 Key | LRU | 只淘汰设了 TTL 的 |
| allkeys-lfu | 所有 Key | LFU(最不经常使用) | 4.0+,按访问频率淘汰 |
| volatile-lfu | 有过期时间的 Key | LFU | 按访问频率淘汰有 TTL 的 |
| allkeys-random | 所有 Key | 随机 | 差不多都要,随机吧 |
| volatile-random | 有过期时间的 Key | 随机 | 同上,但只淘汰有 TTL 的 |
| volatile-ttl | 有过期时间的 Key | TTL 最短的优先 | 优先淘汰快过期的 |
LRU vs LFU:
LRU:最近没访问过的先淘汰。问题:偶尔被访问一次的大 Key 一直不被淘汰
LFU:按访问频率淘汰。4.0 引入,更科学。但新 Key 刚开始频率低可能被误淘汰
💡 面试怎么说: "我们生产用的是
allkeys-lfu,Redis 4.0 引入的。相比 LRU,LFU 按访问频率来淘汰更合理——一个 Key 虽然最近没被访问,但如果它历史访问频率很高,LFU 会倾向保留它。实际效果是缓存命中率比 LRU 高了大概 3~5 个百分点。"
6.2 内存碎片
产生原因:频繁的创建和删除导致内存空洞。
监控指标:
解决方案:
| 方案 | 说明 | 推荐度 |
|---|---|---|
| activedefrag | Redis 4.0+ 内置在线碎片整理 | ⭐⭐⭐⭐⭐ |
| 重启 | 最简单粗暴,重建内存 | ⭐⭐⭐ 有停机时间 |
config set activedefrag yes | 开启主动碎片整理 | ⭐⭐⭐⭐⭐ |
6.3 maxmemory 设置建议
💡 面试怎么说: "maxmemory 我们一般设成物理内存的 60%~70%,因为 Redis 在做 RDB 快照或 AOF 重写时需要 fork 子进程,利用 COW 机制虽然初期共享内存,但如果 fork 期间有大量写入,实际会复制大量内存页。留 30%~40% 就是为了应对这种情况。另外我们还会监控
mem_fragmentation_ratio,超过 1.5 就开启activedefrag做在线碎片整理。"
7. Redis 高可用架构
7.1 热 Key 问题
什么是热 Key:某个 Key 被超高频率访问(如秒杀商品、热搜话题)。
问题:单节点承受所有请求压力,可能导致节点崩溃。
解决方案:
方案一:本地缓存
方案二:读写分离 + 多从节点
方案三:热 Key 探测 + 自动复制
7.2 大 Key 问题
什么是大 Key:
| 类型 | 大 Key 标准 |
|---|---|
| String | > 10KB |
| Hash List Set / ZSet | 元素数 > 5000 或总大小 > 10MB |
危害:
网络带宽:一次传输数据量大
阻塞主线程:
DEL大 Key 会阻塞内存不均:大 Key 所在的 shard 内存偏高
集群倾斜:数据分布不均
解决方案:
7.3 缓存和数据库一致性
这是面试超高频题,核心是保证"缓存数据和数据库数据一致"。
方案一:延迟双删
问题:延迟时间不好确定,还是有极小窗口不一致
方案二:Canal 订阅 Binlog(推荐)
| 方案 | 一致性 | 复杂度 | 推荐度 |
|---|---|---|---|
| Cache Aside(先更新 DB,再删缓存) | 最终一致 | 低 | ⭐⭐⭐⭐ 简单场景够用 |
| 延迟双删 | 最终一致 | 中 | ⭐⭐⭐ |
| Canal + Binlog | 最终一致 | 高 | ⭐⭐⭐⭐⭐ 生产推荐 |
💡 面试怎么说: "我们线上用的是 Canal 订阅 MySQL Binlog 的方案。业务流程只负责更新数据库,Canal 解析 Binlog 变更事件,发到 Kafka,消费端根据变更去更新 Redis 缓存。这样做的好处是业务代码完全解耦,缓存更新是异步的,而且有重试机制保证最终一致性。极端场景下 Canal 延迟可能在几百毫秒到几秒,但这个延迟对大部分业务是可以接受的。"
7.4 多级缓存架构设计
各层职责:
| 层级 | 技术 | 容量 | 过期时间 | 作用 |
|---|---|---|---|---|
| L1:浏览器/CDN | 浏览器缓存、CDN | 用户端 | 分钟~小时 | 挡住静态资源和重复请求 |
| L2:应用本地缓存 | Caffeine / Guava | 小(受 JVM 堆限制) | 秒级 | 挡住热 Key,减轻 Redis 压力 |
| L3:分布式缓存 | Redis Cluster | 大 | 分钟~小时 | 核心缓存层 |
| L4:数据库 | MySQL | 最大 | 永久 | 数据最终来源 |
本地缓存一致性:
8. Redis 7.x 新特性
8.1 Redis Functions(7.0+)
替代传统 EVAL + Lua 脚本的方案。
| 对比 | EVAL + Lua | Redis Functions |
|---|---|---|
| 脚本管理 | 每次都要传脚本内容 | 先注册,后调用(函数名) |
| 版本管理 | ❌ | ✅ 支持多版本 |
| 持久化 | ❌(每次加载) | ✅ 持久化到 RDB / AOF |
| 调试 | 困难 | 更好(有 function stats) |
8.2 Multi-part AOF(7.0+)
好处:
Rewrite 更高效(不用重写整个文件)
管理更清晰
恢复更快
8.3 ACL v2(7.0+)
8.4 Sharded Pub/Sub(7.0+)
8.5 客户端缓存优化(7.0+)
8.6 其他 7.x 重要特性
| 特性 | 版本 | 说明 |
|---|---|---|
| Listpack 替代 Ziplist | 7.0 | 彻底消灭 ziplist |
SHARD 命令 | 7.0 | 查看 Cluster 槽位信息 |
| 多部分 AOF | 7.0 | AOF 文件拆分管理 |
FUNCTION | 7.0 | 替代 EVAL |
EVAL_RO / FCALL_RO | 7.0 | 只读脚本,可路由到从节点 |
| 概率数据结构 | 7.4 | Bloom Filter / Cuckoo Filter 原生支持 |
💡 面试怎么说: "Redis 7.0 有几个我觉得比较重要的改进:一是 Redis Functions 替代了 EVAL + Lua,脚本可以注册和持久化,管理更方便;二是 Multi-part AOF,AOF 文件不再是一个大文件,而是拆分成 base + incr 多个文件,rewrite 更高效;三是 Sharded Pub/Sub,解决了 Cluster 模式下 Pub/Sub 消息冗余传播的问题。另外 7.4 还新增了原生的布隆过滤器支持,不用再依赖 RedisBloom 模块了。"
9. 面试高频问答
Q1:Redis 为什么这么快?
初中级回答:基于内存操作;单线程避免上下文切换;高效数据结构。
高级回答:
"Redis 快的原因有几个层面:第一,纯内存操作,避免磁盘 IO;第二,单线程模型避免了多线程的上下文切换和锁竞争;第三,高效的数据结构设计,如跳表、压缩列表、整数集合等;第四,IO 多路复用(epoll)处理大量并发连接。但要注意,Redis 6.0 之后引入了多线程,主要是为了处理网络 IO 的并行化,命令执行还是单线程的。多线程处理网络读写,单线程执行命令,这样既利用了多核 CPU 的网络处理能力,又避免了命令执行的锁竞争。"
Q2:Redis 单线程为什么能处理高并发?
高级回答:
"Redis 用的是 IO 多路复用(epoll),一个线程就能同时监听大量 socket 连接。当某个连接有数据可读时,epoll 会通知 Redis,Redis 再去读取和处理。这样即使单线程也能处理数万并发连接。加上 Redis 的命令执行大多是 O(1) 或 O(logN) 的,单次执行时间极短(微秒级),所以吞吐量非常高,单实例能到 10W+ QPS。"
Q3:Redis 6.0 为什么引入多线程?
高级回答:
"Redis 6.0 的多线程是为了解决网络 IO 瓶颈。当 QPS 非常高时,网络读写成为瓶颈——单线程处理网络 IO 来不及了。6.0 引入了线程池来处理网络读写和协议解析,但命令执行仍然是单线程的,所以不需要同步锁,保持了原有的简单性。本质上是用多线程加速 IO,命令执行依然是单线程保证原子性。"
Q4:Redis 的过期策略和内存淘汰有什么区别?
高级回答:
"这是两个不同的机制。过期策略是处理设了 TTL 的 Key,有惰性删除(访问时检查)和定期删除(每 100ms 随机检查一批)两种。内存淘汰是 maxmemory 达到上限时的策略,和过期策略无关——淘汰策略决定哪些 Key 被踢出,可能是有 TTL 的也可能是没有的。简单说,过期是时间到了自动删,淘汰是内存不够了主动踢。"
Q5:Redis 如何实现分布式锁?有什么问题?
(详见第 3 章分布式锁部分,面试时重点说:SET NX EX + Lua 删锁 + Redisson Watch Dog + RedLock 争议)
Q6:缓存和数据库一致性怎么保证?
(详见 7.3 节,面试时重点说 Canal + Binlog 方案)
Q7:Redis Cluster 的数据分片原理?
高级回答:
"Redis Cluster 用哈希分片,16384 个 slot 分配到各主节点。Key 通过 CRC16(key) % 16384 计算所属 slot,找到对应节点。如果访问了错误的节点,会返回 MOVED 重定向。在 slot 迁移过程中会返回 ASK 重定向。节点间通过 Gossip 协议交换状态,PING/PONG 做心跳,PFAIL/FAIL 做故障检测。"
Q8:什么是缓存穿透?怎么解决?
(详见第 2 章,面试时重点说布隆过滤器原理 + 空值缓存方案)
Q9:Redis 持久化怎么选?
(详见第 4 章,面试时推荐混合持久化,说明 RDB + AOF 各自优缺点)
Q10:Redis 内存淘汰策略你用的哪个?为什么?
高级回答:
"我们用的是 allkeys-lfu。Redis 4.0 引入的 LFU 比 LRU 更合理——LRU 只看最近访问时间,一个偶尔被访问的 Key 可能长期不被淘汰;LFU 综合考虑了访问频率,更能反映 Key 的热度。实际测试中 LFU 的缓存命中率比 LRU 高 3~5%。当然如果 Key 都是均匀访问的,两者差别不大,但真实业务中访问频率往往符合幂律分布,LFU 优势明显。"
Q11:如何发现和处理大 Key?
高级回答:
"发现方面:我们用
redis-cli --bigkeys做初步扫描,更精确的会用 redis-rdb-tools 分析 RDB 文件,生成大 Key 报告。处理方面:首先业务层面避免大 Key 设计——Hash 超过 5000 个字段就考虑拆分;其次删除时用UNLINK异步删除,避免DEL阻塞主线程;对于已经存在的大 Key,用HSCAN分批删除。我们还会在监控系统里加大 Key 告警,超过阈值自动通知。"
Q12:热 Key 问题怎么解决?
高级回答:
"热 Key 会导致单节点压力过大。我们的方案是多层防护:第一层是本地缓存(Caffeine),设置很短的过期时间(3~5 秒),挡住绝大部分请求;第二层是 Redis 读写分离,把读请求分散到从节点;第三层是热 Key 探测框架(如京东 hotkey),自动检测热 Key 并在多个节点创建副本。同时我们会在监控里加热点 Key 探测,超过阈值的 Key 自动告警,运维可以提前处理。"
Q13:Redis 的 ZSet 底层为什么用跳表不用红黑树?
高级回答:
"Redis 作者 Antirez 的解释有几个原因:第一,跳表实现更简单,代码可读性好,红黑树的旋转操作复杂;第二,跳表范围查询更友好——找到起点后沿着链表顺序遍历即可,红黑树需要中序遍历;第三,跳表通过调整层数可以改变时间复杂度,灵活性更好;第四,跳表的内存局部性更好,对 CPU 缓存更友好。虽然红黑树查找是严格的 O(logN),跳表期望 O(logN) 但最坏 O(N),但实际表现差不多。"
Q14:布隆过滤器的原理和局限?
高级回答:
"布隆过滤器本质上是一个 bit 数组 + k 个哈希函数。插入元素时,用 k 个哈希函数计算 k 个位置,全部置 1。查询时,检查 k 个位置是否全为 1——全为 1 才认为可能存在。局限:1)不支持删除(标准版),因为删除可能影响其他元素;2)有误判率,可能把不存在的判断为存在,但不会把存在的判断为不存在;3)随着元素增多,误判率上升。生产上我们用 RedisBloom 模块,支持动态扩容和计数布隆过滤器(支持删除)。"
Q15:Redis 主从复制的全量同步流程?
高级回答:
"首次同步或断线太久(超过 repl-diskless-sync-max-replicas)会触发全量同步:1)从节点发送 PSYNC 命令,主节点判断需要全量同步回复
? -1;2)主节点执行 BGSAVE fork 子进程生成 RDB 快照,期间主进程继续处理请求,新命令写入 repl_backlog;3)RDB 生成后发送给从节点,从节点加载;4)主节点把 repl_backlog 中的增量命令发给从节点执行。增量复制时走 PSYNC 的偏移量机制,从节点告诉主节点自己的偏移量,主节点从 repl_backlog 中找到对应位置发送增量数据。"
Q16:Watch Dog 续期机制如果 Redis 主节点挂了怎么办?
高级回答:
"Watch Dog 续期依赖于客户端和 Redis 的连接。如果用的是单节点 + 哨兵,主节点挂了触发故障转移,客户端连接到新主节点后可以重新加锁。如果用的是 Cluster,原理类似。但如果锁还没续期成功主节点就挂了,可能出现两个客户端同时拿到锁的情况——这就是 RedLock 要解决的问题。不过 RedLock 本身也有争议,所以我们生产上更倾向于接受这种极端情况下的短暂不一致,或者在业务层做幂等性保障。"
Q17:Redis 的 Pipeline 和 Lua 脚本有什么区别?
高级回答:
"Pipeline 是客户端批量发送命令,减少网络 RTT,但服务端是逐条执行的,不保证原子性——中间可能插入其他客户端的命令。Lua 脚本在服务端原子执行,执行期间不会处理其他命令,保证原子性。但 Lua 脚本如果执行时间太长会阻塞主线程(Redis 有 busy-command 超时机制 5 秒限制)。选型建议:只需要减少网络往返用 Pipeline,需要原子性用 Lua。"
Q18:如何保证 Redis 和 MySQL 的数据不丢?
高级回答:
"这是两个维度的问题。Redis 不丢数据靠持久化——我们用混合持久化(RDB + AOF),最多丢 1 秒数据,再加上 RDB 定期备份到 OSS 做灾备。MySQL 不丢靠主从复制 + binlog。两者之间的一致性靠 Canal 订阅 binlog 异步更新缓存。极端场景比如 Canal 延迟期间读到的可能是旧数据,业务层通过版本号或乐观锁做最终一致性保障。"
Q19:Redis 的 IO 多路复用原理?
高级回答:
"Redis 用的是 epoll(Linux)作为 IO 多路复用机制。核心思想是一个线程同时监听多个 socket 描述符,当某个 socket 有数据可读或可写时,epoll 会返回就绪的描述符列表。Redis 主循环不断调用 epoll_wait 获取就绪事件,然后依次处理。这样单线程就能高效处理数万并发连接,避免了每个连接一个线程的开销。相比 select 和 poll,epoll 的时间复杂度是 O(1)(基于红黑树 + 就绪链表),效率更高。"
Q20:Redis 集群模式下,客户端怎么知道数据在哪个节点?
高级回答:
"客户端本地维护一个 slot → 节点的映射表。请求时 CRC16(key) % 16384 算出 slot,查映射表找到目标节点直接发送。如果节点信息过期,目标节点会返回 MOVED 重定向,客户端更新本地映射表。如果 slot 正在迁移中,返回 ASK 重定向到目标节点,但客户端不更新映射表(因为迁移还没完成)。Redis 的 Java 客户端如 Jedis、Lettuce 都实现了这个逻辑。我们生产用 Lettuce,它基于 Netty,性能更好,而且天然支持异步和 Cluster 模式。"
附录:Redis 面试速记口诀
📌 使用建议:
面试前 1 天:重点看第 2、3、7 章(缓存三兄弟、分布式锁、高可用)
面试前 3 天:通读全文,每个知识点看「面试怎么说」
面试前 1 周:系统学习,配合实际项目经验,把知识点串成故事
面试时:先给结论,再展开原理,最后说生产实践
本文档由面试备战经验整理,持续更新中。
本内容由 Coze AI 生成,请遵循相关法律法规及《人工智能生成合成内容标识办法》使用与传播。