事务跟行锁(Lock)的关系
首先解释下事务和锁各自的作用
- 事务的作用
事务主要保证一组数据库操作(增删改)的原子性, 即要么全部执行成功,要么全部失败,避免出现数据不一致的中间状态. 加锁的作用
加锁主要解决并发场景下的数据竞争问题, 比如多个请求同时修改同一条数据时,可能导致"脏读""不可重复读"等问题.如果仅仅使用事务不用锁的局限性
事务操作虽然能保证原子性,但是但是无法阻止并发场景下的数据竞争问题. 以'库存扣减'为例,假设商品初始库存为10,同时有两个订单(A和B)都要买5件,流程都是"查询库存->判断足够->扣减库存".
场景1:仅用事务不加锁会出现超卖
- 订单 A 的事务开始,查询库存:10(此时还没扣减)。
- 订单 B 的事务开始,也查询库存:10(因为 A 的扣减还没提交,B 读到的是旧数据)。
- 订单 A 判断库存足够,扣减 5,库存变为 5,提交事务。
- 订单 B 也判断库存足够(基于之前读到的 10),扣减 5,库存变为 0,提交事务。
结果: 最终库存0,两个订单各卖5件,看似没有问题,但如果两个订单都买6件哪? - A查库存10->扣6(剩4)->提交
B查库存10->扣6(剩-2)->提交
这里事务虽然保证了A和B各自的'扣减操作'是原子性的(不会只扣一半), 但无法阻止B在A未完成时读取并修改同一份库存数据,这就是并发数据竞争导致的问题.场景2: 事务+锁,避免超卖
加锁的作用是让并发操作"排队",确保同一时间只有一个事务能够修改目标数据,其它事务必须等待,从而避免基于旧数据做判断.
- 悲观锁(直接锁定数据,阻止并发修改)
在查询库存的时候就锁定对应数据, 其它事务必须等当前事务完成才能操作
事务跟行锁(Lock)结合使用
在 ThinkPHP 中,使用行锁(Lock) 锁定记录时,必须将锁操作写在事务内部。这是因为数据库的行锁依赖事务的上下文,只有在事务中才能保证锁的有效性和原子性。
原因分析
- 行锁的特性:
数据库的行锁(如for update)需要在事务中生效, 事务提交或者回滚后,锁会自动释放. 如果在事务外执行行锁操作,锁会立即释放,无法达到预期的锁定效果. 原子性保证
事务的核心作用是保证一组操作的原子性(要么全部成功,要么全部失败).将锁操作放在事务内,可以确保锁定的记录在事务完成前不会被其它事务修改,避免并发冲突.正确示例(锁在事务内部)
use think\Db; use app\model\User; // 开启事务 Db::startTrans(); try { // 1. 在事务内锁定记录(FOR UPDATE) $user = User::where('id', 1) ->lock(true) // 等同于 FOR UPDATE ->find(); if (!$user) { throw new \Exception('用户不存在'); } // 2. 执行更新操作(依赖锁定的记录) $user->balance -= 100; $user->save(); // 3. 提交事务(锁自动释放) Db::commit(); echo '操作成功'; } catch (\Exception $e) { // 回滚事务(锁自动释放) Db::rollback(); echo '操作失败:' . $e->getMessage(); }错误的示例(锁在事务外)
// 错误:锁在事务外,会立即释放 $user = User::where('id', 1)->lock(true)->find(); Db::startTrans(); try { // 此时锁已释放,可能被其他事务修改 $user->balance -= 100; $user->save(); Db::commit(); } catch (\Exception $e) { Db::rollback(); }上面这种写法lock(true)在事务外执行,查询结束后锁会立即释放,无法阻止其它事务修改记录,可能导致数据不一致
总结
必须将锁操作(lock(true))写在事务内部,才能保证锁的有效性和并发安全。正确流程是:
开启事务 → 锁定记录 → 执行操作 → 提交/回滚事务。
这种方式可以有效避免并发场景下的资源竞争,确保数据一致性。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。