ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PHP 性能优化实战:从 3s 到 300ms,TaoToken 配置与接口调优全记录

PHP 性能优化实战:从 3s 到 300ms,TaoToken 配置与接口调优全记录 1. 一个后台列表接口为什么能慢到 3 秒订单列表接口PHP 8.1 Laravel 10 MySQL 8订单表 200 万行关联用户表和商品表。功能上就是分页查订单、带出用户昵称和商品标题、再算两个统计数字。听起来没什么难度但上线后平均响应 3 秒起步QPS 不到 10数据库 CPU 时不时飙到 80% 以上。这种接口的慢通常不是 PHP 本身慢而是「一次请求里干了太多重复的活」。我先用 Laravel Debugbar 挂上去看一眼就看到 SQL 执行时间 2.5 秒、SQL 条数 41 条、内存峰值 120MB。41 条 SQL 里1 条查订单20 条查用户20 条查商品——典型的 N1。所以这篇的路线是先定位再动手先数据库再 PHP先干掉 N1再补索引和缓存最后用 OPcache 和响应裁剪收尾。整个过程我会把可复制的配置、命令、代码都贴出来你照着改自己的项目就行。中途排查 SQL 和读慢日志时我也会用 TaoToken 统一走 AI 辅助分析省得在多个平台之间来回切 Key。2. 前置准备TaoToken 统一 Key 与 API 通道优化过程中经常要做几件事让 AI 帮忙读 EXPLAIN 结果、解释慢查询日志、生成索引建议、review 一段 Laravel 查询代码。如果每个工具都单独配 Key管理起来很烦。我的做法是用 TaoToken 做统一入口一个 Key 走所有模型调用。TaoToken 的定位是统一的模型 API 通道兼容 OpenAI 风格的接口格式所以 Laravel 里用 Guzzle 或 Http 门面直接发请求就行不需要额外装 SDK。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 。你需要先拿到 Key进控制台创建 API Key地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。创建完复制出来放到 Laravel 的.env里别硬编码进代码。# .env TAOTOKEN_API_KEYsk-你的key TAOTOKEN_BASE_URLhttps://taotoken.net/api如果你只是想先验证模型通不通可以直接用模型对话页面试一条 prompt地址是 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。把 EXPLAIN 的输出粘进去让它判断 type 和 rows比人肉看快很多。注意Key 只放服务端环境变量不要提交到 Git也不要在前端代码里出现。生产环境和本地环境建议用不同的 Key方便单独限额和排查。3. 可复制配置OPcache、慢日志与 Laravel 查询改造3.1 开启 OPcache先把 PHP 自身的开销压下去生产环境不开 OPcache等于每次请求都重新编译一遍 PHP 文件。改php.ini; php.ini 生产环境配置 opcache.enable1 opcache.enable_cli1 opcache.memory_consumption128 opcache.interned_strings_buffer8 opcache.max_accelerated_files4000 opcache.validate_timestamps0 opcache.revalidate_freq0 opcache.save_comments1validate_timestamps0意味着 PHP 不再每次检查文件是否被修改性能最好但代价是改代码后必须 reload PHP-FPM。部署流程里加一步systemctl reload php-fpm就行。实测这一项能让 PHP 执行时间下降 30% 左右。改完用php -i | grep opcache确认生效或者写个opcache_get_status()的临时脚本看一眼命中率。3.2 打开 MySQL 慢查询日志别靠猜在 MySQL 配置文件里加[mysqld] slow_query_log1 slow_query_log_file/var/log/mysql/slow.log long_query_time0.5 log_queries_not_using_indexes1重启 MySQL 后用mysqldumpslow聚合分析mysqldumpslow -s t -t 10 /var/log/mysql/slow.log-s t按总耗时排序-t 10取前 10 条。你会很快看到哪条 SQL 是大头。我这边排第一的就是订单列表那条带status和created_at条件的查询。3.3 干掉 N1Eager Loading问题代码长这样$orders Order::paginate(20); foreach ($orders as $order) { echo $order-user-name; echo $order-product-title; }改成$orders Order::with([user:id,name, product:id,title]) -select([id, order_no, user_id, product_id, status, created_at]) -paginate(20);with里用user:id,name这种写法只查需要的列减少数据传输。SQL 从 41 条降到 3 条时间从 2.5 秒掉到 600ms 左右。3.4 索引重建复合索引 EXPLAIN 验证原查询select * from orders where status 1 and created_at 2024-01-01 order by created_at desc limit 20 offset 0;EXPLAIN显示type: ALL、rows: 2000000、Using filesort。加复合索引ALTER TABLE orders ADD INDEX idx_status_created_at (status, created_at);再EXPLAIN变成type: range、rows: 20、filesort 消失。SQL 时间从 600ms 降到 150ms。索引顺序很重要等值条件status放前面范围条件created_at放后面这样 MySQL 才能用上索引的有序性。3.5 深度分页换成游标分页limit 20 offset 100000这种写法MySQL 还是要扫前 10 万行。后台列表 99% 的场景不需要跳页直接换游标分页$orders Order::with([user:id,name, product:id,title]) -where(status, 1) -cursorPaginate(20);游标分页用where id last_id代替 offset深度分页从 1 秒以上稳定到 100ms 内。注意游标分页不支持随机跳页前端要改成「上一页/下一页」。3.6 缓存分层只缓存变化少的数据商品信息和统计数据适合缓存分页结果不要缓存容易脏。用 Laravel 的Cache::remember$product Cache::remember( product:{$order-product_id}, 600, fn () Product::select(id, title)-find($order-product_id) );缓存 Key 要带业务前缀更新商品时主动Cache::forget(product:{$id})。这一层加上后接口从 150ms 降到 80ms 左右。3.7 用 TaoToken 辅助读 EXPLAIN 和慢日志排查阶段我会把EXPLAIN的 JSON 输出和慢日志片段发给模型让它帮我判断索引是否命中、有没有隐式类型转换。用 Laravel 的 Http 门面调 TaoTokenuse Illuminate\Support\Facades\Http; $response Http::withToken(config(services.taotoken.key)) -post(config(services.taotoken.base_url) . /v1/chat/completions, [ model claude-sonnet-4-20250514, messages [ [role user, content 分析这条 EXPLAIN 结果指出索引问题\n . $explainJson], ], ]); $advice $response-json(choices.0.message.content);config/services.php里加taotoken [ key env(TAOTOKEN_API_KEY), base_url env(TAOTOKEN_BASE_URL, https://taotoken.net/api), ],这样排查时不用来回切平台一个 Key 就能把 EXPLAIN、慢日志、代码片段都丢进去分析。如果你长期做这类编码和 Agent 任务可以看下 Coding Plan地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 按额度走比单次调用更省心。4. 验证请求压测前后对比与成功结果改完之后不能靠感觉要用数据说话。我用ab和wrk各压一轮接口是GET /api/orders?status1page1。# 优化前 ab -n 200 -c 20 http://your-app.test/api/orders?status1 # 优化后 ab -n 200 -c 20 http://your-app.test/api/orders?status1优化前的结果平均响应 3000ms 以上QPS 不到 10失败请求若干。优化后平均响应 300ms 左右QPS 80无失败。再用 Laravel Debugbar 确认 SQL 条数和内存指标优化前优化后平均响应3000ms300msSQL 条数413内存峰值120MB35MBQPS1080响应体积也从 200KB 降到 30KB因为select只取了需要的列API Resource 里也精简了字段。网络传输时间在弱网环境下差别很明显。验证 TaoToken 通道是否通可以单独发一条请求curl -s https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d {model:claude-sonnet-4-20250514,messages:[{role:user,content:ping}]}返回里有choices字段就说明通道正常。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 参数细节可以对着看。5. 本篇常见错排查OPcache 没生效先确认php -i | grep opcache.enable是 On再看 PHP-FPM 是否 reload 过。validate_timestamps0时改代码不 reload 是不会生效的这是最容易踩的坑。索引加了但没走用EXPLAIN看key字段是不是你新建的索引。如果status是字符串类型而查询传了整数会触发隐式转换导致索引失效。字段类型和查询参数类型要一致。游标分页报错cursorPaginate要求排序字段唯一默认用主键。如果你的查询里有自定义orderBy要确保排序字段组合唯一否则分页会丢数据或重复。缓存脏数据更新商品后忘了Cache::forget列表里还是旧标题。建议把缓存的写入和清除封装到模型事件里别散落在业务代码各处。TaoToken 返回 401检查.env里的 Key 有没有多余空格config:cache之后要重新php artisan config:clear。Base URL 结尾不要带/v1代码里已经拼了。慢日志没记录long_query_time0.5表示超过 0.5 秒才记如果你的慢查询是 0.3 秒就不会出现。排查阶段可以临时调到 0.1定位完再调回去。6. 把 AI 排查接进你的优化流程这套优化下来真正花时间的不是改代码而是定位问题。N1 靠 Debugbar 一眼能看出来但 EXPLAIN 的type、rows、filtered这些字段以及慢日志里一堆相似 SQL 的聚合判断有 AI 辅助会快很多。我的做法是本地用 Debugbar 和 EXPLAIN 拿到原始数据把结果通过 TaoToken 发给模型让它给出索引建议和改写方案再自己验证。Key 在控制台创建 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 接入方式看文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。如果你用 Claude Code 做编码辅助Anthropic 兼容通道的说明在 https://taotoken.net/claude-code?utm_sourcetaotoken_aicg_blog_endutm_contentclaude-codeutm_campaignrewrite 。最后留一个我自己的检查顺序先看 SQL 条数再看 EXPLAIN 的 type再看有没有 filesort最后才看 PHP 层。顺序反了容易在 OPcache 上折腾半天结果发现大头是数据库。
返回列表