Hyperf定时任务中事务未回滚的根本原因是应用未主动管理事务和协程上下文,MySQL不自动回滚悬挂事务;必须手动用try-catch+显式rollback、信号标志控制、避免阻塞调用,并通过MySQL日志和INNODB_TRX验证回滚效果。

Hyperf 中定时任务进程收到 SIGTERM/SIGINT 信号后,若正在执行数据库事务却未回滚,根本原因不是信号没捕获,而是事务未被主动管理或协程上下文未正确清理——Hyperf 不会自动回滚未提交的事务,哪怕进程优雅退出。
信号中断时事务不会自动回滚
Hyperf 的 OnShutdown 事件只在 Worker 空闲或刚完成请求时触发,而定时任务(如自定义 Process)通常长期运行、不走 HTTP 生命周期,OnShutdown 默认不监听它。更关键的是:MySQL 事务生命周期完全由应用控制,Swoole 或 Hyperf 不会在进程退出时帮你执行 ROLLBACK —— 即使你用了 DB::transaction(),只要没显式提交或异常退出,连接断开后 MySQL 会自动回滚,但这个“自动”依赖连接正常关闭;若进程被 kill -9、OOM 或协程卡死,连接可能直接丢弃,事务状态变为悬挂,甚至引发锁等待或死锁残留。
定时任务中必须手动保障事务完整性
- 别依赖“连接断开即回滚”:生产环境网络抖动、Redis/DB 临时不可用都可能导致连接异常中断,此时事务未提交也未回滚,数据处于不确定状态
-
每个定时任务逻辑应包裹在 try-catch + 显式 rollback 中:尤其在
handle()或run()方法里,避免使用裸DB::beginTransaction(),优先用闭包式事务(自动 rollback) -
对长周期事务加超时与上下文感知:在事务开始前设置
Context::set('task.stopping', false),并在信号处理器中设为true;循环内定期检查该标志,若为true则主动DB::rollBack()并退出 -
禁止在事务中混用阻塞调用:如
sleep()、file_get_contents()、同步 Redisexec()等,否则协程无法调度,信号无法及时响应,事务卡住无法退出
让定时任务感知并响应信号
Hyperf 默认只对 Worker 进程注册 WorkerStopHandler,自定义定时任务进程(如继承 AbstractProcess)需自行注册信号处理器,并配合 Context 控制流程:
- 在
process()方法开头调用pcntl_signal(SIGTERM, fn() => Context::set('stopping', true)),并确保declare(ticks=1)或周期性调用pcntl_signal_dispatch() - 在事务执行前检查
Context::get('stopping', false),为true则跳过执行或立即回滚 - 若使用
ProcessStopHandler,需将其加入config/autoload/signal.php的handlers数组,并确认其timeout足够覆盖单次事务最长耗时(建议 ≥ 2× 预估最大执行时间) - 避免在信号 handler 里做 DB 操作 —— 只设标志、记日志,所有清理逻辑放在主循环中判断执行
验证事务是否真被回滚
光看日志不够,要从 MySQL 层确认:
- 启用
innodb_print_all_deadlocks = ON和log_error_verbosity = 3,复现中断场景后查 error log,确认是否有未提交事务残留锁 - 执行
SELECT * FROM information_schema.INNODB_TRX WHERE trx_state = 'RUNNING',观察中断前后活跃事务数量变化 - 在事务内写入一条中间状态记录(如
status = 'processing'),中断后查该记录是否被 rollback 或仍为 processing —— 若存在且未更新,说明事务未结束


















