本文目录导读:

我来详细讲解PHP缓存穿透的排查方法和解决方案。
缓存穿透的识别与排查
现象确认
// 异常特征: // - 缓存命中率骤降 // - 数据库QPS突然飙升 // - 大量请求查询不存在的数据
监控指标排查
// 1. 监控缓存命中率
$stats = $redis->info();
$hits = $stats['keyspace_hits'];
$misses = $stats['keyspace_misses'];
$hitRate = $hits / ($hits + $misses) * 100;
if ($hitRate < 50) {
// 可能存在穿透风险
logger::alert("缓存命中率过低: " . $hitRate . "%");
}
// 2. 监控数据库慢查询
// 查看数据库慢查询日志
// MySQL: SHOW VARIABLES LIKE 'slow_query_log';
// 找到特定时间段的慢查询记录
// 3. 监控异常请求
// 检查日志中的异常请求模式
$log = tail('/var/log/php/error.log');
if (preg_match('/SELECT.*WHERE id=(\d+)/', $log, $matches)) {
// 大量非连续ID查询
logger::warning("疑似缓存穿透请求");
}
日志分析
// 添加专门的穿透检测日志
class CacheService {
public function get($key) {
// 记录所有未命中的请求
$value = $this->redis->get($key);
if ($value === false) {
$this->logCacheMiss($key, $_SERVER['REQUEST_URI']);
}
return $value;
}
private function logCacheMiss($key, $uri) {
$log = [
'time' => date('Y-m-d H:i:s'),
'key' => $key,
'uri' => $uri,
'ip' => $_SERVER['REMOTE_ADDR']
];
file_put_contents('/var/log/cache_miss.log', json_encode($log) . "\n", FILE_APPEND);
}
}
常见原因分析
代码层面排查
// 检查是否有无效查询
class ProductService {
public function getProduct($id) {
// 问题代码
$data = $this->cache->get("product:" . $id);
if (!$data) {
// 直接查询数据库,没有空值缓存
$data = $this->db->query("SELECT * FROM products WHERE id = $id")->fetch();
// 如果不存在,每次都查数据库
}
return $data;
}
}
业务场景分析
// 高并发请求不存在的数据
// 示例:攻击者请求 -1 或超大ID
$id = $_GET['id']; // 可能是-1, 0, 999999999
$data = $cache->get("user:" . $id);
if (!$data) {
// 每次都穿透到数据库
$user = $db->query("SELECT * FROM users WHERE id = " . $id);
// 不存在则每次查询都落空
}
解决方案
空值缓存
class ProductService {
private $cache;
private $db;
public function getProduct($id) {
// 参数过滤
if (!is_numeric($id) || $id <= 0) {
return null;
}
$cacheKey = "product:{$id}";
$data = $this->cache->get($cacheKey);
// 检查是否为空值标记
if ($data === 'NULL_VALUE') {
return null;
}
if (!$data) {
$data = $this->db->query(
"SELECT * FROM products WHERE id = ?",
[$id]
)->fetch();
if (!$data) {
// 缓存空值,设置较短过期时间
$this->cache->set($cacheKey, 'NULL_VALUE', 60); // 1分钟
return null;
}
// 正常缓存
$this->cache->set($cacheKey, $data, 3600);
}
return $data;
}
}
布隆过滤器
class BloomFilter {
private $redis;
private $bfKey = 'product_bloom_filter';
// 初始化布隆过滤器
public function init() {
// 创建布隆过滤器,错误率0.01%,预期元素100万
$this->redis->rawCommand(
'BF.RESERVE',
$this->bfKey,
'0.0001',
'1000000'
);
}
// 添加商品ID到过滤器
public function add($id) {
$this->redis->rawCommand('BF.ADD', $this->bfKey, $id);
}
// 批量添加
public function addBatch($ids) {
$pipeline = $this->redis->pipeline();
foreach ($ids as $id) {
$pipeline->rawCommand('BF.ADD', $this->bfKey, $id);
}
$pipeline->exec();
}
// 检查是否存在(可能有误判)
public function mightContain($id) {
return $this->redis->rawCommand('BF.EXISTS', $this->bfKey, $id);
}
}
// 使用布隆过滤器
class ProductService {
private $bloomFilter;
public function getProduct($id) {
// 先检查布隆过滤器
if (!$this->bloomFilter->mightContain($id)) {
return null; // 一定不存在
}
// 可能存在的才查询缓存和数据库
$cacheKey = "product:{$id}";
$data = $this->cache->get($cacheKey);
if (!$data) {
$data = $this->db->query(
"SELECT * FROM products WHERE id = ?",
[$id]
)->fetch();
if ($data) {
$this->cache->set($cacheKey, $data, 3600);
}
}
return $data;
}
}
请求限流与IP限制
class RateLimiter {
private $redis;
private $limit = 10; // 每分钟最多10次
private $window = 60;
public function check($ip, $action) {
$key = "rate_limit:{$ip}:{$action}";
$count = $this->redis->incr($key);
if ($count === 1) {
$this->redis->expire($key, $this->window);
}
return $count <= $this->limit;
}
public function block($ip, $duration = 3600) {
$key = "blocked_ip:{$ip}";
$this->redis->set($key, 1, $duration);
}
public function isBlocked($ip) {
return $this->redis->exists("blocked_ip:{$ip}");
}
}
// 使用限流
class ApiController {
public function detail($id) {
$ip = $_SERVER['REMOTE_ADDR'];
$rateLimiter = new RateLimiter();
// 检查是否被封锁
if ($rateLimiter->isBlocked($ip)) {
return response(403, ['message' => 'IP已被临时封锁']);
}
// 检查访问频率
$action = "product_detail";
if (!$rateLimiter->check($ip, $action)) {
// 记录异常并封锁
logger::warning("高频访问: {$ip} - action: {$action}");
$rateLimiter->block($ip, 600);
return response(429, ['message' => '请求过于频繁']);
}
// 正常业务逻辑
$service = new ProductService();
return $service->getProduct($id);
}
}
参数校验与规范化
class RequestValidator {
public function validateProductId($id) {
// 1. 类型检查
if (!is_numeric($id)) {
throw new InvalidArgumentException('无效的商品ID');
}
// 2. 范围检查
$id = (int)$id;
if ($id < 1 || $id > 1000000) {
throw new OutOfRangeException('商品ID超出范围');
}
// 3. 格式规范化
// 防止 001 和 1 造成重复查询
return $id;
}
public function validateSearchParams($params) {
// 黑名单关键词
$blocked = ['union', 'select', 'where', 'and', 'or', '--'];
foreach ($params as $key => $value) {
foreach ($blocked as $word) {
if (stripos($value, $word) !== false) {
throw new SecurityException('非法搜索关键词');
}
}
}
return true;
}
}
性能监控与告警
class CacheMonitor {
private $redis;
private $db;
public function monitor() {
$metrics = [];
// 1. 缓存命中率
$metrics['hit_rate'] = $this->getCacheHitRate();
// 2. 数据库连接数
$metrics['db_connections'] = $this->getDbConnections();
// 3. 慢查询数量
$metrics['slow_queries'] = $this->getSlowQueries();
// 4. 异常请求比例
$metrics['abnormal_requests'] = $this->getAbnormalRequestCount();
// 检查是否超过阈值
if ($metrics['hit_rate'] < 80) {
$this->sendAlert('缓存命中率过低', $metrics);
}
return $metrics;
}
}
// 部署监控页面
// /monitor/cache.php
$monitor = new CacheMonitor();
$data = $monitor->monitor();
echo json_encode($data, JSON_PRETTY_PRINT);
实战排查流程
// 1. 查看今日缓存命中率 redis-cli INFO stats | grep hits // 2. 查看数据库慢查询 mysql> SHOW GLOBAL STATUS LIKE 'Slow_queries'; mysql> SHOW FULL PROCESSLIST; // 3. 分析PHP日志 tail -f /var/log/php/error.log | grep -i "cache miss\|穿透" // 4. 使用tcpdump分析网络请求 tcpdump -i eth0 -c 1000 port 3306 | grep SELECT // 5. 通过Xdebug分析具体代码 // 配置xdebug xdebug.start_with_request=yes xdebug.mode=debug
最佳实践建议
final class CacheSolution {
// 1. 分层缓存策略
public static function layeredCache($key, $callback) {
// L1: 本地内存缓存
$local = self::getFromLocal($key);
if ($local) return $local;
// L2: Redis缓存
$redis = self::getFromRedis($key);
if ($redis) {
self::setToLocal($key, $redis, 30); // 30秒
return $redis;
}
// L3: 查数据库
$data = $callback();
// 空值处理
if ($data === null) {
self::setToRedis($key, null, 60); // 1分钟
} else {
self::setToRedis($key, $data, 3600); // 1小时
}
return $data;
}
// 2. 预热机制
public static function warmUp() {
// 启动时加载热门数据
$hotProducts = DB::query("SELECT * FROM products WHERE hot = 1");
foreach ($hotProducts as $product) {
self::setToRedis("product:" . $product['id'], $product, 3600);
}
}
// 3. 自动降级
public static function degradeIfAbnormal() {
$hitRate = self::getHitRate();
if ($hitRate < 50) {
// 触发降级模式
Redis::disableWrite(); // 停止缓存写入
// 启用本地文件缓存
self::enableFileCache();
}
}
}
通过以上方法,你可以系统地排查和解决PHP缓存穿透问题,重点是要建立完善的监控体系,及时发现异常,并采取多层防护措施。