Laravel事务监听依赖数据库事件而非钩子,TransactionBeginning事件在BEGIN前触发可获取连接,但transactionLevel()仍为0;Committed/RolledBack事件不区分回滚原因,DB::transaction()闭包不触发事件,监听器内禁止写库操作。

事务开始时怎么拿到连接实例
事务监听不是靠“注册钩子”实现的,Laravel 没有 onTransactionStarting 这种 API。真正能捕获事务起点的方式,是监听数据库事件——尤其是 Illuminate\Database\Events\TransactionBeginning 事件。
这个事件在 DB::beginTransaction() 被调用、且底层 PDO 连接实际执行 BEGIN 之前触发,此时事务尚未真正开启,但连接对象已就位,可以安全地取到 $event->connection。
常见错误是试图在事件里调用 $connection->transactionLevel(),结果返回 0 —— 因为事件触发时机早于事务计数器自增。别依赖这个值判断“是否已进事务”,它这时还没变。
实操建议:
- 在
EventServiceProvider的$listen中注册监听器:Illuminate\Database\Events\TransactionBeginning→App\Listeners\LogTransactionStart - 监听器构造函数不要依赖
DB或具体连接,事件对象本身已携带$event->connection - 避免在监听器里触发新查询(比如写日志表),否则可能意外开启嵌套事务或干扰当前事务状态
提交/回滚后如何区分成功与失败
Laravel 提供了两个独立事件:Illuminate\Database\Events\TransactionCommitted 和 Illuminate\Database\Events\TransactionRolledBack,它们只在事务**真正完成**后触发,不关心你用的是 DB::commit() 还是 $db->commit()。
关键点在于:这两个事件不传递“事务是否干净结束”的上下文。比如手动 DB::rollBack() 会触发 TransactionRolledBack,但底层异常导致的自动回滚也会触发它——你无法单从事件类型判断是主动放弃还是出错了。
性能影响很小,但要注意兼容性:Laravel 9+ 才保证这两个事件在所有连接驱动(包括 SQLite)下稳定触发;8.x 在某些 MySQL 配置下可能漏发 Committed。
实操建议:
- 如果需要区分异常回滚和主动回滚,得自己维护状态。例如在事务外设一个
$shouldRollback = false标志,在 catch 块里设为 true,再通过 Closure 绑定传入监听器 - 不要在
TransactionCommitted里重试操作——事务已关闭,连接可能已被释放 - 监听器中避免调用
$connection->getPdo(),部分驱动(如 SQL Server)在此时 PDO 实例可能已不可用
为什么 DB::transaction() 匿名函数里加监听没用
这是最容易踩的坑:DB::transaction(function ($db) { ... }) 是 Laravel 封装的“带自动回滚的 try-catch + 事务块”,它的整个执行过程不触发任何事务事件。
原因很简单:这个方法内部直接调用 $db->beginTransaction()、$db->commit()、$db->rollBack(),绕过了事件分发逻辑。只有显式调用 DB::beginTransaction() 等全局辅助函数,才会走事件系统。
使用场景上,如果你需要监听,就不能用 DB::transaction() 闭包形式,而要手动展开:
DB::beginTransaction();
try {
// 业务逻辑
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
实操建议:
- 统一代码风格:项目里要么全用事件监听 + 手动事务,要么全用
DB::transaction()闭包,混用会导致监听失效且难以排查 - 测试时注意:单元测试里用
DatabaseTransactionstrait 会静默开启/关闭事务,它不触发TransactionBeginning等事件 - 想保留
DB::transaction()的简洁性又需要监听?只能包装一层,比如写个TrackedTransaction::run(...),里面先触发自定义事件,再调用原生DB::transaction()
监听器里读写数据库的风险在哪
监听器运行在事务生命周期的关键节点,但它本身**不属于当前事务上下文**。比如在 TransactionCommitted 里执行 DB::table('logs')->insert(...),这条语句会走独立连接或复用连接,但不会被包含在刚提交的事务中。
更危险的是:如果监听器抛出异常,Laravel 不会回滚刚刚完成的事务(它已经 commit 了),但会中断后续监听器执行,并可能让调用方误以为“事务失败”。这种静默失配很难调试。
另一个隐形问题:高并发下多个事务几乎同时提交,监听器里的写操作可能因表锁或唯一约束冲突而失败,而你根本没地方 catch 它。
实操建议:
- 监听器里禁止任何写操作。只做轻量记录(如写文件、发消息队列、打日志)
- 真要落库,用异步方式:dispatch(new LogTransactionEventJob($event)),由队列 worker 在事务外处理
- 如果必须同步写,确保目标表无事务依赖(比如日志表引擎用 MyISAM 或配置为 autocommit=1),并包好 try/catch,忽略插入失败
事务监听本质是观察者模式,不是扩展点。越想在里面做事情,越容易把事务边界搞模糊。真正需要干预流程的地方,应该回到业务层手动控制 begin/commit/rollback,而不是指望监听器能帮你兜底。











