本文目录导读:

- 应用层缓存(最常用、最灵活)
- 数据库查询缓存机制(MySQL 8.0 之前版本)
- 数据库端查询优化
- 使用 PDO 的持久连接与缓冲查询
- 预编译语句(Prepared Statements)
- 使用数据库连接池(高并发场景)
- 具体场景的缓存策略
- 监控与诊断工具
- 最佳实践路线图
在PHP项目中优化数据库查询缓存,可以从应用层缓存、数据库内置缓存、查询优化以及架构层面四个维度入手,以下是系统性的优化策略和具体实现方案:
应用层缓存(最常用、最灵活)
这是将查询结果直接存储到内存中,避免重复执行SQL。
a. 使用 Redis / Memcached(推荐)
将复杂的、耗时且不频繁更新的查询结果缓存起来。
// 查询缓存示例(使用 Redis)
function getCachedUserList($db, $redis) {
$cacheKey = 'user:active:list';
$data = $redis->get($cacheKey);
if (!$data) {
// 缓存未命中,执行数据库查询
$stmt = $db->query("SELECT id, name FROM users WHERE status = 1");
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 设置缓存,过期时间 300 秒
$redis->setex($cacheKey, 300, serialize($data));
} else {
$data = unserialize($data);
}
return $data;
}
关键点:
- 缓存键设计:使用
函数名_参数MD5或SQL摘要作为键。 - 过期策略:根据数据更新频率设置 TTL(如 60s~1h)。
- 缓存穿透:对于空结果也要缓存(如空数组),防止恶意请求击穿数据库。
- 缓存雪崩:设置随机过期时间(如
base_ttl + rand(0, 120))。
b. 使用文件缓存(适用于简单场景、无Redis时)
对于低并发、服务器资源有限的情况,可以使用文件缓存。
function getExpensiveData() {
$cacheFile = '/tmp/data_cache_' . md5('query_key') . '.cache';
$cacheTime = 300; // 缓存5分钟
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < $cacheTime)) {
return file_get_contents($cacheFile);
}
// 执行数据库查询
$data = $db->query("SELECT ...")->fetchAll();
file_put_contents($cacheFile, serialize($data));
return $data;
}
数据库查询缓存机制(MySQL 8.0 之前版本)
MySQL 的 Query Cache(查询缓存)在 8.0 版本已被移除,因为在高并发场景下性能反而下降,如果你仍在使用 MySQL 5.7 及以下,可以启用:
# my.cnf 配置 query_cache_type = 1 query_cache_size = 64M # 根据内存情况调整 query_cache_limit = 2M # 单个查询结果最大缓存大小
注意: 表一旦有写操作(INSERT/UPDATE/DELETE),该表的所有缓存都会失效,适合读多写少的场景。
数据库端查询优化
缓存不是银弹,优化查询本身可以减少不必要的数据库压力。
a. 使用 Explain 分析慢查询
EXPLAIN SELECT * FROM users WHERE status = 1 ORDER BY created_at DESC;
- 检查
type是否为ALL(全表扫描)或index。 - 检查
Extra中是否有Using filesort(需要优化索引)。
b. 合理创建索引
-- 复合索引:最左前缀原则 ALTER TABLE orders ADD INDEX idx_user_status_date (user_id, status, created_at); -- 覆盖索引:避免回表查询 ALTER TABLE users ADD INDEX idx_cover (id, name, email) WHERE status = 1;
c. 避免重复查询
// 错误:循环中多次查询数据库
$userIds = [1,2,3,4,5];
foreach ($userIds as $id) {
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
$users[] = $stmt->fetch();
}
// 正确:一次查询,IN 子句
$placeholders = implode(',', array_fill(0, count($userIds), '?'));
$stmt = $db->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($userIds);
$users = $stmt->fetchAll();
使用 PDO 的持久连接与缓冲查询
持久连接:减少建立/关闭连接的开销(适用于长连接场景,如 Swoole / Workerman)。
$db = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass', [
PDO::ATTR_PERSISTENT => true
]);
缓冲查询:默认 PDO 使用缓冲模式(所有结果一次性返回),适用于结果集较小的查询,如果结果集非常大(百万级),可以使用非缓冲模式:
$stmt = $db->prepare("SELECT * FROM large_table", [
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false
]);
// 此时逐行获取,避免内存溢出
预编译语句(Prepared Statements)
不仅防止 SQL 注入,还能让 MySQL 缓存执行计划,对相同结构的查询加速。
// 预编译语句的缓存
$stmt = $db->prepare("SELECT * FROM users WHERE email = ?");
$stmt->execute(['user1@example.com']);
$stmt->execute(['user2@example.com']); // 第二次执行时,SQL解析被复用
使用数据库连接池(高并发场景)
PHP 传统模型(如 FPM)每个请求独立连接,连接池提升有限,但在 Swoole / Workerman / Hyperf 等常驻内存框架中非常有效:
// Hyperf 的数据库连接池配置
return [
'default' => [
'driver' => 'pdo',
'host' => '127.0.0.1',
'pool' => [
'min_connections' => 5,
'max_connections' => 50,
],
'cache' => [
'adapter' => 'redis', // 配合查询缓存
],
],
];
具体场景的缓存策略
| 数据类型 | 缓存策略 | 过期时间 | 示例 |
|---|---|---|---|
| 用户会话 | Redis + Hash | 30分钟 | session:user_id |
| 文章列表 | Redis List + 分页 | 5分钟 | posts:page:1 |
| 计数器(点赞) | Redis INCR | 同步到DB | post:123:likes |
| 配置信息 | Redis String | 1小时 | site:config:json |
| 统计报表 | 文件缓存 + 定时任务 | 1天 | report/daily_20241005.cache |
监控与诊断工具
- 慢查询日志:
SET GLOBAL slow_query_log = 1; - EXPLAIN 分析:逐步优化索引。
- Redis 监控:
INFO commandstats查看执行次数。 - APM 工具:如 SkyWalking、Pinpoint 检测数据库调用。
最佳实践路线图
- 先优化 SQL 和索引(成本最低,效果最明显)。
- 对热点查询(如首页、列表页)添加 Redis 缓存。
- 为缓存设置合理的过期时间和防御策略(防穿透、防雪崩)。
- 如果使用 MySQL 5.7,可考虑 Query Cache;8.0 则忽略。
- 高并发场景下考虑 Swoole + 连接池。
- 持续监控,对慢查询进一步优化。
通过以上方法,你可以将大部分数据库查询命中率提升到 85% 以上,有效降低数据库 CPU 和 IO 压力。