作为一名深耕 PHP 开发五年的程序员,从最初用 ThinkPHP 搭简单接口,到现在维护日均百万请求的电商后台,踩过的坑、趟过的雷能装满一整个笔记。最近刚完成一个老项目的重构优化,把接口响应时间从 800ms 压到了 150ms 以内,趁这个机会把实战中的经验整理出来,希望能帮到同样在 PHP 赛道上深耕的同行。​

一、先搞懂业务:别让 “过度优化” 毁了项目​

刚入行时总觉得 “优化” 就是堆技术,比如上来就想把 MySQL 换成 Redis 缓存全量数据,结果上线后发现业务里有大量实时库存校验,缓存同步延迟直接导致超卖。后来才明白,PHP 开发的核心是 “业务驱动技术”,优化前必须先理清业务逻辑里的 “关键路径”。​

比如这次重构的订单系统,最初的代码里把 “创建订单”“库存扣减”“日志记录”“消息推送” 全揉在一个方法里,代码快 500 行了。先画了张业务流程图,发现 “日志记录” 和 “消息推送” 其实不影响订单创建的核心流程,完全可以用异步处理。​

这里用了 PHP 的Swoole扩展做异步任务队列,把日志和消息推送的逻辑丢到任务里,主流程只处理订单数据入库和库存扣减。改完后单接口的执行时间直接少了 200ms,更重要的是,后续再改日志格式或者消息模板,再也不用动核心订单代码了 —— 这才是优化的意义,不只是快,还要好维护。​

二、数据库优化:别只盯着索引,SQL 写法才是坑​

PHP 开发者打交道最多的就是 MySQL,很多人觉得优化数据库就是加索引,但实际项目里,烂 SQL 比没索引更致命。​

之前遇到一个问题:商品列表接口要筛选 “上架状态”“所属分类”,还要按 “销量倒序”,最初的 SQL 是这么写的:​

// 最初的烂SQL​

看似没问题,但EXPLAIN分析后发现,虽然给status和category_id加了联合索引,但ORDER BY sales导致了 “文件排序”,数据量一多,接口直接超时。​

后来调整了索引和 SQL 写法:​

  1. 把联合索引改成(category_id, status, sales)—— 因为筛选条件是category_id和status,排序是sales,这样索引顺序能覆盖筛选和排序,避免文件排序;​
  1. 避免用SELECT *,只查前端需要的字段,比如id, name, price, sales,减少数据传输量。​

改后的 SQL 和 PHP 代码:​

// 优化后的SQL​

$fields = "id, name, price, sales";​

$sql = "SELECT {$fields} FROM goods WHERE status = 1 AND category_id = ? ORDER BY sales DESC LIMIT 10";​

// 用PDO预处理,避免SQL注入​

$stmt = $pdo->prepare($sql);​

$stmt->execute([$categoryId]);​

$goodsList = $stmt->fetchAll(PDO::FETCH_ASSOC);​

这么一改,接口的数据库查询时间从 300ms 降到了 50ms 以内。另外提醒一句,PHP 里用 PDO 或者 MySQLi 的预处理语句真的很重要,之前见过同事直接拼接变量导致 SQL 注入,最后赔了用户数据还丢了工作,血的教训。​

三、缓存使用:别让缓存变成 “定时炸弹”​

提到 PHP 性能优化,缓存是绕不开的,但缓存用不好,反而会出大问题。​

之前维护的用户中心项目,用 Redis 缓存用户信息,key 是user:info:{uid},过期时间设了 24 小时。结果有次运营后台改了用户的会员等级,用户端却一直显示旧等级,查了半天才发现是缓存没更新。​

后来总结了几个缓存使用的原则:​

  1. 更新数据时同步更新缓存:比如修改用户等级后,先更 MySQL,再用Redis::hMSet更新缓存,或者直接DEL缓存让下次查询重新生成;​
  1. 避免缓存穿透:比如查询不存在的用户 ID,会直接查数据库,这时可以缓存一个空值,设置短过期时间(比如 5 分钟);​
  1. 缓存 key 加前缀区分环境:开发、测试、生产环境的缓存 key 用不同前缀,比如dev:user:info:{uid}和prod:user:info:{uid},避免环境间缓存污染。​

举个 PHP 里的缓存处理示例:​

class UserService {​

public function getUserInfo($uid) {​

$redisKey = 'prod:user:info:' . $uid;​

$redis = Redis::getInstance();​

// 先查缓存​

$userInfo = $redis->hGetAll($redisKey);​

if (!empty($userInfo)) {​

return $userInfo;​

}​

// 缓存没命中,查数据库​

$userInfo = $this->userModel->getUserById($uid);​

if (empty($userInfo)) {​

// 缓存空值,避免穿透​

$redis->hMSet($redisKey, ['empty' => 1]);​

$redis->expire($redisKey, 300); // 5分钟过期​

return [];​

}​

// 缓存用户信息,24小时过期​

$redis->hMSet($redisKey, $userInfo);​

$redis->expire($redisKey, 86400);​

return $userInfo;​

}​

// 修改用户等级时同步更新缓存​

public function updateUserLevel($uid, $level) {​

// 1. 更新数据库​

$this->userModel->updateLevel($uid, $level);​

// 2. 更新缓存​

$redisKey = 'prod:user:info:' . $uid;​

四、代码规范:PHP 不是 “随便写” 的语言​

很多人觉得 PHP 门槛低,写代码可以随便来,但维护过老项目的人都知道,混乱的代码才是最大的性能瓶颈 —— 因为你根本不敢改,更别说优化。​

这几年我一直在项目里推 “PSR 规范”,尤其是 PSR-4 自动加载和 PSR-12 代码风格:​

  1. 用 Composer 管理依赖,避免手动引入文件,比如需要用 Redis 时,直接composer require predis/predis,然后use Predis\Client即可;​
  1. 类和方法的命名统一用 “大驼峰” 和 “小驼峰”,比如UserService类、getUserInfo方法,团队里所有人都按这个来,看代码不用猜意思;​
  1. 控制方法长度,一个方法最多 50 行代码,超过就拆分成子方法,比如之前的订单创建方法,拆成createOrderData(组装数据)、checkStock(库存校验)、sendOrderMsg(消息推送)三个方法,逻辑清晰多了。​

另外,PHP 7 + 的新特性真的要多用,比如类型声明和返回值类型,能在编译阶段发现很多错误:​

// 用类型声明避免传错参数​

public function calculatePrice(int $goodsId, int $num, float $discount): float {​

$goodsPrice = $this->goodsModel->getPrice($goodsId);​

$totalPrice = $goodsPrice * $num * $discount;​

return $totalPrice;​

}​

之前有个同事传了字符串类型的num,结果计算出问题,加了类型声明后,直接报致命错误,当场就能发现问题,不用等到线上排查。​

总结:PHP 开发,稳比花里胡哨更重要​

做 PHP 开发这些年,最大的感受是:技术不是用来炫的,而是用来解决问题的。不用盲目追求新框架、新扩展,把 MySQL、Redis 这些基础工具用透,把业务逻辑理清楚,写出可维护、高性能的代码,才是最核心的竞争力。​

这次项目重构优化,没有用任何 “高大上” 的技术,只是把业务拆清楚、SQL 写规范、缓存用到位,就实现了性能的大幅提升。后续打算再研究下 PHP 8 的 JIT 编译,看看能不能再进一步优化执行效率,有结果了再跟大家分享。​

最后,欢迎同行在评论区交流,你在 PHP 项目优化中遇到过哪些坑?又是怎么解决的?咱们一起踩坑一起成长!​

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