缓存雪崩 缓存击穿 热key大key 持久化尽量避免丢失数据 数据库redis一致性
下面给出一份「面试速记表 + 实战代码 + 口诀」,把 Redis 最常见的 5 类“大坑”一次性整理清楚。
口诀:
雪崩一大片,击穿一个点;
热 key 扛不住,大 key 拖全网;
持久化丢数据,缓存库不同步。
| 坑名 | 触发场景 | 典型现象 | 防护 4 件套(按优先级) | 一句话记忆 |
|---|---|---|---|---|
| 1. 缓存雪崩 | 大量 key 同时过期 / Redis 宕机 | 所有请求直达 DB,DB 被打挂 | • 过期时间加随机值(散列) • Redis Cluster + 哨兵高可用 • 本地 Caffeine 二级缓存 • 降级限流(Sentinel) | “把过期时间打散,别让千军万马一起冲” |
| 2. 缓存击穿 | 单个热点 key 失效瞬间 | 该 key 的并发全部打到 DB | • 热点 key 永不过期 + 异步刷新 • 互斥锁(SET NX PX + Lua) • 逻辑过期(value 里存时间戳) • 提前预热 + 布隆过滤器 | “热点 key 永不过期,或加互斥锁” |
| 3. 热 key | 访问 QPS 极高,单节点网卡占满 | Redis 单分片 CPU 100% | • 本地缓存 + 一致性哈希分片 • 拆 key:将热 key 按 hash 后缀分成 N 份 • 读写分离(副本节点) | “把 1 把钥匙拆成 100 把” |
| 4. 大 key | value 过大(>10 MB) | 慢查询、阻塞、同步延迟 | • 业务拆分:按日期/ID 分段 • 使用 Hash / List 分片 • 定期扫描: redis-cli --bigkeys• 惰性删除:UNLINK 代替 DEL | “大 key 瘦身,拆、分、删” |
| 5. 持久化丢数据 | RDB 快照间隔内崩溃 | 最近写入全部蒸发 | • AOF 每秒刷盘 (appendfsync everysec)• RDB + AOF 混合模式 • 双写日志:Binlog → Canal → Redis • 异地多活 | “RDB 做备份,AOF 做实时,双保险” |
数据库 & 缓存一致性 3 种套路
Cache-Aside(旁路缓存)
读:先缓存 → 没有再查库 → 写回缓存
写:先写库 → 再删缓存(延迟双删防并发)java@CacheEvict(key = "#id", beforeInvocation = false) public void updateOrder(Long id, OrderDTO dto) { orderMapper.updateById(id, dto); // 延迟 500 ms 再次删,防并发脏读 executor.schedule(() -> redisTemplate.delete("order:"+id), 500, TimeUnit.MILLISECONDS); }分布式锁 + 双写
写请求先拿 Redisson 锁 → 写库 → 写缓存 → 解锁Canal / Debezium 异步订阅
MySQL binlog → Canal → Kafka → 更新 Redis,零侵入,最终一致
热 key 互斥锁示例(Lua 原子脚本)
lua
-- KEYS[1] 锁名 ARGV[1] 过期毫秒
if redis.call('SET', KEYS[1], 1, 'NX', 'PX', ARGV[1]) then
return 1 -- 拿到锁
else
return 0 -- 没拿到
endJava 封装
java
Boolean locked = redisTemplate.execute(
RedisScript.of(lua, Boolean.class),
Collections.singletonList("lock:sku:" + skuId),
1000);
if (Boolean.TRUE.equals(locked)) {
try {
// 查询数据库并回写缓存
} finally {
redisTemplate.delete("lock:sku:" + skuId);
}
} else {
// 自旋 or 返回默认值
}总结口诀再背一遍
雪崩—打散过期,击穿—锁/永不过期,热 key—拆分,大 key—瘦身,持久化—RDB+AOF,一致性—延迟双删 + 异步 binlog。