附录:面试高频考点速记
| 主题 | 核心考点 | 关键词 |
|---|---|---|
| 缓存穿透 | 参数校验 + 布隆过滤器 + 空值缓存 | 不存在的请求、三层防护 |
| 缓存击穿 | 分布式锁 + 逻辑过期 + 预热 | 热点 key 过期、并发查 DB |
| 缓存雪崩 | TTL 扰动 + 高可用 + 熔断限流 | 大量 key 同时失效 |
| 双写一致性 | 延迟双删 + 异步通知 + Canal | 最终一致性、先更 DB 后删缓存 |
| 持久化 | RDB + AOF + 混合持久化 | BGSAVE、COW、fsync |
| 过期删除 | 惰性删除 + 定期删除 | 随机采样、hz 参数 |
| 内存淘汰 | 8 种策略、LRU vs LFU | allkeys vs volatile |
| 分布式锁 | SET NX PX + 看门狗 + Redlock | 唯一 value、Lua 脚本 |
| 主从复制 | 全量同步 + 增量同步 | replid、offset、Backlog |
| 哨兵模式 | 监控 + 故障转移 + 选举 | 主观/客观下线、脑裂 |
| Cluster | 16384 哈希槽 + Gossip | CRC16、水平扩展 |
| 单线程模型 | 内存操作 + IO 多路复用 | epoll、无锁、高效数据结构 |
一、缓存穿透
面试官:你的项目里用到了 Redis 做缓存,有没有遇到过缓存穿透的问题?能详细说说是什么吗?会带来什么风险?你们是怎么解决的?
面试者回答:
这个问题在项目中确实遇到过,我们当时做了一套比较系统的防护方案。
先说下缓存穿透的定义:请求的数据在缓存中不存在,在数据库中也不存在。这种请求每次都会绕过 Redis 直接打到数据库。如果有人恶意构造大量非法请求,比如狂刷不存在的 ID,数据库压力会剧增,严重的时候能把 DB 直接打挂。
举个我们项目的实际例子:
在用户详情接口里,前端传 userId 过来,我们的查询链路是先查 Redis,没命中再查 MySQL。但如果有人一直请求 userId = -1、userId = -2 这种明显不合法的值,系统又没做拦截的话,这些请求就会全部落到数据库上。
我们的解决方案是做了三层防护:
第一层:参数校验
请求刚进服务的时候,先对关键参数做合法性检查。比如 userId 必须是正整数、长度在合理范围内。不符合规则的直接返回 400,连缓存都不查。这一步成本极低,但能拦掉大量低级恶意请求。
第二层:布隆过滤器前置过滤
过了参数校验的请求,我们会用布隆过滤器判断这个 ID 是否"可能存在"。
布隆过滤器的原理是这样:程序启动时,我们会把所有有效的主键加载到布隆过滤器中。当查询到来时,如果布隆过滤器返回"不存在",那这个 ID 一定不存在,直接拒绝请求;如果返回"可能存在",才继续往下走查缓存和数据库的流程。
布隆过滤器有个特点:它可能会误判(说不存在的其实存在),但绝不会漏判(说存在的其实不存在)。所以用它来做"快速否定"非常合适。
第三层:空值缓存兜底
对于极少数绕过前两层的情况,比如某个 userId 合法但用户确实被删除了,数据库查不到数据时,我们会把这个 key 对应的 value 缓存为一个特殊标记,比如 "NULL",然后设置一个比较短的过期时间,一般是 2~5 分钟。这样相同请求在短时间内就不会再穿透到数据库了。
这三层防护加起来,相当于给系统上了三道保险:
- 第一道拦明显不合法的请求
- 第二道处理看起来正常但实际不存在的 ID
- 第三道兜底,防止万一前两道没拦住
从实际效果来看,数据库的异常查询量下降了 90% 以上。
二、缓存击穿
面试官:你能解释一下什么是"缓存击穿"吗?你有没有在实际项目中遇到过?是怎么解决的?
面试者回答:
缓存击穿和穿透不一样,它指的是某个热点 key 在缓存中过期的瞬间,大量并发请求同时发现这个 key 不在缓存了,于是全都去查数据库。
因为数据库的 QPS 承受能力远低于 Redis,Redis 能扛 10w+ QPS,但 MySQL 可能只有几千。这一下几千个请求全打到 DB 上,很容易把连接池打满,甚至引发服务雪崩。
我们电商平台的商品详情页就遇到过这个问题,后来我们针对不同场景做了差异化处理:
对于普通商品:分布式锁 + 本地缓存兜底
当 Redis 缓存 miss 的时候,我们通过 SET key value NX EX 5 尝试获取分布式锁。NX 保证原子性,EX 设置 5 秒自动释放防止死锁。
拿到锁的线程负责查 DB 并回写缓存;没拿到锁的线程就短暂自旋重试(比如 sleep 50ms 后再读一次 Redis),避免所有请求都打到 DB。
同时我们在应用层加了一层 Caffeine 本地缓存,即使 Redis 短暂不可用,也能扛住一部分压力。
对于 Top 100 热门商品:逻辑过期 + 定时预热
对于这种绝对热点,我们不能让用户请求等锁。我们的做法是:
- 缓存不设物理 TTL(永不过期),但在 value 里嵌入一个逻辑过期时间戳
- 请求读到数据后判断是否逻辑过期,如果过期了,不阻塞当前请求(继续返回旧数据),而是异步触发一个刷新任务去更新缓存
- 另外部署了一个独立的预热服务,每 5 分钟主动拉取 Top 商品数据更新缓存,确保缓存始终"热着"
这种方式的核心思想是:热点数据不能等过期了再重建,要提前预热;万一过期了也要异步更新,不能阻塞用户请求。
三、缓存雪崩
面试官:你能说说什么是 Redis 的缓存雪崩吗?针对"大量 Key 同时过期"导致的雪崩,你有哪些解决方案?实际项目中遇到过吗?
面试者回答:
缓存雪崩分两种情况:
- 大量缓存在同一时间失效(比如都设置了相同的 TTL),导致请求瞬间穿透到数据库
- Redis 服务整体宕机,所有缓存不可用
举个例子:一个商品中心有 10 万个商品缓存,TTL 都设为 2 小时,整点一到全部失效,几万 QPS 直接打到数据库,很容易把 DB 打挂。
我们项目中用了多种策略组合来防御:
策略一:随机过期时间(TTL 扰动)
在基础过期时间上加一个随机偏移量,比如 2小时 + random(0~10分钟),让缓存失效时间分散开。
这个方法实现简单,能缓解大部分集中失效问题。但有个缺点:在批量初始化缓存的场景下,热点数据仍可能集中过期。
策略二:永不过期 + 逻辑过期 + 异步更新
缓存不设物理过期时间,在 value 里嵌入逻辑过期时间戳。请求读取时判断是否过期,如果过期就触发异步任务刷新缓存,而不是同步回源查 DB。
这个方法能彻底规避雪崩,特别适合核心高频数据。但实现复杂,而且可能返回略微陈旧的数据(最终一致性),如果异步更新失败还可能长期使用脏数据。
策略三:Redis 高可用架构
部署 Redis Sentinel 或 Redis Cluster,避免单点故障导致全量缓存不可用。
Sentinel 能在主节点宕机时自动故障转移,Cluster 通过分片支持多主多从。但运维复杂度会显著上升,要处理网络分区、脑裂、数据迁移等问题,故障切换期间也可能有秒级不可用。
策略四:应用层熔断限流
用 Alibaba Sentinel 对访问数据库的关键接口配置 QPS 限流、错误率熔断和降级策略。一旦 DB 压力过大,就快速失败或返回兜底数据。
这个策略能在雪崩发生初期切断冲击,防止级联故障。但用户体验可能受影响,而且规则配置不当容易误熔断。
实际项目案例:
我们一个大促活动页面的配置缓存全部在整点过期,预估峰值 QPS 超过 5 万。最终采用了 TTL 扰动 + 异步预热 + Sentinel 限流 的三重防护:
- 提前用定时任务在过期前 5 分钟异步刷新热点数据
- 对 DB 查询接口设置熔断阈值
最终活动期间数据库负载平稳,系统零故障。
总结一句话:缓存雪崩不能靠单一手段解决,要从缓存设计、架构高可用、应用层防护三个层面协同防御。
四、双写一致性
面试官:什么是 Redis 双写一致性问题?有哪些方案可以保证缓存和数据库的一致性?不同方案的适用场景是什么?
面试者回答:
Redis 双写一致性问题是说:同时使用数据库和缓存时,更新操作无法保证原子性,导致两者数据会出现短暂不一致。
这个问题的根源在于:Redis 和 MySQL 是两个独立的系统,我们无法用一个事务同时操作它们。
针对这个问题,我会根据业务对一致性的容忍度来选型,分最终一致和强一致两类:
绝大多数场景:最终一致性方案
最常用的方案是 Cache-Aside 模式下的延迟双删:
- 先更新数据库
- 再删除缓存
- 延迟一小段时间(比如 500ms)后二次删除缓存
为什么要延迟双删?这是为了兜底一个极端情况:第一次删除缓存后,有并发读请求把旧数据又刷回缓存了。延迟第二次删除可以把这个旧数据清掉。
这个方案实现简单,适合并发中等、允许毫秒级不一致的业务。
如果系统已经集成了 MQ,我会选择 异步通知方案:
- 更新 DB 后发送删除消息
- 由消费者保证最终删除成功
- 通过消息重试机制解决删除失败问题
这个方案性能更好,而且解耦。
如果业务对代码侵入敏感,或者需要同步来自 SQL 工具的直接修改,我会引入 Canal 监听 Binlog:
- 由 Binlog 事件触发缓存删除
- 完全无代码侵入
- 但要接受秒级延迟
极端场景:强一致性方案
只有面对库存扣减、金融交易这种对一致性要求极高的场景,我才会考虑分布式读写锁:
- 写操作加写锁阻塞并发读
- 保证在更新周期内无人读到旧数据
但这类方案并发能力差,我通常只在数据库层面通过悲观锁解决,缓存层只做降级兜底。
核心观点:在分布式系统中,追求绝对的强一致性代价太高,大多数业务场景最终一致性就够用了。
五、持久化机制
面试官:Redis 有哪些持久化方式?RDB 和 AOF 各自的工作原理是什么?优缺点?如何选择?
面试者回答:
Redis 主要提供两种持久化方式:RDB 快照 和 AOF 日志,从 4.0 开始还支持混合持久化。
RDB(Redis Database)
RDB 本质上是内存快照,把某个时间点的完整数据集以二进制格式写入磁盘。
这里有个常见误区:RDB 本身只是数据格式,真正执行快照的是 BGSAVE 命令。BGSAVE 会 fork 出一个子进程,利用操作系统的 Copy-On-Write(COW)机制,在子进程中完成文件写入,主进程几乎不受影响。
注意:生产环境绝对不能用 SAVE 命令,它是同步阻塞的,会卡住主线程。
RDB 的优点:
- 文件紧凑、恢复快,适合做冷备、迁移或灾难恢复
- 对主线程影响小
RDB 的缺点:
- 两次快照之间的数据可能丢失
- fork 子进程时如果内存很大,会有短暂的内存压力和 CPU 开销
AOF(Append Only File)
AOF 记录的是每个写命令的日志,以文本形式追加到 .aof 文件末尾。重启时通过重放这些命令重建数据。
AOF 的核心在于 fsync 策略:
always:每次写都刷盘,最安全但性能差everysec:每秒刷一次,是默认推荐值,平衡了安全与性能no:由 OS 决定,风险最高
为了避免 AOF 文件无限膨胀,Redis 提供 AOF 重写(BGREWRITEAOF):
- fork 子进程,根据当前内存状态生成一个最小化的 AOF 文件
- 重写期间的新写入会同时写入旧 AOF 和重写缓冲区,保证不丢数据
AOF 的优势:
- 数据更安全(配合 everysec 最多丢 1 秒)
- 日志可读、便于审计
AOF 的劣势:
- 文件大、恢复慢
- 高写负载下 I/O 压力大
混合持久化(Redis 4.0+)
开启 aof-use-rdb-preamble yes 后,AOF 重写时会先写一份 RDB 格式的全量数据,再追加增量命令。重启时先快速加载 RDB 部分,再重放少量 AOF,兼顾速度和完整性。
选型建议:
- 缓存类场景(如用户会话),允许少量丢失:只开 RDB + 定期 BGSAVE 就够了
- 核心数据(比如库存、订单计数):必须用 AOF + everysec + 混合持久化
六、过期删除策略
面试官:Redis 的 key 设置了过期时间后,它是怎么删除的?有哪些删除策略?
面试者回答:
Redis 设置了过期时间的 key,并不会在到期那一刻立即被删除,而是通过 惰性删除 和 定期删除 两种策略配合来清理。
惰性删除
原理是:key 到期后不主动清理,等到下次有客户端访问这个 key 时,Redis 才检查它是否已过期。如果已过期,先删除再返回空值给客户端。
优点:对 CPU 非常友好,不产生额外的扫描开销,只在访问时触发,成本极低。
缺点:如果某个过期 key 长期无人访问,它就会一直占用内存,可能造成内存浪费。
定期删除
原理是:Redis 内部有个时间事件,由 hz 参数控制触发频率(默认 hz=10,也就是每 100ms 触发一次)。每次触发时:
- 从设置了过期时间的 key 集合中随机采样 20 个
- 删除其中已过期的 key
- 如果本批采样中过期 key 的比例超过 25%,就立即再次执行,直到比例低于 25%
为什么用随机采样而不是遍历全部?因为过期 key 可能非常多,全量遍历会阻塞主线程。随机采样是在 CPU 效率和内存回收之间做的权衡。
两种策略的配合
- 频繁访问的"热 key",主要靠惰性删除及时清理
- 长期不访问的"冷 key",则依赖定期删除慢慢回收
这种设计在性能和内存之间取得了很好的平衡。但要记住:Redis 不能保证过期 key 会被立即删除,只是最终会被清理掉。
七、内存淘汰策略
面试官:Redis 内存满了怎么办?有哪些淘汰策略?allkeys 和 volatile 的区别,LRU 和 LFU 的区别?
面试者回答:
当 Redis 配置了 maxmemory 限制,且内存使用达到上限时,如果继续写入新数据,Redis 就必须决定如何释放空间。这就是内存淘汰机制。
Redis 提供了 8 种淘汰策略,我先说核心区别,再说具体策略。
allkeys vs volatile 的区别
这是两个不同的淘汰范围:
allkeys:作用于数据库中所有的 keyvolatile:只作用于设置了过期时间的 key
如果用 volatile 类策略但数据库中没有带 TTL 的 key,那就无法淘汰数据,会导致写入失败。而 allkeys 总能找到可淘汰的 key。
8 种策略快速对比:
| 策略 | 淘汰范围 | 算法 | 适用场景 |
|---|---|---|---|
| noeviction(默认) | 不淘汰 | - | 严禁数据丢失的核心业务 |
| allkeys-lru | 全部 key | 近似 LRU | 通用缓存场景 |
| volatile-lru | 有过期时间的 key | 近似 LRU | 混合存储场景 |
| allkeys-random | 全部 key | 随机 | 测试环境、数据价值均等 |
| volatile-random | 有过期时间的 key | 随机 | 临时数据价值相近 |
| volatile-ttl | 有过期时间的 key | 剩余 TTL 最短 | 时间敏感型业务 |
| allkeys-lfu | 全部 key | 近似 LFU | 热点长期稳定的场景 |
| volatile-lfu | 有过期时间的 key | 近似 LFU | 带有效期的热度缓存 |
LRU vs LFU 的区别
这是两个不同的淘汰算法:
LRU(Least Recently Used,最近最少使用)
- 基于"最近访问时间",认为最近没被访问的数据未来也不太可能被访问
- Redis 的 LRU 是近似实现:每次淘汰时随机采样若干 key(默认 5 个),从中选最久未使用的
- 可通过
maxmemory-samples参数调整采样数,越大越接近真实 LRU,但 CPU 开销也越大
LFU(Least Frequently Used,最不经常使用)
- 基于"访问频率",认为访问次数少的数据价值低
- Redis 的 LFU 使用 24 位计数器:高 16 位记录上次访问时间(分钟级),低 8 位为频率计数
- 频率会随时间衰减(避免历史高频数据永远不被淘汰)
- 新 key 会赋予初始频率偏置(防止刚写入就被淘汰)
适用场景区别:
- LRU 适合访问模式变化快的场景(比如用户会话)
- LFU 适合热点长期稳定的场景(比如首页推荐内容),对突发流量抗干扰能力更强
八、分布式锁
面试官:如何用 Redis 实现分布式锁?有什么问题?
面试者回答:
我会从实现方式、存在的痛点以及架构选型三个层面来说。
基础实现
最基础的方式是利用 Redis 的 SET 命令扩展参数:
SET key value NX PX milliseconds
NX:只有 key 不存在时才能设置成功,保证互斥性PX:给锁设置过期时间(毫秒),防止客户端宕机导致死锁
这里有个关键细节:value 必须是全局唯一标识(比如 UUID + 线程 ID)。因为释放锁时必须通过 Lua 脚本校验当前锁的持有者是否是自己,只有确认一致才能删除。
如果不这么做,会出现问题:客户端 A 的锁过期了,客户端 B 拿到了锁,A 任务完成后误删了 B 的锁。
痛点一:锁过期时间难以设定
这是个两难的问题:
- 时间设短了:业务逻辑还没跑完锁就自动释放了,导致并发安全问题
- 时间设长了:一旦服务宕机,锁要很久才能释放,影响可用性
解决方案是引入 Redisson 的"看门狗"机制:
- 加锁成功后开启一个后台线程
- 每隔一段时间(默认是过期时间的 1/3,比如过期时间 30 秒,每 10 秒检查一次)检查业务是否还在执行
- 如果还在执行就自动续期
这样就解决了业务执行时间不确定的问题。
痛点二:主从架构下的锁丢失
Redis 的主从复制是异步的。假设客户端 A 在 Master 节点加锁成功,但数据还没同步到 Slave 时 Master 就宕机了,Slave 升级为新 Master 后就没有这把锁的数据,此时客户端 B 就能成功加锁,互斥性失效。
针对这个问题,Redis 作者提出了 Redlock 算法:
- 部署多个独立的 Redis Master 节点
- 只有大多数节点加锁成功才算成功
但这个方案实现复杂、性能差,而且在业界存在争议。
架构选型思考
这就引出一个更深层次的问题:
Redis 本质上是遵循 CAP 理论中的 AP(可用性优先) 架构,异步复制机制牺牲了一致性来换取性能。
如果业务对锁的强一致性要求极高(比如金融类场景),强行用 Redis 实现分布式锁是很困难的。这种情况下,更适合选用 CP(一致性优先) 的组件,比如 ZooKeeper:
- ZooKeeper 通过 ZAB 协议保证数据强一致性
- 当写请求(加锁)成功时,数据已经同步到大多数节点
- 天然避免了主从切换丢锁的问题
总结:Redis 分布式锁适合大部分业务场景,但对强一致性要求极高的场景,应该选 ZooKeeper 等 CP 架构组件。
九、主从复制
面试官:Redis 主从复制的原理是什么?主从同步的流程是怎样的?全量复制和增量复制有什么区别?数据一致性如何保障?
面试者回答:
关于 Redis 主从复制,我从核心机制、同步流程、一致性问题三个方面来说。
核心机制
Redis 主从复制默认采用的是异步复制。核心架构是"读写分离":
- 主节点负责写
- 从节点负责读
主节点执行完写命令后,会立即返回给客户端,不会等待从节点确认。这是 Redis 高性能的关键,但也带来了数据延迟的问题。
同步流程
Redis 会根据情况在全量同步 和增量同步之间切换。这里先铺垫三个核心概念:
- Replication ID (replid):主节点的身份 ID
- Offset (复制偏移量):记录数据同步的位置
- Replication Backlog (复制积压缓冲区):主节点维护的一个固定长度的队列(默认 1MB)
全量同步
通常在以下场景触发:
- 从节点第一次连接主节点
- 从节点断开时间过长,导致主节点积压的数据已经超过了 Backlog 缓冲区范围
- 从节点的 replid 和主节点不匹配(说明主从关系变了)
流程是这样的:
- 从节点发送同步请求
- 主节点执行
BGSAVE生成 RDB 快照(在此期间,新来的写命令会写入积压缓冲区) - 主节点把 RDB 发给从节点
- 从节点清空内存并加载 RDB
- 主节点再把缓冲区里积压的增量命令发给从节点
这个过程开销很大,涉及磁盘 IO 和网络传输,所以要尽量避免频繁全量同步。
增量同步
这是 Redis 2.8 之后的重要优化,主要解决网络抖动导致的频繁全量同步问题。
当从节点短暂断线重连时,它会带着自己的 offset 请求同步。主节点判断如果这个 offset 还在积压缓冲区的范围内,就只需要把从节点缺失的那一小部分命令发送过去即可,避免了全量重传。
数据一致性保障
因为 Redis 是异步复制,所以默认情况下无法保证强一致性,只能保证最终一致性。如果主节点写成功但还没同步给从节点就宕机了,这部分数据就会丢失。
我们通常有几种策略应对:
- 最常用的:通过哨兵或 Cluster 机制实现故障自动转移,尽量缩小数据丢失窗口
- 强一致性要求极高时 :使用
WAIT命令,让主节点阻塞等待指定数量的从节点确认收到数据(但会牺牲性能) - 基础保障:主节点必须开启持久化(AOF 或 RDB),防止主节点重启后数据丢失导致从节点数据被清空
十、哨兵模式
面试官:请简述 Redis 哨兵模式的作用是什么?哨兵集群为什么至少需要 3 个节点?哨兵是如何检测 Master 节点故障的?选举新 Master 的规则是什么?会出现脑裂问题吗?如何解决?
面试者回答:
哨兵模式的作用
简单来说,哨兵就是 Redis 的高可用保障。核心作用有三个:
- 监控:实时监控 Master 和 Slave 是否正常运行
- 故障转移:如果 Master 宕机了,自动把一个 Slave 提升为 Master,并更新其他 Slave 的指向
- 通知:充当配置提供者,客户端连接哨兵来获取当前 Master 地址;发生故障转移后及时通知客户端新 Master 是谁
为什么至少需要 3 个节点?
这主要是为了保证"少数服从多数"的选举机制。
哨兵判断 Master 下线并进行故障转移时,需要超过半数的哨兵节点同意:
- 如果是 1 个节点:它挂了系统就瘫痪了
- 如果是 2 个节点:其中 1 个挂了,剩下 1 个刚好是 50%,无法凑成"半数以上",会导致无法进行故障转移
- 所以 3 个节点是保证高可用的最低标准,允许 1 个节点故障,剩下 2 个还能达成多数派
如何检测 Master 故障?
这个过程分两步:
- 主观下线:哨兵节点每隔 1 秒向 Master 发送 PING 命令,如果超时未回复,当前这个哨兵会"主观"认为 Master 下线了
- 客观下线 :当认为 Master 主观下线的哨兵数量达到配置文件中设定的
quorum值(通常是半数以上)时,哨兵集群会判定 Master"客观下线",此时才会真正开始故障转移流程
选举新 Master 的规则
当 Master 客观下线后,哨兵会从剩余的 Slave 节点中按优先级筛选:
- 首先看配置优先级(
replica-priority),数值越小优先级越高 - 如果优先级一样,看复制偏移量(offset),也就是谁从旧 Master 同步的数据最多,谁优先
- 如果数据量也一样,最后看运行 ID(run_id),ID 越小优先级越高
简而言之:优先级高的 > 数据新的 > ID 小的。
脑裂问题
是可能出现的。
举个例子:因为网络分区,Master 还活着,但和哨兵网络断了。哨兵集群误判 Master 下线,选出了新 Master。此时客户端可能还在向旧 Master 写数据,等网络恢复,旧 Master 降级为 Slave,那部分数据就丢失了。
解决方法主要靠配置参数:
min-replicas-to-write 1 # Master 必须有至少 1 个 Slave 在线,我才能写
min-replicas-max-lag 10 # Slave 的心跳延迟不能超过 10 秒
在脑裂场景下,旧 Master 被网络隔离,它会立即失去与所有 Slave 的正常连接(或者延迟飙升超过设定的阈值)。一旦触发条件(比如连接的 Slave 数量为 0,或者延迟都超过了 10 秒),旧 Master 就会主动拒绝所有客户端的写请求,防止数据写入丢失。
十一、Cluster 集群
面试官:Redis Cluster 是什么?为什么要用集群?数据分片原理是什么?Cluster 模式下如何保证高可用?
面试者回答:
什么是 Redis Cluster?
简单来说,Redis Cluster 是 Redis 官方提供的分布式数据库解决方案。
在 Redis 3.0 之前,想做分布式只能靠客户端分片或者借助 Codis 这类代理中间件。而 Cluster 模式是官方原生支持的,实现了数据的分布式存储,允许我们将数据自动分片存储在多个节点上。
为什么要用集群?
核心原因是为了解决单机 Redis 的瓶颈。
虽然 Redis 很快,但单机 Redis 在面对海量数据和高并发场景时,有两个硬伤:
- 内存容量限制:单机内存不可能无限扩充,比如需要存储 100G 数据,单机肯定扛不住
- 读写性能瓶颈:虽然 Redis 单机 QPS 能达到 10 万,但如果业务并发量更高,单机就顶不住了
使用 Cluster 集群,可以实现水平扩展,把数据分散到多台机器上,突破单机的内存和性能限制。
数据分片原理
Redis Cluster 没有采用一致性哈希,而是采用了哈希槽的概念。
整个集群被划分为 16384 个槽位。集群的每个 Master 节点负责处理其中一部分槽位。
当客户端需要对一个 Key 进行读写操作时,Cluster 会计算这个 Key 属于哪个槽位。计算公式是:
CRC16(key) % 16384
这就好比把数据分成了 16384 个格子,然后把这些格子均匀地分给不同的 Master 节点管理。比如:
- 节点 A 负责 0 到 5000 号槽
- 节点 B 负责 5001 到 10000 号槽
- 节点 C 负责 10001 到 16383 号槽
这样做最大的好处是解耦了数据和节点之间的关系。当需要扩容或缩容时,只需要将槽位在节点间迁移即可。比如把节点 A 的 1000 个槽位迁给节点 B,这大大降低了运维复杂度,也保证了数据的均匀分布。
高可用机制
Redis Cluster 的高可用机制和哨兵模式有相似之处:
- 主从复制:Cluster 集群中的每个 Master 节点都可以拥有多个 Slave 节点。Master 负责读写,Slave 负责同步数据
- 故障检测与转移 :
- 集群中的节点之间通过 Gossip 协议互相通信,交换状态信息
- 如果某个 Master 被大多数节点标记为"主观下线",最终被标记为"客观下线"
- 一旦确认 Master 下线,该 Master 对应的 Slave 就会发起选举
- 选举成功后,Slave 升级为新的 Master,接管原来 Master 负责的槽位,继续对外提供服务
十二、单线程模型与 IO 多路复用
面试官:你了解 Redis 的单线程模型吗?为什么单线程还能这么快?能解释一下 IO 多路复用吗?
面试者回答:
Redis 的单线程模型
首先要明确一个概念:Redis 的"单线程"主要指的是处理网络请求和键值对读写的核心逻辑是由一个主线程完成的。
虽然 Redis 4.0 之后引入了多线程来处理异步删除(比如 UNLINK、FLUSHDB ASYNC),Redis 6.0 引入了多线程来处理网络数据的读写(解包和封包),但处理命令的核心逻辑(命令排队、执行)依然是单线程的。
为什么单线程还能这么快?
我认为主要有四个原因:
-
纯内存操作:这是最根本的原因。Redis 的所有数据都存在内存中,内存的读写速度极快(纳秒级),相比磁盘 IO 拉开了几个数量级的差距
-
避免了多线程的并发开销:多线程虽然能利用多核 CPU,但会带来线程上下文切换的损耗,以及为了线程安全而加锁带来的性能开销。Redis 单线程模型天然保证了线程安全,不需要锁,也没有上下文切换的成本
-
高效的数据结构:Redis 针对不同场景设计了高效的数据结构,比如 SDS(简单动态字符串)、哈希表、跳表、压缩列表等,操作效率非常高
-
IO 多路复用模型:这是 Redis 高性能的关键技术支撑
IO 多路复用
传统的阻塞 IO 模型是一个线程处理一个连接。如果有 1000 个连接,就需要 1000 个线程。当连接空闲时,线程会被阻塞等待数据,造成巨大的资源浪费。
IO 多路复用简单来说,就是一个线程可以监控多个网络连接。
打个比方:
- 传统的阻塞 IO:服务员站在桌子旁等客人点菜,客人不说话,服务员就一直等着
- IO 多路复用:服务员在餐厅巡视,哪张桌子举手了(有网络事件就绪),服务员就过去处理哪张桌子
这样就用一个线程搞定了高并发。
在 Linux 下,Redis 默认使用的是 epoll 机制。它利用操作系统的底层能力,高效地批量监控大量的文件描述符(Socket),只处理那些真正有数据进来的连接,避免了无效等待。
总结一句话:Redis 单线程快,不是因为单线程本身快,而是因为它把单线程的优势发挥到了极致------纯内存操作 + 无锁 + 高效数据结构 + IO 多路复用。