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%,靠监控和预案。

相关推荐
葫芦和十三3 小时前
图解 MongoDB 19|Oplog:复制的真正载体,不是文档是操作
后端·mongodb·agent
葫芦和十三3 小时前
图解 MongoDB 20|复制延迟与 catch up:Secondary 为什么跟不上
后端·mongodb·agent
IT_陈寒8 小时前
SpringBoot自动配置的坑,我的API突然就404了
前端·人工智能·后端
ServBay9 小时前
为什么说 MCP 是 2026 年开发者必须掌握的黄金协议?
后端·mcp
程序员夏洛9 小时前
Spring Boot 多模块项目中 IDEA 提示 Cannot resolve symbol 的一次排查记录
后端
子兮曰9 小时前
OpenMontage 深度解剖:你的 AI 编程助手,其实是个视频工作室
前端·后端·ai编程
子兮曰9 小时前
前端工具链的「Rust 化」:一场没有赢家的军备竞赛?
前端·后端·rust
爱勇宝10 小时前
从 Ctrl+CV 到 Enter:程序员正在失去什么
前端·后端·程序员
码事漫谈10 小时前
EdgeOne Makers + WorkBuddy:零基础也能快速搭建可上线的 AI 智能体(附图文教程)
后端