一、缓存雪崩
问题原理
大量缓存数据在同一时间过期或Redis集群宕机,导致所有请求直接穿透到数据库,造成数据库压力激增甚至崩溃。雪崩发生时,Redis中资源存在但已过期,数据库资源实际存在。
PHP示例:
// 危险做法:批量设置相同过期时间
$productIds = range(1, 10000);
foreach ($productIds as $id) {
$key = "product_{$id}";
$redis->setex($key, 3600, get_product_data($id)); // 全部1小时后同时过期
}
// 雪崩发生时的请求处理
function get_product(int $ id) {
$key = "product_{$id}";
if (!$data = $redis->get($key)) {
// 所有请求同时进入这里查询数据库
$data = query_database("SELECT * FROM products WHERE id = {$id}");
$redis->setex($key, 3600, $data);
}
return $data;
}
解决方案:
- 随机过期时间:
$expire = 3600 + random_int(0, 600); // 基础1小时+随机10分钟 $redis->setex($key, $expire, $data);
- 双层缓存策略:
// 主缓存 $redis->setex($key, 3500, $data); // 备份缓存(永不过期) $redis->set("backup_{$key}", $data);
- 熔断降级机制:
use Swoole\Coroutine; if ($dbConcurrentRequests > 1000) { // 监控数据库压力 Coroutine::sleep(0.1); // 延迟处理 return cached_fallback_data(); // 返回降级数据 }
二、缓存击穿
问题原理
热点Key突然失效时,大量并发请求同时穿透缓存访问数据库。击穿发生时,Redis中资源已过期,但数据库资源实际存在。
PHP示例:
// 热点商品缓存处理
function get_hot_product(int $id) {
$key = "hot_product_{$id}";
if (!$data = $redis->get($key)) { // 缓存失效瞬间
// 瞬间大量并发请求到达此处
$data = query_database("SELECT * FROM products WHERE id = {$id}");
$redis->setex($key, 300, $data); // 重新设置缓存
}
return $data;
}
解决方案:
- 分布式锁(互斥锁):
$lockKey = "lock:{$key}"; if (!$redis->exists($key)) { if ($redis->set($lockKey, 1, ['NX', 'EX' => 5])) { // 获取锁 $data = get_from_db($id); $redis->setex($key, 300, $data); $redis->del($lockKey); } else { usleep(200000); // 等待200ms return $this->get_hot_product($id); // 重试 } }
- 逻辑过期策略:
$data = [ 'expire' => time() + 300, // 逻辑过期时间 'value' => $realData ]; $redis->set($key, json_encode($data)); // 永不过期 // 读取时检查逻辑过期 $cached = json_decode($redis->get($key), true); if ($cached['expire'] < time()) { // 异步更新缓存 start_async_update($key); }
三、缓存穿透
问题原理
恶意查询不存在的数据(如id=-1),绕过缓存直接访问数据库。穿透发生时,Redis和数据库中资源均不存在。
PHP示例:
// 未做防护的查询
function get_user(int $id) {
$key = "user_{$id}";
if (!$data = $redis->get($key)) {
// 恶意请求反复查询不存在的ID
$data = query_database("SELECT * FROM users WHERE id = {$id}");
// 未缓存空结果,导致每次都会查库
}
return $data;
}
解决方案:
- 布隆过滤器拦截:
// 初始化布隆过滤器 $redis->executeRaw(['BF.RESERVE', 'user_filter', '0.001', '1000000']); // 查询前检查 if (!$redis->executeRaw(['BF.EXISTS', 'user_filter', $id])) { return ['error' => 'Data not exists']; // 直接拦截 } // 数据入库时添加标记 $redis->executeRaw(['BF.ADD', 'user_filter', $newUserId]);
- 空值缓存策略:
if (!$data = $redis->get($key)) { $data = get_from_db($id); if (empty($data)) { $redis->setex($key, 300, 'NULL'); // 缓存空值 return null; } $redis->setex($key, 3600, $data); }
- 请求参数校验:
// 拦截非法请求 if ($id <= 0 || !is_numeric($id)) { http_response_code(400); exit('Invalid ID parameter'); }
四、问题排查与诊断方法
当无法确定问题类型时,通过以下步骤排查:
-
监控指标分析:
// 获取Redis关键指标 $info = $redis->info(); $hitRate = $info['keyspace_hits'] / ($info['keyspace_hits'] + $info['keyspace_misses']); $expiredKeys = $info['expired_keys']; // 判断标准: if ($hitRate < 0.8) { if ($expiredKeys > 1000) return "可能雪崩"; if (array_key_exists('malicious_ids', $_REQUEST)) return "可能穿透"; return "可能击穿"; }
-
日志追踪模式:
// 开启请求日志 function cache_get($key) { $start = microtime(true); $data = $redis->get($key); if ($data === false) { // 记录缓存未命中 log_message("CACHE_MISS:{$key}:" . (microtime(true)-$start)); if (is_malicious_key($key)) { log_message("PENETRATION_SUSPECTED:{$key}"); } } return $data; }
-
压力测试诊断:
# 模拟雪崩(批量过期) redis-cli keys "product_*" | xargs redis-cli expire 1 # 模拟击穿(删除热点key) redis-cli del "hot_product_123" # 模拟穿透(请求不存在key) ab -n 10000 -c 100 "http://api.example.com/user/-1"
五、通用防御体系
-
多级缓存架构:
// L1: 本地缓存 (APCu) if ($data = apcu_fetch($key)) return $data; // L2: Redis集群 if ($data = $redis->get($key)) { apcu_store($key, $data, 60); // 回写本地缓存 return $data; } // L3: 数据库查询(带保护) $data = protected_db_query($key);
-
动态熔断机制:
class CircuitBreaker { private $failCount = 0; public function query($sql) { if ($this->failCount > 10) { throw new Exception("DB熔断开启"); } try { $result = $db->query($sql); $this->failCount = max(0, $this->failCount-2); return $result; } catch (Exception $e) { $this->failCount++; throw $e; } } }
-
缓存预热策略:
// 定时任务预热热点数据 $scheduler->addTask('0 3 * * *', function() { $hotProducts = get_hot_products_from_logs(); foreach ($hotProducts as $id) { $key = "product_{$id}"; $data = query_database(...); $redis->setex($key, 86400, $data); // 24小时缓存 } });
总结对比表
问题类型 | 触发条件 | 数据库压力特征 | 核心解决方案 |
---|---|---|---|
雪崩 | 批量缓存同时过期 | 持续高压 | 随机过期时间+双层缓存 |
击穿 | 热点Key失效 | 瞬时峰值 | 分布式锁+逻辑过期 |
穿透 | 查询不存在数据 | 持续无效请求 | 布隆过滤器+空值缓存 |
最佳实践建议:
监控缓存命中率:
$redis->info('stats')['keyspace_hits']
使用
RedisBloom
模块实现高效布隆过滤器关键业务采用
Redis Cluster
+Sentinel
高可用架构结合
Prometheus
+Grafana
建立监控看板