1.1 缓存穿透
场景:查询一个根本不存在的数据,缓存和数据库中都没有。
问题:每次请求都会绕过缓存,直接打到数据库;被脚本刷、爬虫遍历或恶意构造 ID 时,QPS 会成倍放大。
| 方案 | 原理 | 适合 |
|---|---|---|
| 缓存空值 | 把 null 也写入缓存,TTL 设短 | 不存在的数据量有限、会被重复查 |
| 布隆过滤器 | 位数组 + 多哈希,判断「可能存在 / 一定不存在」 | 数据量大、只增不删、能接受误判 |
| 布谷鸟过滤器 | 桶 + 指纹,支持删除 | 需要删除、能控制写入/删除路径 |
| Redis Set | 精确存在性判断 | 数据量小、要求精确 |
缓存空值
- TTL 要短(例如 30–120 秒),否则新建的数据会在一段时间内不可见;
- 新建数据成功后,主动删除对应的空值缓存,不要只等 TTL;
- 攻击者用大量随机 key 刷时,空值缓存会变成内存炸弹,要配合参数校验和限流。
布隆过滤器
位数组 + k 个哈希函数:插入时把 k 个位置置 1;查询时 k 个位置全为 1 才认为「可能存在」。
- 不会漏判:真正插入过的元素一定查得到;
- 会误判:没插入过的元素可能因为哈希碰撞被误判为存在;
- 误判率随容量和哈希数变化:
p ≈ (1 - e^(-kn/m))^k,最优k = ceil(-ln(p)/ln2); - 参考:100 万元素、0.1% 误判率,大约需要 14.4 bit/元素,约 1.8 MB。
# 误判率 0.1%,容量 100 万
BF.RESERVE user:filter 0.001 1000000
BF.MADD user:filter user:1001 user:1002
BF.EXISTS user:filter user:1001 # 1=可能存在,0=一定不存在
BF.INFO user:filter
要点:
- 不能删除:一个 bit 可能被多个元素共享,清 0 会影响别人;需要删除就用布谷鸟或 Set;
- 容量要预留:扩容会生成子过滤器,查询变慢、误判率上升;
- Redis 8 起模块已包含在发行包中,但默认
redis.conf不会自动加载,要用redis-full.conf或显式loadmodule;Redis 7.x 需要 Redis Stack / RedisBloom; - 集群里同一过滤器用 hash tag 固定 slot,例如
{user}:filter。
布谷鸟过滤器
每个元素存一个短指纹,并有两个候选桶;桶满了就「踢」一个已有指纹到它的另一个位置。查询只比指纹,所以也会误判,但支持删除。
CF.RESERVE coupon:filter 1000000 BUCKETSIZE 2 MAXITERATIONS 20 EXPANSION 1
CF.ADDNX coupon:filter coupon:123 # 不存在才加入;CF.ADD 允许重复
CF.EXISTS coupon:filter coupon:123
CF.COUNT coupon:filter coupon:123 # 可能存在几次,不是精确计数
CF.DEL coupon:filter coupon:123
CF.INFO coupon:filter
参数默认值:BUCKETSIZE 2、MAXITERATIONS 20、EXPANSION 1;capacity 会向上取整到 2 的幂。过滤器可能在没到 capacity 时就自报 full,然后创建子过滤器(最多 32 个)。
误判率:bucket size 1 时最小约 0.78%;bucket size 3 约 2.35%。桶越大填充率越高,误判率也越高。
⚠️
CF.DEL官方明确警告:不确定加过就不要删。指纹会碰撞,删一个没加过的元素可能误删别人的指纹,造成 false negative。
适合:优惠券核销、活动报名、已读/已领取去重——加和删都由同一个业务写路径控制。
选型建议
- 数据量小、要求精确:直接用 Redis Set。
- 只增不删、数据量大、能接受误判:布隆过滤器。
- 需要删除、且能控制写入/删除路径:布谷鸟过滤器。
- 不想引入概率结构:缓存空值 + 参数校验 + 限流,先解决「随机 key」问题。
- 过滤器丢了:从 DB 分页扫描重建,重建期间降级放行。
| 对比 | 布隆过滤器 | 布谷鸟过滤器 |
|---|---|---|
| 删除 | 不支持 | 支持(有误删坑) |
| 误判率下限 | 可调得很低 | 约 0.78% 起 |
| 内存 | 更低 | 略高 |
| 适合 | 只增不删 | 需要删除 |
面试追问
Q:为什么不用 Redis Set? A:数据量大时每个元素都要存完整字符串,内存比布隆/布谷鸟高一个量级;数据量小、要求精确时 Set 反而更简单。
Q:布隆过滤器放本地还是 Redis? A:本地快、少一次网络,但多实例之间不一致、重建困难;Redis 共享、易重建,但每次查询多一次网络往返。看数据规模和一致性要求。
Q:布隆过滤器为什么不能删除? A:bit 被多个元素共享,清 0 会影响其他元素;需要删除就用计数布隆、布谷鸟或 Set。
Q:布隆过滤器误判了怎么办? A:误判只会把「不存在」判成「可能存在」,继续查缓存/DB 即可,不能把「可能存在」当成业务事实。
Q:布谷鸟过滤器为什么能删除? A:它存的是指纹,删除时删对应指纹;但指纹碰撞会导致误删,所以官方要求「确定加过才删」。
1.2 缓存击穿
场景:热点 key 过期瞬间,大量并发请求同时涌入数据库。
| 方案 | 原理 | 特点 |
|---|---|---|
| 互斥锁 | 只让一个线程查库回写缓存,其余阻塞等待 | 强一致性,性能较差 |
| 逻辑过期 | 不立即删除,先返回旧数据,异步更新缓存 | 高可用,性能好 |
逻辑过期的实现要点:
- value 里存
{data, expireAt},Redis 的物理 TTL 设长或永不过期; - 读到逻辑过期:先返回旧值,再异步抢锁刷新;
- 刷新失败要告警并继续用旧值兜底,不能返回空;
- 价格、库存这类不能返回旧值的业务不要用逻辑过期。
互斥锁的 TTL 怎么定? 覆盖「查库 + 回填」的耗时并留余量;等锁要有超时,超时后降级,不能无限阻塞。
1.3 缓存雪崩
场景:同一时段大量 key 同时过期,或 Redis 宕机,请求全部打到数据库。
解决方案:
- TTL 加随机值 — 避免大量 key 同时过期
- Redis 集群部署 — 提高可用性和分区容错性
- 限流降级策略 — 业务层添加多层防护
- 多级缓存 — 引入本地缓存(如 Caffeine)作为前置缓冲
雪崩其实分两种:
| 类型 | 原因 | 主要应对 |
|---|---|---|
| 大量 key 同时过期 | 批量导入 TTL 相同、整点刷新 | TTL 加随机抖动、分批预热 |
| Redis 整体不可用 | 节点宕机、网络分区 | 高可用、多级缓存、熔断降级 |
TTL 抖动一般是在基础 TTL 上加 10%–30% 的随机值;大促或发版后要提前预热,不要等用户请求来触发。
1.4 双写一致性
核心问题:修改数据库的同时,如何保证缓存与数据库的数据一致?
方案一:先删缓存,再操作数据库
正常流程:删除缓存 → 更新数据库 → 查询数据库 → 更新缓存
异常情况:删除缓存 → 查询数据库(旧值)→ 写入缓存(旧值)→ 更新数据库 → 缓存中仍是旧数据!
方案二:先操作数据库,再删除缓存
正常流程:更新数据库 → 删除缓存 → 查询缓存(未命中)→ 查库 → 写入缓存
异常情况:缓存未命中 → 查库(旧值)→ 数据库被更新 → 删除缓存 → 旧值写入缓存
为什么是删缓存,不是更新缓存?
- 更新缓存要处理并发写覆盖、计算逻辑重复、缓存和 DB 双写不一致;
- 删除更简单,下一次读自然回填;
- 工程默认是 Cache-Aside:先更新数据库,事务提交后删缓存,再用 TTL 兜底。
解决方案总结
| 方案 | 原理 | 适用场景 |
|---|---|---|
| 延时双删 | 删缓存 → 更新库 → 等一会 → 再删缓存 | 兜底策略,轻量级 |
| 分布式锁(读写锁) | 写时加排他锁,读时加共享锁 | 强一致性要求 |
| 异步通知(MQ / Canal) | 更新后异步通知缓存更新/删除 | 允许短暂延迟 |
- 共享锁:加锁后其他线程可共享读操作
- 排他锁:加锁后阻塞其他线程的读写操作
总结:只有读时用共享锁,有写操作时用排他锁。保证了强一致性,但性能较低。
延迟双删的局限:它只是降低旧值回填的概率,不是强一致;延迟时间无法精确覆盖所有读请求,第二次删除失败仍会留下旧值。
更常用的现状做法:
| 做法 | 原理 | 代价 |
|---|---|---|
| Cache-Aside + 事务提交后删缓存 + TTL | 最常用;删除失败由 TTL 兜底 | 允许短暂旧值 |
| binlog / CDC 异步失效 | Canal、Debezium 订阅数据库变更后删缓存 | 有延迟,需维护 CDC 链路 |
| 事务性 outbox + MQ | 本地事务和消息一起提交,消费端删缓存 | 实现复杂,要保证幂等 |
强一致要求(金额、库存)通常不要靠缓存保证:读走数据库,或者用分布式锁/读写锁;缓存只做加速。
1.5 持久化(RDB / AOF)
RDB(Redis Database Backup)
利用页表映射关系 + Copy-on-Write 技术:
- 主进程读操作 → 直接访问共享内存
- 主进程写操作 → 拷贝一份数据,在新副本上执行
AOF(Append Only File)
核心思路:把每一条写命令追加记录到日志文件中,重启时重新执行这些命令来恢复数据。
AOF 重写机制:用最少的命令达到相同的数据恢复效果,大幅压缩文件体积。
appendfsync 三档:
| 值 | 行为 | 可靠性 | 性能 |
|---|---|---|---|
always | 每条命令都 fsync | 最高 | 最差 |
everysec | 每秒 fsync 一次 | 通常最多丢约 1 秒 | 默认折中 |
no | 交给操作系统 | 可能丢更多 | 最好 |
Redis 7+ 用 Multi-Part AOF:不再是一个大文件,而是 manifest + base AOF + incr AOF;重写时先生成新的 base 再切换 manifest,避免旧版重写的阻塞窗口。
RDB vs AOF 对比
| 维度 | RDB | AOF |
|---|---|---|
| 文件大小 | 小 | 大 |
| 恢复速度 | 快 | 慢 |
| 数据安全性 | 可能丢失最后一次快照后的数据 | 更高 |
| 默认 | 默认配置通常有 RDB save 规则 | 默认关闭(以实际 conf 为准) |
1.6 删除策略与淘汰策略
删除策略(Key 过期后如何删除)
惰性删除:访问 key 时检查,过期则删除。对 CPU 友好,但对内存不友好。
定期删除:每隔一段时间随机抽取 key 检查,过期则删除。有 Slow / Fast 两种模式。
Redis 实际使用 惰性删除 + 定期删除 的组合策略。
数据淘汰策略(内存满了怎么办)
| 策略 | 范围 | 说明 |
|---|---|---|
noeviction | — | 默认。不淘汰,内存满后不允许写入 |
volatile-ttl | 有 TTL 的 key | TTL 越小越先淘汰 |
allkeys-random | 全体 key | 随机淘汰 |
volatile-random | 有 TTL 的 key | 随机淘汰 |
allkeys-lru | 全体 key | LRU 算法淘汰 |
volatile-lru | 有 TTL 的 key | LRU 算法淘汰 |
allkeys-lfu | 全体 key | LFU 算法淘汰 |
volatile-lfu | 有 TTL 的 key | LFU 算法淘汰 |
volatile-lrm | 有 TTL 的 key | Redis 8.6+,最久未修改 |
allkeys-lrm | 全体 key | Redis 8.6+,最久未修改 |
LRU vs LFU:
| 算法 | 全称 | 淘汰依据 |
|---|---|---|
| LRU | Least Recently Used | 当前时间 - 最后访问时间,差值越大越优先 |
| LFU | Least Frequently Used | 访问频率越小越优先 |
1.7 Redis 分布式锁
场景:服务集群部署下,普通的 JVM 锁无法跨节点互斥。
SETNX 原理
核心命令:SETNX(SET if Not eXists)。利用 Redis 单线程特性,只有一个客户端能成功设置 key。
⚠️ 不设置失效时间可能导致死锁(线程宕机后锁无法释放)。
Redisson 实现
public void redisLock() throws InterruptedException {
// 1. 获取锁对象
RLock lock = redissonClient.getLock("heimalock");
// 2. 尝试获取锁
// tryLock(waitTime, timeUnit)
// - waitTime(10s): 最大等待时间,超时未获取则放弃并返回 false
// - 省略 leaseTime 时,Redisson 默认启用「看门狗」机制:
// 锁默认有效期 30s,看门狗会自动续期
boolean isLock = lock.tryLock(10, TimeUnit.SECONDS);
// 3. 执行业务
if (isLock) {
try {
System.out.println("执行业务");
} finally {
// 4. 释放锁(必须放在 finally 中!)
lock.unlock();
}
} else {
System.out.println("获取锁失败,请稍后重试");
}
}
Redisson 封装的关键能力:看门狗机制、Lua 脚本保证原子性、UUID 验证锁持有者、可重入、集群兼容。
看门狗只解决「业务没跑完锁先过期」,不解决主从切换丢锁:Redis 复制是异步的,主节点还没把锁写入同步给从节点就挂了,从节点升主后不知道这把锁存在。
如果锁失效的后果很严重(资金、库存),不要只依赖「锁还没过期」:加 fencing token,每次拿锁得到一个单调递增的编号,写数据库时带上编号,存储层拒绝旧编号的写入。Redlock 和五种锁方案的完整讨论见 《分布式锁选型》。
1.8 Redis 集群
主从复制
定位:数据备份 + 读写分离的基础方案。
- 全量同步:首次连接,Master 生成 RDB 发送给 Slave
- 增量同步:基于 repl_backlog,断线重连后只同步差异命令
哨兵机制
定位:主从复制的进阶补丁,解决高可用问题。哨兵持续监控节点状态。
脑裂问题:网络分区导致多个 Master 同时存在,数据冲突。
分片集群
定位:解决海量存储 + 高并发写问题。分布式分片 + 自动扩展。
核心机制 — 哈希槽(Hash Slot):16384 个槽位分配给不同主节点,根据 key 计算槽 → 路由到对应节点。去中心化的多主多从架构。
两个路由细节:
- MOVED:槽已经永久迁到新节点,客户端应更新本地路由;
- ASK:槽正在迁移,这次请求去目标节点查,但不更新路由。
跨 slot 的多 key 操作、Lua、事务会受限;需要把相关 key 放同一 slot 时用 hash tag,例如 {user:1001}:name 只对花括号里的内容计算槽。
1.9 单线程为什么快 / I/O 模型
Redis 为什么快
- 纯内存操作,执行速度非常快(根本原因)
- 命令执行单线程,避免不必要的上下文切换和竞争条件,单条命令天然原子
- I/O 多路复用模型,非阻塞 IO
I/O 多路复用及网络模型
Redis 基于 Reactor 模式,使用 I/O 多路复用监听多个 Socket,主线程串行执行命令,避免了多线程的锁竞争问题。
准确说法是「命令执行单线程」,不是「整个 Redis 单线程」:Redis 6+ 的 I/O threads 可以并行读写 socket/解析协议,后台线程还会处理 AOF fsync、lazy free 等。
pipeline 为什么快? 把多条命令合并成一次网络往返,减少 RTT 和 syscall;pipeline 不是事务,也不保证原子性。
1.10 Redis 8.x 现状(速记)
| 版本 | 关键变化 |
|---|---|
| 8.0(2025-05) | Bloom/Cuckoo/JSON/Search/TimeSeries 并入发行包;AGPLv3 许可选项;Vector Set beta |
| 8.2(2025-08) | kvobj 统一存储、短字符串内存优化、I/O 吞吐提升 |
| 8.4(2025-11) | FT.HYBRID 混合检索;Atomic Slot Migration |
| 8.6(2026-02) | HOTKEYS 热 key 检测;volatile-lrm / allkeys-lrm;Streams XADD 幂等 |
| 8.8(2026-05) | Array 数据类型;INCREX 窗口限流 |
| 8.10(2026-07) | compact hash / HIMPORT;BACKUP |
注意:Redis 8 的发行包已经包含 Bloom/Cuckoo 等模块,但默认
redis.conf不会自动加载,需要用redis-full.conf或显式loadmodule;Redis 7.x 仍然依赖 Redis Stack / RedisBloom。8.12 目前是 RC(2026-10-29),生产基线仍是 8.10。
1.11 大 key / 热 key(速记)
| 问题 | 危害 | 发现 | 治理 |
|---|---|---|---|
| 大 key | 阻塞主线程、网络拥塞、迁移慢 | redis-cli --bigkeys、MEMORY USAGE key、SLOWLOG | 拆分 key、UNLINK、hash field 过期、避免 KEYS * |
| 热 key | 单节点/单 slot 过载 | redis-cli --hotkeys(需 LFU)、HOTKEYS(8.6+)、CLUSTER SLOT-STATS(8.2+) | 本地缓存、key 拆分、读写分离、迁移热 slot、限流 |
MEMORY USAGE user:1001
SLOWLOG GET 10
HOTKEYS START METRICS 2 CPU NET COUNT 10 DURATION 60000 SAMPLE 100
HOTKEYS GET
监控重点:命中率、evicted_keys、used_memory / used_memory_rss、碎片率、慢查询、复制延迟。