面试官抛出“ThinkPHP 如何支撑高并发”这个问题时,很多候选人的第一反应是“上 Swoole”。但真正能拿到高薪 offer 的人,会从协程调度、连接池管理、框架生命周期三个层面拆解问题。这篇文章就围绕这三个关键词,把面试中最容易踩坑的知识点讲透。
一、先搞清楚:ThinkPHP 的两种运行模式
ThinkPHP 从 6.0 开始正式支持 Swoole 和 Workerman,但很多人没意识到框架其实有两种截然不同的生命周期:
- 传统 PHP-FPM 模式:每次请求都经历“加载框架 → 连接数据库 → 处理业务 → 销毁一切”的完整过程。优点是隔离性好,缺点是连接无法复用,框架启动开销大。
- 常驻内存模式(Swoole/Workerman):框架只加载一次,请求以协程或进程为单位复用应用实例。优点是性能提升显著,缺点是状态污染和连接管理成为必须解决的问题。
面试中如果只答“用 Swoole 就快了”,基本会被判定为停留在表面。真正的考点在于:常驻内存之后,数据库连接、Redis 连接、全局变量该怎么处理?
二、协程:不是多线程,别搞混了
Swoole 的协程是用户态轻量级线程,由 Swoole 底层在 IO 等待时自动切换,而不是靠操作系统调度。这一点在面试中经常被追问。
协程的核心特征
- 单线程内并发:一个 Worker 进程内可以同时运行成百上千个协程,但它们共享同一个进程内存空间。
- 遇到 IO 自动让出:执行到 MySQL 查询、Redis 请求、文件读写等操作时,协程挂起,CPU 去执行其他协程。
- 切换成本极低:一次协程切换大约几十纳秒,而线程切换在微秒级别。
ThinkPHP 中协程的典型陷阱
// 错误示范:协程间共享了可变状态
class UserService
{
protected $userData = [];
public function handle($id)
{
$this->userData = Db::name('user')->find($id);
// 协程 A 执行到这里挂起,协程 B 修改了 $this->userData
return $this->userData;
}
}
在 FPM 模式下这段代码没问题,因为每个请求都是独立的进程。但在 Swoole 协程模式下,$this->userData 是进程级共享的,协程 A 挂起期间协程 B 可能覆写它,导致数据错乱。
正确做法:使用协程上下文(Context)隔离请求级数据,或者避免在服务类中保存可变状态。
use Swoole\Coroutine;
Context::set('user_data', Db::name('user')->find($id));
$data = Context::get('user_data');
三、连接池:高并发的命脉
这是面试中最能拉开差距的部分。很多人知道“要用连接池”,但说不清楚为什么需要、怎么实现、ThinkPHP 里怎么配。
为什么需要连接池?
在 FPM 模式下,每次请求新建 MySQL 连接,TCP 三次握手 + 权限认证大约消耗 1-3ms。QPS 上千时,光建连就吃掉大量 CPU。连接池的核心价值是复用已建立的连接,把建连开销摊薄到接近零。
连接池的工作原理
一个典型的连接池包含三个要素:
- 空闲队列:存放当前未被使用的连接
- 最大连接数:防止连接过多拖垮数据库
- 获取/归还逻辑:协程取连接时若队列为空则等待,用完必须归还
ThinkPHP + Swoole 的连接池配置
ThinkPHP 6.0 的 think-swoole 扩展内置了连接池支持,配置位于 config/swoole.php:
return [
'pool' => [
'db' => [
'max_active' => 30, // 最大活跃连接数
'max_wait_time' => 5, // 获取连接的最大等待时间(秒)
],
'cache' => [
'max_active' => 20,
'max_wait_time' => 3,
],
],
];
面试高频追问:连接池设多大合适?
参考答案:不是越大越好。MySQL 默认 max_connections 是 151,如果开 8 个 Worker,每个 Worker 池设 30,总连接数就是 240,直接超过数据库上限。合理公式是:Worker 数 × 单池大小 < 数据库最大连接数 × 0.8。同时要结合业务 IO 密集程度调整,IO 等待长的业务可以适当调大。
连接归还的坑
最容易被忽略的是连接泄漏。如果协程执行中抛出异常,连接没有归还到池里,池子很快就会被耗尽。ThinkPHP 的 Swoole 扩展通过 defer 机制自动归还,但如果你手动操作 PDO 或原生 Redis,必须自己保证 try...finally 归还。
四、高并发实践:从配置到代码
把协程和连接池串起来,高并发场景下的完整优化路径如下:
1. 框架层配置
// config/swoole.php
return [
'server' => [
'worker_num' => swoole_cpu_num() * 2, // CPU 核数的 2 倍
'max_request' => 10000, // 防止内存泄漏累积
'enable_coroutine' => true,
],
];
max_request 是个关键参数:Worker 处理满 10000 个请求后自动重启,避免常驻内存导致的内存膨胀。
2. 中间件中避免阻塞操作
协程模式下,任何同步阻塞操作(如 sleep()、大文件同步读写)都会卡住整个 Worker 进程内的所有协程。必须改用 Swoole 提供的协程化 API:
// 阻塞写法,会卡住整个进程
sleep(1);
// 协程写法,只挂起当前协程
Swoole\Coroutine::sleep(1);
3. 数据库查询优化
- 使用
Db::name('user')->cache(true, 60)减少重复查询 - 避免在循环中查数据库,改用
whereIn批量查询 - 大结果集使用
chunk分块处理,防止单次查询占用连接过久
4. Redis 连接池与管道
高并发下 Redis 往往是瓶颈。除了连接池,还应使用 Pipeline 批量执行命令,减少网络往返:
$redis = app('cache')->store('redis')->handler();
$pipeline = $redis->pipeline();
foreach ($keys as $key) {
$pipeline->get($key);
}
$results = $pipeline->exec();
五、面试答题框架
如果面试官问“ThinkPHP 如何支撑高并发”,可以按这个结构回答:
- 运行模式:从 FPM 切换到 Swoole 常驻内存,消除框架启动开销
- 协程调度:利用协程在 IO 等待时切换,提升单进程并发能力
- 连接池:复用数据库和 Redis 连接,控制最大连接数保护后端
- 代码约束:避免全局可变状态、避免阻塞调用、保证连接归还
- 监控调优:根据 QPS、连接池等待时间、Worker 内存占用持续调整参数
总结
ThinkPHP 的高并发能力不是靠一个开关实现的,而是运行模式 + 协程 + 连接池 + 代码规范四者协同的结果。面试中最容易失分的点,是只记住了“用 Swoole”,却说不清协程切换时机、连接池大小计算、以及常驻内存下的状态污染问题。把这三点讲清楚,你就超过了大多数候选人。
未经允许不得转载:任鹏个人博客 » ThinkPHP 面试精讲:协程、连接池与高并发实践

