Redis 缓存设计避坑指南:穿透、击穿、雪崩与一致性问题

Redis 缓存设计避坑指南:穿透、击穿、雪崩与一致性问题


高并发系统中,Redis 是挡在数据库前的那道盾。但这道盾,稍有不慎就会被自己人捅穿。

本文把 Redis 缓存最致命的四大问题------穿透、击穿、雪崩、一致性------一次性讲透,给出可直接落地的方案。


一、缓存穿透:查一个不存在的东西,却把数据库查崩了

问题本质

用户请求一个数据库中根本不存在的 Key(如 userId = -1),缓存没命中,直接打到数据库。攻击者用大量不重复的随机 ID 发起请求,每一条都穿透到数据库,库被压垮

三道防线

防线 原理 适用场景 局限
接口层参数校验 请求入口拦截非法参数(ID 必须为正整数等) 所有场景 只能挡格式错误,挡不住"合法但不存在"的 ID
缓存空值 DB 查不到也缓存一个 null,TTL 设 5 分钟 重复查询少的场景 大量不重复攻击会塞满缓存,挤掉有效数据
布隆过滤器 预加载所有合法 Key 到位图,O(1) 判断是否存在 高并发、大数据量 有误判率(约 0.1%~1%),不支持删除

最佳实践:布隆过滤器 + 空值缓存组合使用。 布隆过滤器挡掉绝大多数非法请求,空值缓存兜底漏网之鱼。

kotlin 复制代码
java
// 布隆过滤器预加载所有合法ID
BloomFilter<Long> bloomFilter = BloomFilter.create(
    Funnels.longFunnel(), 10000000L, 0.001
);
allIds.forEach(bloomFilter::put);

// 查询时先过滤
if (!bloomFilter.mightContain(userId)) {
    return null;  // 一定不存在,直接拦截
}

二、缓存击穿:一个热点 Key 过期的瞬间,万箭齐发

问题本质

某个热点 Key(如首页商品)刚好过期,此时大量并发请求同时发现缓存失效,全部涌向数据库,造成瞬时压力激增。

与穿透的区别:穿透查的是"不存在的数据",击穿查的是"存在但缓存刚好过期的数据"。

三种解法

方案 原理 优缺点
互斥锁(分布式锁) 缓存未命中时,只有拿到锁的线程去查 DB,其他线程等待 简单有效,但高并发下大量线程阻塞,吞吐量下降
热点数据永不过期 物理不设 TTL,逻辑过期时间存在 value 里,后台异步刷新 性能最优,但返回的可能是短暂旧数据
定时主动刷新 定时任务在过期前主动刷新缓存 实现复杂,适合 Key 固定的场景

推荐方案:热点数据永不过期(逻辑过期)

csharp 复制代码
java
public class CacheItem {
    private Object value;
    private long expireTime;  // 逻辑过期时间戳

    public boolean isExpired() {
        return System.currentTimeMillis() > expireTime;
    }
}

// 读取时判断
CacheItem item = cache.get(key);
if (item.isExpired()) {
    // 返回旧数据,同时异步刷新
    CompletableFuture.runAsync(() -> refreshCache(key));
}
return item.getValue();

这种方式从根源上消灭了"过期瞬间"这个窗口期,是击穿问题的终极解法。


三、缓存雪崩:所有 Key 集体罢工,数据库裸奔

问题本质

大量缓存同时过期(如整点失效),或 Redis 集群宕机,所有请求瞬间穿透到数据库,数据库直接崩溃

三板斧

1. TTL 加随机偏移(最常用、最有效)
ini 复制代码
java
// 真实 TTL = 基础 TTL + 随机值(±5分钟)
long ttl = 3600 + (long)(Math.random() * 600 - 300);
redis.setex(key, ttl, value);

让失效时间均匀分布,避免集体失效。

2. 多级缓存
复制代码
Caffeine 本地缓存(10分钟)→ Redis 分布式缓存(1小时)→ 数据库

