在缓存系统(如Redis、Memcached)的使用中,缓存击穿和缓存雪崩是两种常见的性能问题,均会导致大量请求直接穿透到数据库,引发数据库压力骤增甚至宕机。但二者的场景和原因不同,解决方案也各有侧重。
一、缓存击穿(Cache Breakdown)
定义
缓存击穿是指:某个“热点Key”(被高频访问的Key)在缓存中失效(过期或不存在)的瞬间,大量并发请求直接穿透到数据库,导致数据库瞬间压力过大。
例如:秒杀活动中的商品ID(热点Key)缓存过期,此时数万用户同时抢购,请求全部打到数据库查库存,可能直接压垮数据库。
产生原因
- 热点Key的缓存过期(如设置了较短的TTL,且过期瞬间有大量并发请求)。
- 热点Key在缓存中根本不存在(如首次访问,缓存未预热),且被高频访问。
解决方案
核心思路:在热点Key失效瞬间,控制对数据库的并发请求,避免“一窝蜂”访问。
-
互斥锁(分布式锁)
- 当缓存失效时,先尝试获取分布式锁(如Redis的
SETNX),只有获取到锁的请求才能访问数据库,其他请求等待或重试。 - 数据库查询完成后,更新缓存,再释放锁。后续请求即可从缓存获取数据。
- 示例(伪代码):
public string GetHotData(string key) { // 1. 先查缓存 string data = redis.Get(key); if (data != null) return data; // 2. 缓存失效,尝试获取分布式锁 bool locked = redis.SetNX($"lock:{key}", "1", TimeSpan.FromSeconds(5)); // 锁超时避免死锁 if (locked) { try { // 3. 拿到锁,查数据库 data = db.QueryData(key); // 4. 更新缓存(设置合理的过期时间) redis.Set(key, data, TimeSpan.FromMinutes(10)); return data; } finally { // 5. 释放锁 redis.Delete($"lock:{key}"); } } else { // 6. 未拿到锁,休眠后重试(避免频繁请求) Thread.Sleep(100); return GetHotData(key); // 递归重试 } }
- 当缓存失效时,先尝试获取分布式锁(如Redis的
-
热点Key永不过期
- 对热点Key不设置过期时间(物理不过期),但通过业务代码手动更新缓存(如定时任务)。
- 适用于热点数据更新频率低的场景(如静态配置、热门商品基本信息)。
-
提前预热 + 过期时间错开
- 系统启动时,主动将热点Key加载到缓存(预热),避免首次访问穿透。
- 对同一批热点Key设置随机的过期时间(如基础过期时间±10%),避免同时失效。
二、缓存雪崩(Cache Avalanche)
定义
缓存雪崩是指:大量缓存Key在同一时间失效,或缓存服务器(如Redis集群)整体宕机,导致所有请求全部穿透到数据库,数据库因瞬间压力过大而崩溃。
例如:电商平台在凌晨0点对所有商品缓存设置了24小时过期,次日0点所有商品缓存同时失效, millions级请求直接打到数据库,导致数据库宕机。
产生原因
- 大量Key集中过期:缓存设置了相同的过期时间(如批量更新时统一设置TTL),到期后同时失效。
- 缓存服务不可用:缓存集群宕机(如Redis主从切换失败、网络故障),导致所有缓存查询失败。
解决方案
核心思路:避免大量Key同时失效,提高缓存服务可用性,降低数据库压力。
-
过期时间随机化
- 对缓存Key的过期时间添加随机值(如
基础TTL + 随机数(0~300秒)),避免集中过期。 - 示例:
// 原过期时间:1小时,改为1小时±5分钟 int baseExpire = 3600; int random = new Random().Next(0, 300); int actualExpire = baseExpire + random; // 3600~3900秒 redis.Set(key, data, TimeSpan.FromSeconds(actualExpire));
- 对缓存Key的过期时间添加随机值(如
-
缓存集群高可用
- 部署缓存集群(如Redis主从+哨兵、Redis Cluster),避免单点故障。即使部分节点宕机,其他节点仍可提供服务。
- 配置自动故障转移(如哨兵模式自动切换主节点),减少服务中断时间。
-
多级缓存
- 引入本地缓存(如C#中的
MemoryCache)+ 分布式缓存(如Redis)的多级架构。 - 当分布式缓存失效时,先查本地缓存,减少对数据库的直接冲击。
- 引入本地缓存(如C#中的
-
熔断降级
- 使用熔断工具(如Sentinel、Hystrix)监控数据库压力,当请求量超过阈值时,自动“熔断”对数据库的访问,返回默认值(如“系统繁忙,请稍后再试”)。
- 避免数据库被压垮,保障核心服务可用。
-
缓存预热与降级开关
- 提前将热点数据加载到缓存(预热),减少缓存失效的概率。
- 配置降级开关:当缓存不可用时,临时关闭非核心功能(如商品详情页的推荐模块),减少请求量。
二、缓存穿透(Cache Penetration)
在缓存系统中,缓存穿透(Cache Penetration) 是指:查询一个“根本不存在的数据”(缓存和数据库中都没有该数据),导致每次请求都无法命中缓存,直接穿透到数据库。由于缓存无法拦截这类请求,若存在大量此类查询(如恶意攻击),会导致数据库持续接收无效请求,压力骤增甚至宕机。
1、缓存穿透的典型场景
- 恶意攻击:黑客伪造大量不存在的Key(如随机生成的用户ID、订单号)发起高频查询,由于缓存和数据库均无对应数据,所有请求直接打向数据库。
- 业务误操作:例如,某商品数据被误删除,而前端仍在持续请求该商品详情,导致每次查询都穿透到数据库。
- 新数据未同步:新生成的数据尚未写入缓存和数据库(如刚创建的订单还未落地),此时查询会穿透。
2、产生原因
- 缓存和数据库中均无该数据:请求的Key是“无效的”(如不存在的ID、非法参数),缓存无法命中,数据库查询结果也为空。
- 高频访问无效Key:此类无效请求被大量、持续发起,缓存无法“屏蔽”(因为缓存中没有对应Entry),导致数据库持续处理无效查询。
3、解决方案
核心思路:拦截无效请求,避免其穿透到数据库,或通过缓存“空结果”减少数据库访问。
(1). 缓存空值(Cache Null)
当数据库查询结果为空时,将“空值”(或特殊标记,如null、"")存入缓存,并设置较短的过期时间(如1~5分钟)。后续请求会命中缓存的空值,无需再访问数据库。
示例(伪代码):
public string GetData(string key) {
// 1. 先查缓存
string data = redis.Get(key);
if (data != null) {
// 缓存命中:若为特殊空标记,返回空;否则返回数据
return data == "NULL_MARKER" ? null : data;
}
// 2. 缓存未命中,查数据库
data = db.QueryData(key); // 数据库查询结果为null(数据不存在)
if (data == null) {
// 3. 缓存空值(设置短过期,如1分钟,避免长期占用缓存)
redis.Set(key, "NULL_MARKER", TimeSpan.FromMinutes(1));
return null;
}
// 4. 数据库有数据,正常缓存并返回
redis.Set(key, data, TimeSpan.FromHours(1));
return data;
}
优缺点:
- 优点:实现简单,能快速拦截重复的无效请求。
- 缺点:
- 缓存空值会占用缓存空间(若无效Key极多,可能导致缓存膨胀);
- 过期时间需合理设置(太短则无法有效拦截,太长则可能影响新数据写入后的查询)。
(2). 布隆过滤器(Bloom Filter)
布隆过滤器是一种空间效率极高的概率性数据结构,用于快速判断“一个元素是否在集合中”。它的核心思想是:预先将所有“可能存在的有效Key”存入布隆过滤器,查询时先通过布隆过滤器判断Key是否可能存在——若不存在,直接返回;若可能存在,再走“缓存→数据库”流程。
工作流程:
- 预热阶段:将数据库中所有有效Key(如用户ID、商品ID)存入布隆过滤器。
- 查询阶段:
- 接收请求Key,先通过布隆过滤器判断:若布隆过滤器判定“不存在”,直接返回空(无需访问缓存和数据库);
- 若布隆过滤器判定“可能存在”,再查询缓存→数据库(正常流程)。
示例(伪代码):
// 初始化布隆过滤器(预先加载所有有效Key)
private BloomFilter<string> _bloomFilter = new BloomFilter<string>(expectedElements: 1000000, falsePositiveRate: 0.01);
public string GetData(string key) {
// 1. 先过布隆过滤器:若不存在,直接返回
if (!_bloomFilter.Contains(key)) {
return null;
}
// 2. 布隆过滤器判定可能存在,再查缓存
string data = redis.Get(key);
if (data != null) {
return data;
}
// 3. 缓存未命中,查数据库
data = db.QueryData(key);
if (data != null) {
redis.Set(key, data, TimeSpan.FromHours(1));
}
return data;
}
优缺点:
- 优点:
- 空间效率极高(存储100万Key仅需几MB内存);
- 查询速度快(O(1)时间复杂度),能有效拦截绝大多数无效请求。
- 缺点:
- 存在误判率(可能将不存在的Key判定为“可能存在”,但不会漏判),误判率可通过参数调整(误判率越低,占用空间越大);
- 不支持删除操作(删除Key会影响其他元素的判断),适合Key相对稳定的场景(如用户ID、商品SKU)。
(3). 接口层限流与参数校验
- 限流:对接口设置访问频率限制(如用令牌桶算法),防止恶意请求洪水(如每秒最多1000次请求),即使存在穿透,也能控制数据库压力。
- 参数校验:在接口层对请求参数进行合法性校验,过滤明显无效的Key(如用户ID为负数、订单号格式错误),从源头拦截无效请求。
示例:
// 接口限流(使用ASP.NET Core的RateLimiter)
[EnableRateLimiting("fixed")] // 应用限流策略(如每分钟最多100次)
public IActionResult GetProduct(string productId) {
// 参数校验:过滤无效ID
if (!IsValidProductId(productId)) { // 自定义校验逻辑(如格式、范围)
return BadRequest("无效的商品ID");
}
// 正常业务逻辑...
}
(4). 数据预热与兜底策略
- 数据预热:系统启动时,将所有有效Key提前加载到缓存和布隆过滤器,减少“新Key未同步”导致的穿透。
- 兜底返回:对于无法通过布隆过滤器或参数校验拦截的请求,返回兜底数据(如“数据不存在”),避免直接访问数据库。
四、缓存穿透 vs 缓存击穿 vs 缓存雪崩
| 场景 | 核心原因 | 解决方案核心思路 |
|---|---|---|
| 缓存穿透 | 查询不存在的Key,缓存和数据库均无数据 | 缓存空值、布隆过滤器、参数校验 |
| 缓存击穿 | 热点Key失效,大量并发请求穿透 | 分布式锁、热点Key永不过期 |
| 缓存雪崩 | 大量Key集中失效或缓存服务宕机 | 随机过期时间、缓存集群高可用、熔断降级 |

5万+

被折叠的 条评论
为什么被折叠?



