一、缓存穿透:不存在的数据请求如何防御?
1.1 问题本质与攻击场景
当用户频繁查询数据库中不存在的数据时,缓存层无法命中请求,所有流量直接穿透至数据库。例如某电商系统查询ID为-1的商品,若该ID在数据库中不存在,每次查询都会触发数据库访问。攻击者若利用此特性构造大量非法请求,可在短时间内使数据库连接池耗尽,导致系统瘫痪。
1.2 防御方案对比
方案一:空值缓存+短过期时间
// 伪代码示例:缓存空值public Object getData(String key) {Object value = redis.get(key);if (value == null) {value = db.query(key); // 数据库查询if (value == null) {redis.setex(key, "", 60); // 缓存空值60秒return null;}redis.setex(key, value, 3600); // 缓存有效数据1小时}return value;}
适用场景:适用于读多写少且非法请求集中的业务,如用户信息查询接口。需注意空值缓存会占用内存,需根据业务特点设置合理过期时间。
方案二:布隆过滤器预过滤
布隆过滤器通过哈希函数将键映射到位数组,可高效判断键是否存在。其特性包括:
- 允许误判(可能将存在键判为不存在)
- 不会漏判(不存在键一定判为不存在)
实现步骤:
- 初始化布隆过滤器:根据业务数据量计算最优哈希函数数量和位数组大小
- 启动时加载所有可能存在的键到过滤器
- 查询前先检查过滤器,若键不存在直接返回
性能对比:
| 方案 | 内存占用 | 查询速度 | 实现复杂度 |
|———————-|—————|—————|——————|
| 空值缓存 | 高 | 中 | 低 |
| 布隆过滤器 | 低 | 极快 | 中 |
二、缓存击穿:热点数据过期的致命瞬间
2.1 典型案例分析
某直播平台的礼物排行榜数据每5分钟更新一次,当整点时刻缓存过期时,数万并发请求同时穿透至数据库,导致MySQL主库CPU飙升至100%。此类场景具有以下特征:
- 极少数键占据大部分请求(通常<1%)
- 缓存过期时间与业务高峰期重合
- 请求具有强一致性要求
2.2 解决方案矩阵
方案A:热点数据永不过期
# 伪代码:后台异步刷新def refresh_hot_key(key):while True:new_value = db.query(key) # 从数据库获取最新值redis.set(key, new_value, 0) # 设置永不过期time.sleep(300) # 每5分钟刷新一次
实施要点:
- 需建立热点键识别机制(可通过监控系统或离线分析)
- 刷新间隔需小于业务容忍的最长不一致时间
- 需处理刷新失败时的降级策略
方案B:互斥锁控制更新
// 基于Redis SETNX实现分布式锁public Object getHotData(String key) {Object value = redis.get(key);if (value == null) {String lockKey = "lock:" + key;if (redis.setnx(lockKey, "1", 10) == 1) { // 获取锁,超时10秒try {value = db.query(key); // 数据库查询redis.setex(key, value, 3600); // 更新缓存} finally {redis.del(lockKey); // 释放锁}} else {Thread.sleep(50); // 等待重试return getHotData(key); // 递归重试}}return value;}
优化方向:
- 使用Redisson等成熟框架的RedLock算法
- 增加锁重试次数和间隔时间
- 结合本地缓存减少递归调用
三、缓存雪崩:批量过期引发的系统级灾难
3.1 灾难重现与影响范围
当缓存集群中大量键的过期时间设置相同(如统一设置为凌晨3点),在过期时刻会形成请求洪峰。某金融系统曾因此导致:
- 数据库连接数激增至3000+(超出MySQL最大连接数)
- 缓存集群QPS下降至正常值的15%
- 业务系统响应时间从50ms飙升至12s
3.2 防御体系构建
方案1:随机过期时间分散压力
-- Lua脚本实现随机过期时间local value = redis.call('GET', KEYS[1])if value == false thenvalue = db.query(KEYS[1]) -- 数据库查询local ttl = math.random(1800, 3600) -- 随机30-60分钟过期redis.call('SETEX', KEYS[1], ttl, value)endreturn value
参数选择原则:
- 基础过期时间应大于业务容忍的最长不一致时间
- 随机范围需根据业务请求量调整(高并发系统建议±10%)
- 需监控实际过期时间分布情况
方案2:多级缓存架构设计
客户端请求↓CDN缓存(静态资源)↓分布式缓存(Redis集群)↓本地缓存(Guava Cache/Caffeine)↓数据库
层级策略:
- CDN层:缓存不变数据,TTL设置较长(如24小时)
- Redis层:缓存热点数据,TTL按业务需求设置
- 本地缓存:缓存极热点数据,采用主动刷新机制
- 数据库:最终数据源,通过异步消息触发更新
方案3:熔断降级机制
当检测到数据库请求量超过阈值时,自动触发以下措施:
- 返回缓存空值或默认值
- 限流部分非核心请求
- 启动备用数据源(如离线计算结果)
实现示例:
# 配置示例(Hystrix风格)circuitBreaker:requestVolumeThreshold: 100 # 10秒内100个请求sleepWindowInMilliseconds: 5000 # 熔断5秒errorThresholdPercentage: 50 # 错误率50%触发熔断
四、最佳实践总结
-
缓存策略选择矩阵:
| 问题类型 | 推荐方案 | 避免方案 |
|——————|—————————————————-|—————————-|
| 缓存穿透 | 布隆过滤器+空值缓存 | 直接查询数据库 |
| 缓存击穿 | 互斥锁+本地缓存 | 无锁并发更新 |
| 缓存雪崩 | 随机过期+多级缓存 | 统一过期时间 | -
监控指标体系:
- 缓存命中率(应>85%)
- 缓存穿透次数(应<1%)
- 数据库请求量(波动率应<30%)
- 锁等待时间(应<100ms)
-
容灾演练建议:
- 每月进行缓存故障模拟测试
- 关键业务保留无缓存访问路径
- 建立跨机房缓存同步机制
通过系统化的缓存设计,可有效提升系统吞吐量3-5倍,降低数据库压力80%以上。在实际应用中,需结合业务特点选择组合方案,并通过持续监控优化参数配置。