ThinkPHP8路由缓存与性能优化实战:从200ms到30ms的调优记录

2026-08-30 0 481

项目上线了一个多月,用户量上来之后发现首页接口的平均响应时间到了200ms,偶尔能飙到400ms。用火焰图一看,大部分时间没有花在数据库查询上,反而让路由解析占了不少。当时挺意外的,ThinkPHP8的路由解析已经很成熟了,但仍然有优化空间。后来把路由缓存打开,再顺手优化了两处查询,平均响应直接降到30ms左右。这篇文章把这个过程写出来,包含具体的配置和代码改动,你可以直接参考。

先聊聊路由解析为什么会慢

ThinkPHP8的路由解析分两步:第一步是从URL中找到对应的路由规则,第二步是根据路由规则绑定到控制器和方法。如果项目里定义了大量的路由(尤其是用了路由分组、资源路由、动态参数),每次请求都需要去匹配这些规则。默认情况下规则是从文件里加载,然后逐个进行正则匹配。小项目感觉不到,路由几百条之后,这部分开销就上来了。

另外,路由加载还涉及闭包处理,如果某个路由定义里用了回调函数,那这个闭包也没法被缓存。所以想要用路由缓存,最好全部使用字符串声明控制器和方法,不要使用闭包。

开启路由缓存前的准备

ThinkPHP8自带了一个命令:php think route:cache。它会编译生成路由缓存文件,下次请求直接加载编译后的数据,省去正则匹配的过程。但如果你在路由定义文件里用了Route::get之类的闭包,缓存会直接报错。所以先检查一下你项目里有没有类似写法:

// 这种写法没法缓存
Route::get('hello/:name', function($name){
    return 'Hello '.$name;
});

// 改成控制器方式
Route::get('hello/:name', 'index/Hello/read');

如果你的项目里大量使用了闭包,那建议还是先别开路由缓存。硬开的话,ThinkPHP会提示你“路由无法缓存”,不会帮你生成。

我的项目是怎么写的路由

我接手这个项目的时候,所有接口都写在了一个route/route.php文件里,大概有一百多个路由,用的是分组定义,像这样:

use thinkfacadeRoute;

Route::group('api/v1', function () {
    Route::get('product/:id', 'api.v1.Product/detail');
    Route::post('order/create', 'api.v1.Order/create');
    // 后面还有几十条
})->middleware([appmiddlewareAuthCheck::class]);

这种分组写法OK,里面没有用闭包,所以可以直接生成路由缓存。我直接执行:

php think route:cache

它会提示Route cache created successfully。然后我刷新页面测试,接口响应时间确实下来了一些,从200ms降到了100ms左右。但其实没有达到我预期的30ms,于是继续排查。

路由缓存不是银弹,还得补充查询优化

后来发现,路由解析时间降下来了,但数据库查询时间仍然占大头。尤其是首页这个接口,要查一个商品列表,每个商品又要查对应的SKU、品牌、评价数量,等于是一条主查询外加N条关联查询。我反手用select()查了所有商品,然后在循环里去查SKU。循环里查库这种事情,在流量小的时候无所谓,稍微有点并发就会爆炸。

我先用ThinkPHP的关联预载入解决了循环查询问题。比如商品模型里定义好了关联SKU和评价,这样写:

// 原来的代码 - 循环查库
$products = Product::select();
foreach ($products as $product) {
    $skus = $product->skus->cache(10)->select();
}

// 改成预载入
$products = Product::with(['skus', 'brand'])->select();

就这么一个改动,接口响应时间从100ms直接降到了50ms。看起来预载入把N+1问题解决了。但我还是觉得有点慢,继续看。

一个更隐蔽的坑:路由缓存可能触发额外解析

