Redis-缓存(穿透、击穿、雪崩)
缓存穿透
问题定义
缓存穿透是指查询不存在的数据,导致请求绕过缓存直接访问底层存储系统(如数据库)的现象。与缓存击穿/雪崩的关键区别在于请求的是系统中根本不存在的资源。
典型场景
- 恶意攻击:故意构造大量不存在的ID发起请求
- 业务缺陷:爬虫抓取超出范围的页面ID
- 数据淘汰:已删除数据仍被频繁请求
系统危害
mermaid
graph LR
A[大量非法请求] --> B[缓存层失效]
B --> C[数据库压力激增]
C --> D[连接池耗尽]
D --> E[系统整体崩溃]解决方案
布隆过滤器(Bloom Filter)
实现原理:
- 位数组 + k个哈希函数
- 空间效率极高的概率型数据结构
- 特性:
- 若判定"不存在"则100%准确
- 若判定"存在"可能有误判(可控制)
Redis实现:
bash# 使用RedisBloom模块(推荐) > BF.RESERVE myFilter 0.001 1000000 # 创建过滤器(0.1%误判率,100万容量) > BF.ADD myFilter validKey1 # 添加合法键 > BF.EXISTS myFilter invalidKey # 检查键是否存在(返回0表示肯定不存在) # 原生Redis模拟实现(String+位操作) > SETBIT bf 12345 1 # 模拟哈希函数1 > SETBIT bf 67890 1 # 模拟哈希函数2 > GETBIT bf 12345 # 检查位优化技巧:
- 动态扩容过滤器
- 结合多个小过滤器降低误判率
空值缓存
- 实现方案:
java
// 伪代码示例
public Object getData(String key) {
Object value = redis.get(key);
if (value == null) {
if (redis.exists("null_" + key)) { // 检查空值标记
return null;
}
value = db.get(key);
if (value == null) {
redis.setex("null_" + key, 300, ""); // 空值缓存5分钟
} else {
redis.setex(key, 3600, value); // 正常缓存1小时
}
}
return value;
}- 注意事项:
- 设置合理的短TTL(通常5-30分钟)
- 定期清理历史空值键
- 对恶意攻击可结合IP限流
数据预热
Redis服务启动时加载热点数据到布隆过滤器。
缓存击穿
问题定义
缓存击穿(Cache Breakdown)是指在高并发场景下,当热点数据缓存过期的瞬间,大量请求同时穿透缓存直接访问数据库的现象。与缓存穿透(查询不存在的数据)和缓存雪崩(大量缓存同时失效)不同,缓存击穿特指单个热点key失效引发的数据库压力激增问题。
典型场景
- 电商秒杀:热门商品库存信息缓存过期时,瞬时数万查询涌向数据库
- 新闻热点:突发新闻内容缓存失效后,大量用户同时刷新页面
- 社交feed流:明星动态缓存过期引发粉丝集体访问
系统危害
mermaid
graph LR
A[热点key失效] --> B[万级QPS穿透缓存]
B --> C[数据库连接池耗尽]
C --> D[数据库响应延迟飙升]
D --> E[服务雪崩]解决方案
互斥锁
核心思想:通过分布式锁确保只有一个请求能重建缓存,其他请求等待或重试。
Redis分布式锁实现:
bash# 使用SETNX实现锁(推荐Redisson框架) SET lock_key unique_value NX PX 3000 # 获取3秒锁完整流程:
mermaid
graph TD
A[获取缓存] --> B{存在?}
B -->|是| C[返回数据]
B -->|否| D[获取分布式锁]
D --> E{获锁成功?}
E -->|是| F[查询DB]
F --> G[重建缓存]
G --> H[释放锁]
E -->|否| I[短暂睡眠后重试]- 优化要点:
- 锁超时时间要短于业务超时时间
- 必须设置唯一value防误删
- 推荐使用Redisson的看门狗机制
逻辑过期
数据结构设计:
json{ "value": "真实数据", "expire": 1672500000 // 逻辑过期时间戳 }处理流程:
pythondef get_data(key): data = redis.get(key) if data is None: return load_db_data(key) # 首次加载 if data['expire'] > time.now(): return data['value'] else: if acquire_lock(key): # 获取更新锁 try: new_data = load_db_data(key) redis.set(key, pack_data(new_data)) finally: release_lock(key) return data['value'] # 返回旧数据优势:
- 不阻塞请求
- 保证数据最终一致性
热点数据永不过期
实现方案:
- 不设置TTL,通过后台定时任务定期更新
- 采用延迟双删策略:bash
redis.del(key) # 第一次删除 db.update() # 更新数据库 sleep(500ms) # 等待主从同步 redis.del(key) # 第二次删除
缓存预热
实施步骤:
- 热点发现:通过实时监控(如Redis的hotkeys命令)识别热点key
- 预加载机制:java最佳实践:在业务低峰期执行预热
@Scheduled(fixedRate = 5 * 60 * 1000) // 每5分钟执行 public void preloadHotData() { List<String> hotKeys = monitor.getHotKeys(); hotKeys.forEach(key -> { Object data = db.query(key); redis.set(key, data); }); }
监控指标
| 指标名称 | 报警阈值 | 监控工具 |
|---|---|---|
| 缓存击穿次数 | >50次/分钟 | Prometheus |
| 数据库QPS突增 | >300%基线 | Grafana |
| 分布式锁等待时间 | >500ms | ELK日志分析 |
缓存雪崩
问题定义
缓存雪崩是指大量缓存数据在同一时间失效或缓存服务整体宕机,导致所有请求直接涌向数据库,造成数据库压力激增甚至崩溃的连锁反应现象。与缓存击穿(单个热点key失效)和缓存穿透(查询不存在数据)不同,缓存雪崩的特点是影响范围大、系统级风险高。
典型场景
- 定时任务刷新缓存:凌晨批量更新缓存时设置了相同的过期时间
- 缓存服务重启:Redis集群整体重启导致所有缓存丢失
- 突发流量冲击:热点数据集中失效引发数据库查询风暴
系统级连锁反应
mermaid
graph TD
A[大量缓存同时失效] --> B[万级QPS直连数据库]
B --> C[数据库连接池耗尽]
C --> D[数据库响应延迟飙升]
D --> E[应用服务线程阻塞]
E --> F[系统全面崩溃]解决方案
过期时间分散化(基础防护)
原理:通过为不同key设置随机过期时间偏移量,避免集中失效。
实现方案:
java
// Java示例:基础TTL+随机偏移
int baseTTL = 3600; // 1小时基础过期
int randomTTL = new Random().nextInt(300); // 0-5分钟随机
redisTemplate.opsForValue().set(key, value, baseTTL + randomTTL, TimeUnit.SECONDS);最佳实践:
- 热点数据:±5%随机偏移(如1小时±3分钟)
- 普通数据:±20%随机偏移(如1小时±12分钟)
多级缓存架构(立体防御)
分层设计:
mermaid
graph BT
A[客户端] --> B[浏览器缓存]
A --> C[CDN缓存]
A --> D[Nginx缓存]
D --> E[本地缓存]
E --> F[Redis集群]
F --> G[数据库]各级配置建议:
| 缓存层级 | 技术实现 | TTL策略 | 承载量级 |
|---|---|---|---|
| 浏览器 | Cookie/Storage | 60s固定 | 万级QPS |
| CDN | 边缘节点 | 300s固定 | 百万级QPS |
| 本地缓存 | Caffeine | 30s±10s | 十万级QPS |
| Redis | Cluster | 1h±5min | 百万级QPS |
高可用集群部署(基础设施)
Redis集群方案对比:
| 方案 | 特点 | 适用场景 | 故障恢复时间 |
|---|---|---|---|
| 主从+哨兵 | 配置简单 | 中小规模系统 | 10-30秒 |
| Redis Cluster | 自动分片 | 大规模系统 | 秒级切换 |
| 多活架构 | 跨机房容灾 | 金融级系统 | 毫秒级切换 |
熔断降级机制(应急防护)
三级防护体系:
限流:
yaml# Spring Cloud Gateway配置示例 filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 1000 # 每秒令牌数 redis-rate-limiter.burstCapacity: 2000 # 峰值容量熔断:
java// Hystrix配置示例 @HystrixCommand( fallbackMethod = "getDefaultData", commandProperties = { @HystrixProperty(name="circuitBreaker.requestVolumeThreshold", value="20"), @HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds", value="5000") } )降级:
- 核心数据:返回本地缓存或静态数据
- 非核心数据:直接返回"服务繁忙"提示
热点数据永不过期+异步更新(特殊场景)
实现方案:
python
def get_data(key):
data = redis.get(key)
if data['expire'] < time.time(): # 逻辑过期检查
mq.publish('cache_update', key) # 触发异步更新
return data['value']
# 消费者处理
def update_cache(key):
with distributed_lock(key): # 分布式锁
new_data = db.query(key)
redis.set(key, {
'value': new_data,
'expire': time.time() + 3600 # 重置1小时
})适用场景:
- 商品详情页
- 秒杀活动数据
- 实时排行榜
