Laravel使用lockForUpdate后关联Bars仍为旧值的问题及解决
问题原因及解决方案
原因分析
- 可重复读隔离级别的快照特性:MySQL默认采用
REPEATABLE READ(可重复读)隔离级别,事务启动时会生成一致性快照,事务内所有查询均基于该快照返回数据。若第二次事务在第一次事务提交前启动,即便主查询(Foo表)因lockForUpdate等待至第一次事务提交后执行,关联的bars查询仍会使用事务启动时的旧快照,导致获取到旧数据。 - 关联查询未同步加锁:
lockForUpdate仅作用于主查询的Foo行数据,而with('bars')触发的关联查询未加锁控制。并发场景下,bars的查询可能在第一次事务完成更新前就执行完毕,提前获取了旧数据。
解决方案
方案1:调整事务隔离级别为读已提交
将当前事务的隔离级别改为READ COMMITTED,使事务内的查询始终读取已提交的最新数据,而非启动时的快照。可在事务内临时设置:
DB::transaction(function () { DB::statement('SET TRANSACTION ISOLATION LEVEL READ COMMITTED'); $foo = Foo::whereId(1)->lockForUpdate()->with('bars')->first(); // 业务逻辑:打印、更新字段 });
注意:不建议全局修改数据库隔离级别,避免影响其他依赖可重复读特性的业务。
方案2:给关联查询也添加行锁
通过闭包给关联查询加上lockForUpdate,确保主查询和关联查询同步等待前序事务提交,获取一致的最新数据:
DB::transaction(function () { $foo = Foo::whereId(1)->lockForUpdate() ->with(['bars' => function ($query) { $query->lockForUpdate(); }])->first(); // 业务逻辑:打印、更新字段 });
方案3:拆分查询,手动加载关联
先查询主模型(等待锁释放获取最新Foo数据),再手动调用load方法加载关联,强制从数据库读取最新的bars数据:
DB::transaction(function () { $foo = Foo::whereId(1)->lockForUpdate()->first(); $foo->load('bars'); // 此时加载的bars为最新数据 // 业务逻辑:打印、更新字段 });
内容的提问来源于stack exchange,提问作者Cositanto
相关产品推荐
相关产品推荐