本地缓存挡掉绝大部分请求,Redis 挂了还有一层缓冲。

3. 熔断降级

数据库压力过大时,返回默认值或兜底数据,而非硬扛。

typescript 复制代码
java
@HystrixCommand(fallbackMethod = "getDefaultData")
public Object getData(String key) {
    return db.query(key);
}

四、缓存一致性:更新了数据库,缓存还是旧的

这是四个问题中最隐蔽、最难搞的一个。

核心矛盾

数据库和缓存是两套独立存储,更新操作无法原子化,必然存在不一致窗口

五种方案对比

方案 一致性级别 复杂度 性能影响 适用场景
先更新 DB,再删缓存(Cache-Aside) 弱一致 90% 场景够用
延时双删 弱一致 读多写少,可容忍短暂不一致
Write Through 代理层 强一致 核心业务,需简化业务逻辑
Binlog 异步删除(Canal) 最终一致 高并发读,弱一致可接受
分布式锁 + 版本号 强一致 金融交易,零容忍脏数据

最推荐的组合

常规场景:Cache-Aside + TTL 兜底

typescript 复制代码
java
// 更新流程
public void updateData(String key, Object value) {
    db.update(key, value);        // 1. 先更新数据库
    redis.del(key);               // 2. 再删缓存
}

// 读流程
public Object getData(String key) {
    Object data = redis.get(key);
    if (data != null) return data;
    data = db.query(key);
    redis.setex(key, data, 3600);  // 写入缓存,带 TTL 兜底
    return data;
}

高一致场景:Cache-Aside + 延时双删 + Binlog 异步删除

scss 复制代码
java
public void updateWithDoubleDelete(String key, Object value) {
    redis.del(key);                    // 第一次删
    db.update(key, value);             // 更新数据库
    asyncExecutor.submit(() -> {
        Thread.sleep(500);             // 延迟 = 查DB+写缓存耗时的1.5-2倍
        redis.del(key);                // 第二次删,清除窗口期内的脏数据
    });
}

五、一张表终结所有决策

问题 核心原因 首选方案 备选方案
穿透 查不存在的数据 布隆过滤器 + 空值缓存 参数校验
击穿 热点 Key 过期瞬间 热点永不过期(逻辑过期) 互斥锁
雪崩 大量 Key 同时失效 TTL 随机偏移 + 多级缓存 熔断降级
一致性 DB 与缓存更新不同步 Cache-Aside + TTL 延时双删 / Binlog

六、最后说句实话

Redis 缓存没有银弹。穿透靠过滤,击穿靠永不过期,雪崩靠随机 TTL,一致性靠删缓存 + 兜底。

把这四个问题的方案组合起来,你的缓存层就能扛住 99% 的生产故障。 剩下那 1%,靠监控和预案。

相关推荐
掘金者阿豪1 小时前
运营不会SQL怎么办?我把数据库变成了大家都会用的表格
后端
wheninger2 小时前
DDD 聚合 × Agent 命令:那道拒绝 AI 的墙
后端
狂炫冰美式2 小时前
AI 生成 Draw.io,导入飞书/Lark 画板后可编辑
前端·人工智能·后端
浩风祭月2 小时前
一个开发者的“看门狗”:我把服务器监控从被动告警变成了主动预防
后端·docker
Moment2 小时前
我做了一套前端也能学懂的 AI Agent 系列,从 Prompt 一路讲到多 Agent 😍😍😍
前端·后端·面试
神奇小汤圆2 小时前
两种方式,彻底解决 Codex 令人恼火的问题
后端
用户34232323763173 小时前
工业数据采集安全——当 OT 遇见 IT,谁对谁错?
后端
楼田莉子3 小时前
C++20新特性:协程
开发语言·c++·后端·学习·c++20
元宝骑士3 小时前
SpringBoot + Sa-Token 实现 CSRF 令牌校验(进阶篇)
后端·安全