本文目录导读:

- 核心原则:区分读/写场景
- SQL查询与索引优化(成本最低、效果最显著)
- 缓存策略(减少数据库直接压力)
- 读写分离与架构拆分
- 数据库连接与事务优化
- 数据库配置与硬件层面(DBA/运维)
- PHP代码层面的配合
- 一个综合优化案例(实战思路)
- 优化的优先级排序
针对PHP项目的数据库读写优化,可以从架构设计、查询优化、缓存策略、连接管理、硬件/配置等多个层面入手,以下是经过实践验证的优化方案:
核心原则:区分读/写场景
大多数Web应用是“读多写少”或“写多读少”,优化的第一步是明确你的负载类型。
SQL查询与索引优化(成本最低、效果最显著)
这是最基础但也最容易被忽视的环节。
-
索引优化:
- 慢查询日志:首先开启MySQL的慢查询日志,定位执行时间超过1秒的SQL。
- EXPLAIN分析:对慢查询使用
EXPLAIN,重点关注type(避免ALL全表扫描)、rows(扫描行数)、Extra(是否Using filesort等)。 - 覆盖索引:查询的字段只从索引中获取,避免回表查询。
SELECT id, name FROM users WHERE status=1,(status, id, name)是一个联合索引,则无需读取数据行。 - 联合索引最左前缀:避免在查询条件中对索引列使用函数或计算(如
WHERE DATE(create_time) = '2024-01-01')。
-
查询语句优化:
- 只查必需字段:严禁
SELECT *,只取需要的列。 - 分页优化:避免使用
LIMIT 100000, 20(偏移量大时性能极差),改用 游标分页(WHERE id > last_id LIMIT 20)或 子查询分页(SELECT * FROM table WHERE id >= (SELECT id FROM table ORDER BY id LIMIT 99999, 1) LIMIT 20)。 - 避免N+1查询:在ORM(如Laravel Eloquent、ThinkPHP模型)中,使用预加载(
with())或延迟加载,而非在循环里触发额外SQL。 - 减少JOIN嵌套:超过3个表的JOIN通常需要重构,考虑冗余字段或反范式化。
- 只查必需字段:严禁
缓存策略(减少数据库直接压力)
缓存是解决读密集场景的最有效手段。
-
应用级缓存(Redis/Memcached):
- 查询缓存:对高频访问且不常变的数据(如配置、分类、热门文章)设置缓存,伪代码示例(PHP):
$key = 'article_' . $id; $article = $redis->get($key); if (!$article) { $article = $db->query("SELECT * FROM articles WHERE id = ?", [$id]); $redis->setex($key, 3600, serialize($article)); // 1小时过期 } - 计数缓存:点赞数、浏览数先写入Redis,再定时批量同步到MySQL。
- 查询缓存:对高频访问且不常变的数据(如配置、分类、热门文章)设置缓存,伪代码示例(PHP):
-
页面静态化:
对于几乎不变的页面(如首页、公告),生成静态HTML文件或使用Edge Side Includes (ESI) 技术,Nginx直接读静态文件,0数据库压力。
-
HTTP缓存:
- 合理设置
Cache-Control、ETag、Last-Modified头,让浏览器或CDN缓存GET请求结果。
- 合理设置
读写分离与架构拆分
当单库无法支撑时,需从架构上分离。
-
主从复制 + 读写分离:
- 写操作(INSERT/UPDATE/DELETE)走主库。
- 读操作走从库(可挂多个从库负载均衡),PHP框架如Laravel、ThinkPHP 6+、Hyperf都内置支持。
- 注意:主从延迟问题,对于需要“立即看到自己写的结果”的场景(如用户刚发布评论),可以给查询加一个强制主库读的标记。
-
分库分表:
- 适合单表超过千万行或数据库连接数瓶颈。
- 垂直拆分:按业务模块拆分到不同库(如用户库、订单库、商品库)。
- 水平拆分:按ID取模或时间范围拆分(如 user_0 ~ user_15 共16张表),PHP中可用 ShardingSphere-Proxy 或 ThinkORM、Hyperf DBAL 等支持分表的ORM。
数据库连接与事务优化
-
连接池:
- PHP传统
mysql_connect或PDO每次请求创建/销毁连接,开销大。 - Swoole / Workerman:常驻内存,通过连接池复用MySQL连接(如
easyswoole/pool)。 - 传统方式:使用
pconnect(长连接)需谨慎,配合连接池中间件如 ProxySQL 或 MySQL Router。
- PHP传统
-
事务控制:
- 保持事务简短:不要在事务里调用远程API或长时间循环。
- 避免在事务中执行
SELECT ... FOR UPDATE锁表,改用乐观锁(版本号)。
数据库配置与硬件层面(DBA/运维)
-
MySQL配置调优:
innodb_buffer_pool_size:设置为物理内存的 70%-80%(InnoDB最重要的参数)。query_cache_type:MySQL 8.0已废弃,不再开启。max_connections:根据服务器内存合理设置(避免过多连接导致OOM)。
-
硬件升级:
- 使用 SSD(尤其是NVMe)替代机械硬盘,随机读写性能提升10-50倍。
- 增加内存以扩大
innodb_buffer_pool。
PHP代码层面的配合
-
批量操作:避免在循环里逐条INSERT/UPDATE,改用批量写入:
// 错误做法:循环100次单条插入 // 正确做法: $sql = "INSERT INTO logs (user_id, action) VALUES "; $values = []; foreach ($logs as $log) { $values[] = "(" . intval($log['user_id']) . ", '" . $db->escape($log['action']) . "')"; } $sql .= implode(',', $values); $db->query($sql); -
延迟写入:对于日志、统计等不要求实时一致的数据,先写入消息队列(RabbitMQ/Kafka/Redis List),再由后台PHP进程消费写入数据库。
-
使用持久化框架(ORM)时注意:
- 关闭框架的调试模式(
debug或log_queries)。 - 禁用不必要的模型事件(如Eloquent的
boot方法里的全局作用域)。
- 关闭框架的调试模式(
一个综合优化案例(实战思路)
场景:一个文章阅读量接口,每次访问(GET /article/123)都更新 articles.views 字段。
优化前的问题:
- 每次请求都
UPDATE articles SET views = views + 1 WHERE id = 123—— 写库频繁,且锁行。 - 每次请求都
SELECT * FROM articles WHERE id = 123—— 读库重复。
优化后方案:
- 读:使用Redis缓存文章内容(含标题、正文、作者),过期时间10分钟,缓存命中时直接从Redis返回,不查MySQL。
- 写(浏览量):仅在Redis中
INCR article:123:views,每满100次(或每10秒)由后台PHP脚本或消息队列一次性批量UPDATE回MySQL(UPDATE articles SET views = views + ? WHERE id = ?)。 - 最终效果:数据库读写压力下降90%以上。
优化的优先级排序
- 第一优先:慢查询日志 + 索引优化(免费、高效)。
- 第二优先:Redis/Memcached缓存热点读数据。
- 第三优先:读写分离代码改造(主从复制需运维配合)。
- 第四优先:分库分表、连接池、硬件升级(成本较高)。
建议用 黑盒压测工具(如JMeter、ab) 和 MySQL监控(如Percona Monitoring and Management, PMM) 动态验证优化效果。