根本原因是frankenphp timeout、mysql innodb_lock_wait_timeout及pdo连接超时三者未对齐,须统一设为120秒并同步配置;doctrine transactional()不支持重试,需手动封装带clear()与退避机制的事务逻辑。

FrankenPHP 请求超时直接中断事务,不是代码问题
高峰期大量超时,根本原因不是 PHP 逻辑慢,而是 FrankenPHP 的 timeout 配置和 MySQL 的锁等待超时没对齐。一旦请求在 FrankenPHP 层被强制终止,Doctrine 事务就卡在“已开始未提交”状态,后续请求立刻撞上 Lock wait timeout exceeded 或 TransactionTimedOutException——这是典型的配置级雪崩,不是 bug。
必须同步调大三个地方的 timeout 值
只改其中一个,等于白调。三者必须一致(建议统一设为 120):
-
frankenphp.yaml或 Caddyfile 中的timeout: 120(单位秒),这是 HTTP 请求生命周期上限 - MySQL 的
innodb_lock_wait_timeout = 120(需执行SET GLOBAL innodb_lock_wait_timeout = 120并写入 my.cnf) - PHP 层 Doctrine 执行前显式设置连接超时:
$connection->getWrappedConnection()->setTimeout(120)(仅对 PDO 生效)
注意:set_time_limit(0) 在 FrankenPHP 下无效,它不控制 SAPI 层连接存活时间。
事务失败不能只靠 transactional(),要手动重试 + clear()
EntityManagerInterface::transactional() 只保证单次原子性,不处理并发锁冲突后的恢复。高峰期高并发修改同一行(如库存、优惠券)时,必须自己封装重试逻辑:
- 捕获
PDOException中含Lock wait timeout或Deadlock found when trying to get lock的消息 - 每次重试前调用
$entityManager->clear(),否则旧实体状态会污染新事务 - 限制最大重试次数(如
3次),超限后抛出业务异常(如CouponAlreadyUsed),避免线程池被占满 - 重试间隔建议
usleep(50000)(50ms),避免密集重试加剧锁竞争
线程池撑不住时,别硬扛,先看 max_wait_time
高峰期请求排队等不到空闲 PHP 线程,就会在 FrankenPHP 层直接返回 503。这不是超时,是线程耗尽。此时应检查:
- Caddyfile 中是否配置了
max_wait_time 10s(默认禁用),没有就加一条 - 确认
num_threads和max_threads是否合理:8 核机器建议num_threads 12+max_threads auto,而非盲目堆到 32 - 用
frankenphp --stats实时观察busy_workers和queued_requests,若后者持续 > 0,说明线程数或max_wait_time已成瓶颈
真正难调的不是数值本身,而是三处 timeout 的耦合关系——MySQL 锁等太久,FrankenPHP 先杀进程;FrankenPHP 等太久,Nginx 又先断连;一层断,全链崩。必须当成一个整体来压测,而不是单独调某个参数。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











