事务跟行锁(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))写在事务内部,才能保证锁的有效性和并发安全。正确流程是:
    开启事务 → 锁定记录 → 执行操作 → 提交/回滚事务。
    这种方式可以有效避免并发场景下的资源竞争,确保数据一致性。


daoheng
1 声望0 粉丝

活到老,学到老