PHP缓存穿透怎么查

wen PHP项目 4

本文目录导读:

PHP缓存穿透怎么查

  1. 缓存穿透的识别与排查
  2. 常见原因分析
  3. 解决方案
  4. 性能监控与告警
  5. 实战排查流程
  6. 最佳实践建议

我来详细讲解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缓存穿透问题,重点是要建立完善的监控体系,及时发现异常,并采取多层防护措施。

抱歉,评论功能暂时关闭!