事务未回滚主因是pdo异常模式未启用,导致sql错误不抛异常;需配置pdo::attr_errmode => pdo::errmode_exception,用db::listen或手动事务+唯一id日志追踪全流程,并确认表引擎为innodb。

事务报错却没回滚,通常不是 Laravel 不工作,而是条件没满足。核心就一条:只有未被捕获的异常,才会触发自动回滚;SQL 错误本身不会“自动被发现”,它得先变成 PHP 异常。
确认 PDO 异常模式已启用
这是最常被忽略的前提。如果数据库配置里没打开异常模式,哪怕 SQL 执行失败(比如外键冲突、字段超长),PDO 也只会静默返回 false,事务继续运行,最终 commit —— 数据就“半写入”了。
- 检查 config/database.php 中对应数据库连接的 options 配置,必须包含:
'options' => [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] - 在 .env 中确认
DB_CONNECTION指向的是你正在调试的那个库(尤其是多库项目) - 若使用多库,
DB::transaction()默认操作默认连接;要指定库,必须写成DB::connection('xxx')->transaction(...)
用 DB::listen 查看事务全过程
它能捕获每条 SQL,包括 BEGIN、COMMIT、ROLLBACK,帮你确认事务是否真正开启、何时中断、有没有意外提交。
- 在 AppServiceProvider@boot 中添加监听:
DB::listen(function ($query) {<br> if (str_contains(strtolower($query->sql), ['begin', 'commit', 'rollback'])) {<br> Log::channel('debug')->info('SQL', ['sql' => $query->sql, 'bindings' => $query->bindings, 'time' => $query->time]);<br> }<br>});
- 配合
Log::channel('debug')输出到独立文件,避免和业务日志混在一起 - 观察日志顺序:是否出现 BEGIN → 若干 INSERT/UPDATE → ROLLBACK?如果没有 ROLLBACK,说明异常没抛出或被吞了
手动事务 + 显式日志标记生命周期
比闭包更可控,适合需要排查“到底卡在哪一步”的场景。
- 生成唯一事务 ID:
$txnId = 'txn_'.uniqid(); - 开始前记录:
Log::info("事务启动", ['id' => $txnId]); - 用
try { ... DB::commit(); Log::info("事务提交", ['id' => $txnId]); } - 在
catch中:DB::rollback(); Log::error("事务回滚", ['id' => $txnId, 'exception' => $e->getMessage()]); - 这样每一笔事务的启停、成败、异常信息都可精确追溯
检查事务是否被意外“吞掉”
以下写法会让事务看似没回滚:
- 在
DB::transaction()闭包内写了try/catch,但没throw $e或DB::rollback() - 用了
return false、dd()、exit等非异常方式中断流程 —— 这不会触发回滚,事务连接可能还挂着 - 在闭包里调用了外部 HTTP 请求或文件写入,它们成功了,但 DB 回滚了,造成数据与状态不一致(这不是事务问题,是设计问题)
- 表引擎不是 InnoDB(如 MyISAM),根本**不支持事务** —— 进入数据库容器执行
SHOW CREATE TABLE xxx;确认 ENGINE=InnoDB











