1. 概述
在高并发系统中,缓存是提升性能、降低数据库压力的重要组件。然而,在实际应用中,缓存可能会遇到三种典型问题:缓存穿透、缓存击穿和缓存雪崩。这些问题如果不加以妥善处理,可能会导致数据库负载激增,甚至影响整个系统的稳定性。本文将详细介绍这三种问题的区别,并探讨相应的解决方案。
2. 什么是缓存穿透、缓存击穿和缓存雪崩?
2.1 缓存穿透(Cache Penetration)
现象:指的是缓存和数据库中都没有的数据,但却被大量请求访问,导致请求直接落到数据库上,增加数据库的压力。
速记口诀:恶意请求绕过缓存,访问数据库中不存在的数据,导致数据库负载增加。
2.2 缓存击穿(Cache Breakdown)
现象:某个热点数据在缓存中过期的瞬间,刚好有大量请求同时到达,此时所有请求都会直接访问数据库,可能会导致数据库压力激增。
速记口诀:缓存数据突然失效,大量请求瞬间涌向数据库。
2.3 缓存雪崩(Cache Avalanche)
现象:大量缓存在同一时间过期,使得所有原本应命中缓存的请求都直接访问数据库,造成数据库负载飙升,甚至引发全局性故障。
速记口诀:大量缓存同时失效,流量直击数据库,引发系统风险。
3. 解决方案
3.1 缓存穿透解决方案
方案 1:使用布隆过滤器(Bloom Filter)
- 在缓存层引入布隆过滤器,提前存储可能存在的数据标识,对于不存在的数据,直接拦截请求,避免打到数据库。
方案 2:缓存空值
- 针对数据库和缓存均不存在的数据,可以在缓存中存储一个短时间有效的空值(如
5 分钟
),减少对数据库的冲击。
3.2 缓存击穿解决方案
方案 1:设置热点数据永不过期
- 对于高频访问的热点数据,可以设置永不过期,确保缓存始终存在,避免瞬间失效造成数据库负载激增。
方案 2:互斥锁机制
- 在缓存数据为空时,采用互斥锁(如
setnx
方式),确保只有一个请求能查询数据库并更新缓存,其他请求需等待或短暂休眠后重试。
public static String getProductDescById(String id) {String desc = redis.get(id);if (desc == null) {if (redis.setnx("lock_id", 1, 60) == 1) {try {desc = getFromDB(id);redis.set(id, desc, 60 * 60 * 24);} catch (Exception ex) {LogHelper.error(ex);} finally {redis.del("lock_id");return desc;}} else {Thread.sleep(200);return getProductDescById(id);}}return desc;
}
3.3 缓存雪崩解决方案
方案 1:设置缓存过期时间随机化
- 避免大量缓存同时过期,可以在设定缓存时增加一个随机波动值。例如,原定
10 分钟
过期的缓存,可以随机增加1~3 分钟
,使过期时间分布在7~13 分钟
之间。
redis.set(id, value, 60 * 60 + Math.random() * 1000);
方案 2:热点数据分片存储
- 通过分布式缓存架构(如 Redis Cluster),将不同热点数据分布存储在不同的节点/机房,防止单点故障引发缓存雪崩。
方案 3:双层缓存机制(A/B 缓存)
- 维护两层缓存:A 缓存(正常过期) 和 B 缓存(不过期)。当
A
缓存失效时,读取B
缓存,并异步更新**A**
缓存,确保数据一致性。
4. 总结
问题 | 定义 | 触发条件 | 影响 | 解决方案 |
---|---|---|---|---|
缓存穿透 | 访问数据库中不存在的数据,导致所有请求直接打到数据库 | 缓存和数据库都无数据 | 数据库负载增加 | 布隆过滤器、缓存空值 |
缓存击穿 | 热点数据过期的瞬间,大量请求同时访问数据库 | 高并发下,某个热点数据突然失效 | 数据库瞬时压力飙升 | 设置永不过期、互斥锁机制 |
缓存雪崩 | 大量缓存数据同时过期,导致数据库负载剧增 | 大量缓存数据设定相同的过期时间 | 可能影响整个系统 | 随机化过期时间、热点数据分片、双层缓存 |
缓存作为高并发架构中的重要组成部分,合理的缓存设计可以极大提升系统的性能,降低数据库负载。然而,面对缓存穿透、缓存击穿和缓存雪崩等问题,我们需要根据具体场景采取相应的防范措施,确保系统的稳定性和可靠性。