FrankenPHP环境下Doctrine事务易因超时中断,需同步调大frankenphp.yaml的timeout、MySQL的innodb_lock_wait_timeout,并手动实现带重试与clear()的事务封装。

Doctrine 事务在 FrankenPHP 环境下容易因默认超时设置过短而意外中断,尤其在处理库存扣减、余额更新或批量导入等耗时操作时,TransactionTimedOutException 或 MySQL 的 Lock wait timeout exceeded 错误频繁出现——这不是代码逻辑错误,而是环境与配置未对齐的典型表现。
FrankenPHP 默认请求超时会强制终止 Doctrine 事务
FrankenPHP 的 max_execution_time(默认 30s)和底层 SAPI 的连接生命周期,会直接掐断正在执行的 PHP 进程,导致 Doctrine 事务无法正常提交或回滚。即使你在 PHP 层用 set_time_limit(0),FrankenPHP 仍可能在 HTTP 层关闭连接。
- 检查当前超时:运行
php -i | grep max_execution_time,但注意该值对 FrankenPHP 的 HTTP 请求不完全生效 - 关键配置在
frankenphp.yaml中:timeout: 120(单位秒),必须显式增大 - 同时需同步调整 MySQL 的
innodb_lock_wait_timeout(建议设为120),否则 Doctrine 提交前卡在行锁等待,就会先触发数据库层超时 - 不要依赖
@Transactional注解自动管理超时——它不控制底层连接存活时间
Doctrine transactional() 内部不重试,需手动封装防并发失败
当多个请求并发修改同一行(如优惠券核销),仅靠 EntityManager::transactional() 无法解决竞态;它只保证原子性,不处理 Deadlock found when trying to get lock 或锁等待超时后的重试逻辑。
- 必须在外层加循环 + 捕获异常:
OptimisticLockException、TransactionRequiredException、PDOException中含Lock wait timeout的情况 - 每次重试前调用
$entityManager->clear(),避免旧实体状态污染新事务 - 限制最大重试次数(如 3 次),防止雪崩;超过则抛出业务异常(如
CouponAlreadyUsed) - 示例片段:
$attempts = 0; do { try { return $entityManager->transactional(function (EntityManagerInterface $em) use ($coupon) { $em->refresh($coupon); // 强制读最新版本 if (!$coupon->canBeUsed()) { throw new \RuntimeException('Coupon invalid'); } $coupon->use(); }); } catch (\PDOException $e) { if (str_contains($e->getMessage(), 'Lock wait timeout') && ++$attempts < 3) { usleep(50000); // 50ms 后重试 continue; } throw $e; } } while (false);
FrankenPHP 下长事务需禁用响应流式发送
FrankenPHP 默认启用 HTTP 流式响应(streaming),但 Doctrine 长事务期间若提前 flush 输出(如日志、progress bar),会导致连接被判定为“已响应”,后续事务提交失败时无法正确返回错误状态码。
立即学习“PHP免费学习笔记(深入)”;
- 在事务关键路径中,确保
ob_end_clean()或禁用所有输出缓冲,避免echo/var_dump干扰 - 控制器中显式关闭 streaming:
response->headers->set('X-Accel-Buffering', 'no')(Nginx 兼容),或 FrankenPHP 原生方式:frankenphp_set_response_streaming(false) - 日志写入改用异步通道(如 Monolog 的
Handler\WhatFailureGroupHandler+ Redis 队列),而非同步 file handler - 事务内禁止调用任何可能触发 Symfony EventDispatcher 的非幂等操作(如发邮件、调第三方 API),它们会延长事务窗口并放大超时风险
真正棘手的不是超时数字本身,而是事务边界是否干净——FrankenPHP 的进程模型比传统 FPM 更轻量,但也更“脆弱”:一次未捕获的异常、一个忘记 clear() 的 EntityManager、甚至一条调试 dump(),都可能让事务卡在半提交状态,最终拖垮整个连接池。



















