[TOC] #### 1. 前言 --- 在现代分布式系统架构中,Redis 凭借其卓越的性能,已然成为缓存领域的事实标准,为系统扛住了海量并发流量。 然而,在高并发、高可用的严苛要求下,缓存层潜藏着三大经典问题:缓存穿透、缓存击穿、缓存雪崩(缓存防护问题) 这三个概念看似相近,实则成因迥异,但它们的破坏力都足以让最坚固的数据库防线瞬间崩溃,也是面试中的高频考点 #### 2. 缓存穿透 --- 缓存穿透:请求的数据既不在缓存(Redis)中,也不在数据库(MySQL)中,导致请求每次都打到数据库 缓存穿透产生原因与典型场景: + 恶意攻击:黑客或爬虫故意构造大量不存在的 Key 发起请求,将压力全部转移到后端数据库,意图造成数据库宕机 + 业务代码漏洞:数据库查询返回空结果时,没有将这个空结果缓存,同一无效数据的请求下次查询还会去查询数据库 解决缓存穿透的核心思想是:拦截无效请求,避免其直接冲击数据库。以下是几种主流的解决方案: 参数校验:这是最基础的也是最有效的一道防线。在请求到达缓存查询逻辑之前,先对请求参数进行严格的合法性校验。 + 实现思路:检查ID是否为正数,参数格式是否正确。对于明显的非法请求,直接在应用层拦截,不会进入后续查询流程 + 优点:实现简单,能从源头杜绝大部分低级错误和恶意请求 缓存空对象:这是应对缓存穿透最常用、最直接的方法 + 实现思路:当从数据库中查询不到数据时,将这个 Key 和一个特殊的空值一起写入缓存,设置较短的过期时间 + 优点:实现简单,效果立竿见影 + 缺点:内存浪费、数据不一致 + 如果存在海量不同的无效 Key,会占用大量缓存空间 + 在空值缓存的有效期内,如果该数据在数据库中被创建,客户端在缓存过期前依然会获取到空值 布隆过滤器:它是一种空间效率极高的概率型数据结构,常用于判断一个元素是否可能存在于一个集合中 总结与建议: 缓存穿透是高并发系统中必须防范的风险,在实际项目中,通常采用组合策略来构建稳固的防御体系: + 第一道防线(基础防御):所有外部请求进行严格的参数校验 + 缓存空对象(核心防御):对于查询结果为空的数据,采用缓存空对象的方式,这是最简单有效的通用方案 + 布隆过滤器(高级防御):在数据量巨大且对性能要求极高的场景下,引入布隆过滤器作为前置拦截层 #### 3. 缓存击穿 --- 缓存击穿:某个热点 Key 在缓存失效的瞬间,大量请求同时打到数据库,导致数据库压力剧增,甚至宕机 缓存击穿的三个必要条件:热点数据(高频访问)、缓存过期,高并发请求同时到来 举一个实际的例子: + 比如一个电商系统,商品ID:`product:1001` 是爆款商品,每秒几千请求,Redis 设置了过期时间 10 分钟 + 当第 10 分钟刚过,几千个请求同时发现缓存失效,一起请求数据库,数据库瞬间扛不住,这就是缓存击穿 缓存击穿的解决方案: + 互斥锁:当缓存失效时,不是立即去查数据库,而是先尝试获取一个分布式锁 + 逻辑过期 方案一:互斥锁(强一致性方法) + 实现原理:当缓存失效时,不是立即去查数据库,而是先尝试获取一个分布式锁 + 获取锁成功的线程:去查数据库,并将数据写入缓存,然后释放锁 + 获取锁失败的线程:说明有其它线程正在重建缓存,当前线程可以休眠一小会儿后重试,直接从缓存中读取数据 代码逻辑示意: + 用 `SET key value NX EX time` 命令来实现分布式锁 + 只有拿到锁的线程才能访问 DB,其他线程通过 while 循环重试,直到缓存重建完成 优缺点分析: + 保证了数据的强一致性,数据库同一时刻只会被同一个请求查询 + 缺点:如果缓存重建时间较长,其它线程会阻塞等待,导致整体吞吐量下降 方案二:逻辑过期(高可用方案) 互斥锁的解决方案是 “为了拿到最新数据,宁可让你等一下”,而逻辑过期是 “先给你旧数据应急,我后台偷偷去更新” 技术实现详解: 第一步:改造数据结构 + 存入缓存的数据包含两个部分:data(真实的业务数据)、expireTime(逻辑上的过期时间戳) + 并且不设置缓存的过期时间(TTL) 第二步:查询与判断 + 请求来了,取出反序列化后的缓存,解析处 data 和 expireTime + 判断逻辑过期(比较 expireTime 和当前时间),如果未过期直接返回 data,如果已过期,进入第三步 第三步:异步重建缓存(这是最关键的一步) + 尝试获取互斥锁:使用 SETNX 尝试获取一个针对该 Key 的锁 + 如果不加锁,当缓存逻辑过期时,为了保证同一时间只有一个线程去执行更新任务 + 分支处理: + 获取锁成功:说明我是第一个发现它过期的,提交任务给独立的线程池去更新缓存,主线程立刻返回旧的 data + 获取锁失败:说明已经有别的线程在负责更新了,我啥也不用干,直接返回旧的 data #### 4. 缓存雪崩 --- 缓存雪崩:大量缓存 Key 在同一时间失效,导致所有请求直接穿透到数据库,造成数据库瞬时压力过大甚至宕机的现象 解决雪崩的核心思路:分散风险,留好退路 随机过期时间(最基础、最有效),这是解决 “集中过期” 最简单的方法: + 原理:在原有的过期时间基础上,增加一个随机值,让缓存失效的时间点分散开来 + 做法:基础时间(30分钟),随机范围(0~10分钟),最终时间(30 分钟 + 随机分钟) + 效果:这样即使你批量导入 100 万个 Key,它们的失效时间也会均匀分布在 30~40 分钟这个区间内 高可用架构(防患于未然),这是解决 “Redis 宕机” 的根本方法: + 原理:不要让 Redis 单点运行 + 做法:主从复制 + 哨兵模式,主节点挂了,哨兵自动选举新主节点 构建多级缓存: + 采用 “本地缓存 + Redis 分布式缓存” 的多级缓存架构 + 当 Redis 中的 Key 失效时,请求可以先从响应更快的本地缓存中获取,减少对数据库的直接冲击 限流与降级(保命手段):当雪崩真的发生时,为了保住数据库不死,必须牺牲部分用户体验 + 限流:限制访问数据库的 QPS。比如数据库只能抗 1000 QPS,那就只放行 1000 个请求,多余的直接拒绝 + 降级:检测到系统异常时,直接返回默认值(如“系统繁忙”)或静态页面,不再查询数据库 #### 5. 三大缓存问题对比 --- 为了方便记忆,将 “穿透、击穿、雪崩” 放在一起对比: | 问题类型 | 核心原因 | 解决方案关键词 | | ------------ | ------------ | ------------ | | 缓存穿透 | 查询不存在的数据 | 缓存空值、布隆空滤器 | | 缓存击穿 | 热点 Key 刚好过期 | 互斥锁、逻辑过期 | | 缓存雪崩 | 大量 Key 同时过期 | 随机过期时间、多级缓存、高可用集群、限流降级 |