有次我修改了某个中间件,然后清路由缓存重新生成,却发现有的路由访问后报错,说找不到路由。排查了半天发现是路由分组用了可变参数,然后又在另一个地方定义了相同模式的路由。路由缓存会严格匹配规则,但如果你在运行时修改了路由定义(比如在代码里动态添加路由),那么缓存反而会失效。这种情况很少见,但在二开项目里容易发生。解决办法就是:不要动态生成路由,不要用Route::alias这种花活。老老实实把全部路由写在routes目录下。

另外,ThinkPHP8的route:cache会缓存所有路由定义,包括中间件的配置。如果你的某个路由使用了很重的中间件,缓存不会优化掉中间件本身的开销。所以我顺便把中间件里的重复逻辑优化了一下——原来每个请求的表单验证、签名验证什么的,可以提前到路由解析之前做吗?不行,中间件就是设计在路由之后执行的。但你可以把一些纯静态的验证换成内存缓存,比如签名密钥直接写成常量,不要每次读取配置。

最后把Redis缓存加上,才到30ms

路由缓存虽然有用,但真正让接口快起来的还是数据缓存。首页商品列表几乎一小时才更新一次,完全可以放到Redis里。我用了ThinkPHP8的缓存门面,给接口加了一个简单的时间缓存:

public function index()
{
    $cacheKey = 'home_product_list';
    $data = cache($cacheKey);

    if (!$data) {
        $products = Product::with(['skus', 'brand'])->select();
        $data = json_encode($products);
        cache($cacheKey, $data, 3600);
    }

    return json(json_decode($data));
}

这样一来,接口第一次查库,后面直接读Redis,响应时间稳定在30ms以下。注意,我这里用json_decode后再返回,避免数据格式有问题。如果你用json()返回一个字符串,客户端拿到的是JSON字符串的转义,体验不好。

你以为结束了?其实还有一个坑。因为路由缓存是生成到runtime目录下的,如果你更新了路由定义但没有清缓存,旧缓存会一直生效。所以部署脚本里我加了一行:

php think route:cache

每次发版都重新生成一次,避免出现新旧路由不匹配的问题。

一些可以复述的经验

  • 能用字符串控制器,绝不用闭包。这不仅是为了路由缓存,也是为了代码可维护。
  • 路由分组比单条路由更好缓存。分组内的规则可以整体优化,TP8官方手册也推荐路由分组。
  • 检查你是否开了debug模式。开发环境开了debug,路由缓存是自动无效的,只有部署环境才会启用。
  • php think route:list检查路由列表。它会把当前路由规则全部打印出来,如果太多重复,缓存效果就会打折。
  • 数据库预载入+Redis缓存才是王道。路由缓存只是减少基础解析时间,数据层优化才是大头。

简单的性能对比

我在本地模拟了1000次请求,平均耗时大概是这样:

未开路由缓存 + N+1查询:207ms
开路由缓存 + N+1查询:125ms
开路由缓存 + 预载入:58ms
开路由缓存 + 预载入 + Redis:27ms

从表格可以看到,路由缓存能带来的收益大约是40%左右,而数据层的优化能把时间缩到原来的五分之一。如果你正在为接口慢苦恼,建议先打开路由缓存,然后再去优化查询,最后加Redis。顺序不要搞反了。

写在最后

这次调优给了我一个教训:框架提供的功能要搞清楚适用场景,不要为了用而用。路由缓存确实有效,但前提是你得遵从其限制。而且如果代码里大量使用了闭包,开启前一定要权衡。同时,性能优化的重点还是要放在数据库和缓存设计上,这才是成本最低收益最高的部分。

以上是我项目里的实际调优过程,所有数据都是本地压测得来的,不保证一定跟你的一样,但思路是通用的。欢迎交流,也欢迎指出错误。这篇文章没有别的意思,就是记录一下干活过程。

ThinkPHP8路由缓存与性能优化实战:从200ms到30ms的调优记录
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 thinkphp ThinkPHP8路由缓存与性能优化实战:从200ms到30ms的调优记录 https://www.taomawang.com/server/thinkphp/2672.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务