事务失败调试需在db::listen中实时记录sql、bindings及事务层级,transactionfailed事件中按层级提取;命名绑定需正则替换,事务id应组合连接名、pid与唯一标识,避免日志顺序错乱。

事务失败时只看到 QueryException 或 PDOException,没 SQL、没参数、没堆栈,根本没法修——这不是配置没开,是上下文压根没捕获进去。
DB::listen 捕获不到事务内 SQL?检查 query log 是否启用
默认情况下,DB::listen 能监听所有查询,但前提是查询日志已开启。Laravel 9+ 在 APP_DEBUG=true 时自动启用,但生产环境或自定义连接可能关闭了它。
-
DB::enableQueryLog()必须在事务开始前调用,否则日志为空;DB::flushQueryLog()也得提前清空,避免混入其他请求的语句 - 监听器里不能只依赖
$event->sql,要主动检查$event->connection->isTransactionActive()来过滤出事务内执行的语句 - 绑定参数不在
$event对象里直接暴露,需从$event->connection->getQueryLog()中提取最近一条记录的bindings字段
TransactionFailed 事件拿不到完整 SQL?确认 PDO 错误信息来源
Laravel 8.x+ 的 Illuminate\Database\Events\TransactionFailed 事件确实能拿到异常和连接实例,但它不自动包含已执行的 SQL —— 因为事务失败时,最后那条报错 SQL 可能还没被写入 query log(尤其在死锁、超时等连接级错误下)。
- 不要用
$event->connection->getPdo()->errorInfo()代替 SQL 日志:它只返回驱动错误码(如HY000)和消息,不含原始语句 - 正确做法是:在
DB::listen中把每条语句连同bindings和当前transactionLevel()一起存到内存数组,再在TransactionFailed监听器中按事务层级捞出对应批次 - 注意
getRecordedQuery()是 Laravel 10.27+ 新增方法,旧版本必须手动维护查询日志快照
setBindings 后日志仍显示 ? 占位符?别跳过 raw 查询的日志补全
用 DB::raw() 或 selectRaw() 手动拼 SQL 时,DB::listen 拿到的 $event->sql 仍是带 ? 的模板,bindings 是分离的数组。日志里只写 SQL 模板毫无调试价值。
- 必须在监听回调里自己做参数替换:
vsprintf(str_replace('?', '%s', $event->sql), $event->bindings) - 注意
vsprintf对null值会报 warning,建议先用array_map(fn($b) => $b === null ? 'NULL' : $b, $event->bindings)预处理 - 如果用了命名绑定(如
:lat),$event->bindings是关联数组,得用preg_replace_callback配合键名替换,不能硬套vsprintf
日志里事务 ID 总是重复?用 uniqid() 不够,得加连接标识
单纯用 uniqid('txn_') 生成事务 ID,在高并发或队列任务中容易撞车——因为它是基于微秒时间戳,同一微秒内多个请求会拿到相同 ID。
- 应组合连接名 + 进程 ID + 微秒:
sprintf('txn_%s_%d_%s', DB::getDefaultConnection(), getmypid(), uniqid('', true)) - 更稳妥的是在事务开启前,把 ID 存进
Log::withContext(),后续所有Log::info()自动带上该上下文,无需手动传参 - 别忘了在
rollback或commit后清除上下文,否则下一个请求可能继承上一个事务的 ID
最常被忽略的一点:事务失败时,DB::getQueryLog() 返回的数组顺序不等于执行顺序——Laravel 内部用 array_merge 合并多连接日志,且某些异常路径会跳过日志写入。真要还原执行流,必须在 DB::listen 里实时追加带毫秒时间戳的结构化条目,而不是事后捞。











