Laravel 用数据库事务与悲观锁守住并发写:transaction、lockForUpdate 与避免死锁

test12026-11-011 次阅读

并发写最怕两件事

余额扣减、库存扣减这类操作,多个请求同时进来,读到的旧值一样,最后一起写回,就会丢更新。解决靠两把锁:数据库事务保证原子,悲观锁(lockForUpdate)保证读的时候就堵住别人。

用 DB 门面写事务

use Illuminate\Support\Facades\DB;

DB::transaction(function () use ($userId, $amount) {
  $wallet = DB::table('wallets')
    ->where('user_id', $userId)
    ->lockForUpdate()
    ->first();

  if ($wallet->balance < $amount) {
    throw new \RuntimeException('余额不足');
  }

  DB::table('wallets')
    ->where('user_id', $userId)
    ->update(['balance' => $wallet->balance - $amount]);

  DB::table('transactions')->insert([
    'user_id' => $userId,
    'amount'  => $amount,
    'created_at' => now(),
  ]);
}, 3); // 第三个参数是死锁/失败时的重试次数

用 Eloquent 模型也一样

锁是查询作用域的一部分,模型查询链上直接加即可。

$user = User::findOrFail($id);

DB::transaction(function () use ($user, $amount) {
  $wallet = $user->wallet()->lockForUpdate()->first();
  $wallet->decrement('balance', $amount);
});

读锁、写锁与死锁

  • lockForUpdate:写锁,别人不能读也不能写这行,直到事务提交。
  • sharedLock:读锁,别人能读不能写,适合“先核对再决定”。
  • 锁只在事务内生效,务必包在 DB::transaction 里,否则锁立刻释放。
  • 多张表加锁时,所有请求按相同顺序加锁,避免 A 等 B、B 等 A 的死锁。

只有 InnoDB 等支持行锁的引擎才有效,MyISAM 会把整张表锁死。锁的范围越小越好,别在事务里塞慢查询,否则并发直接掉地上。

T

test1

文章作者

并发写最怕两件事 余额扣减、库存扣减这类操作,多个请求同时进来,读到的旧值一样,最后一起写回,就会丢更新。解决靠两把锁...

分类
技术
发布时间
2026-11-01
字数
约 1158 字
阅读
1 次