Hyperf定时任务子进程中事务不提交,是因事务上下文在协程切换或进程生命周期中丢失;需显式指定连接、预热连接池、使用DB::transaction匿名函数、避免协程脱离事务块,并检查数据库配置与模型事务调用方式。

Hyperf 定时任务子进程里事务不提交,不是“事务没起作用”,而是事务上下文在协程切换或进程生命周期中丢失了——子进程启动后若未显式开启事务、或事务对象未绑定到当前协程上下文,DB::transaction() 里的 SQL 执行完根本不会触发 commit,甚至不报错。
确认事务是否真在子进程中生效
定时任务默认由 CrontabProcess 启动,它本身是独立的 long-running 进程,不共享 HTTP 请求的协程上下文。这意味着:
-
DB::transaction()调用时,若未指定连接名或未强制使用主连接池,可能走的是默认连接(而该连接在子进程中未初始化); - 协程内执行的事务,若未等
commit()完成就结束协程(比如go()启动后直接 return),事务会静默丢弃; - 使用模型操作时,若模型配置了
connection => 'default',但default连接池在子进程中未被预热,事务会降级为自动提交模式(即每条 SQL 立即生效,无法回滚)。
确保事务在 Crontab 子进程中正确提交
- 显式指定连接并预热:在任务 handle 方法开头加
DB::connection('default')->reconnectIfMissing();,避免连接空闲超时导致事务挂起; - 不依赖隐式事务,改用
DB::transaction(function ($db) { ... }, $timeout)匿名函数方式,框架会保证 commit/rollback 自动触发; - 避免在事务块内调用
go()或defer(),协程脱离当前上下文后,事务状态无法追踪; - 若需跨协程协作(如并发查库存再统一扣减),改用
Channel或Atomic+ 主键乐观锁,别把事务拖进异步协程。
检查数据库驱动与连接池配置是否支持子进程事务
- 确认
config/autoload/databases.php中default连接的'driver' => 'mysql',且'pool' => [...]里max_idle_time小于 MySQL 的wait_timeout(建议设为wait_timeout - 30),否则事务中途连接断开,commit 失败但无提示; -
pool.min_connections至少设为2,防止冷启时第一个事务因连接池为空而 fallback 到非事务模式; - 关闭
options => ['strict' => false]类似宽松配置,它可能导致INSERT IGNORE或ON DUPLICATE KEY UPDATE绕过事务控制。
验证事务是否真正提交的简单方法
在事务块末尾加一行日志:
DB::transaction(function () {
DB::table('orders')->insert(['status' => 'pending']);
logger()->info('Transaction executed');
});
logger()->info('Transaction finished'); // 这行必须在事务块外,且能打印出来
如果只看到第二行日志,说明事务块根本没执行(比如被异常提前退出);如果两行都有但数据库无数据,大概率是连接未绑定或 commit 被跳过。
特别注意定时任务中的模型事务陷阱
Hyperf 模型默认不启用事务,即使你在控制器里写了 DB::transaction(),模型的 save()、update() 仍走自动提交。必须显式传入连接:
$order = Order::on('default')->where('id', $id)->lockForUpdate()->first();
DB::transaction(function () use ($order) {
$order->status = 'paid';
$order->save(); // 此时才真正走事务
});
或者直接用 QueryBuilder:DB::table('orders')->where(...)->update([...]),更可控。
不复杂但容易忽略。











