Laravel场景下如何仅锁定可用数据集中指定数量的行?
票务系统座位分配锁优化方案
核心问题拆解
原代码的性能瓶颈在于lockForUpdate锁定了所有符合条件的可用座位,而非仅要分配的N个。这会导致高峰时大量事务等待同一批锁,吞吐量急剧下降。以下是几种可行的优化方案:
方案1:精准锁定待分配座位(最直接的优化)
给查询添加明确的排序规则,让数据库只锁定需要分配的N个座位,而非扫描并锁定所有可用座位。
// 开启事务 $assignedSeats = VenueSeat::available() ->where('event_id', 1) ->orderBy('seat_id') // 按座位唯一标识排序,确保数据库精准定位前N个 ->take(2) ->lockForUpdate() ->get(); // 仅更新已锁定的座位状态 VenueSeat::whereIn('id', $assignedSeats->pluck('id')) ->update(['status' => 'booked']); // 提交事务
原理:添加orderBy后,数据库会利用索引快速定位到符合条件的前N个座位,只会锁定这几行数据,锁范围大幅缩小,避免不必要的资源占用。同时事务内的锁定机制确保其他请求无法修改这些座位,从根源避免重复预订。
方案2:跳过锁定行,提升并发吞吐量(适合高并发场景)
如果你的数据库支持SKIP LOCKED(MySQL 8.0+、PostgreSQL 9.5+),可以直接跳过已被其他事务锁定的座位,无需等待,直接获取可用座位。
// 开启事务 $assignedSeats = VenueSeat::available() ->where('event_id', 1) ->orderBy('seat_id') ->take(2) ->lockForUpdate() ->skipLocked() // 跳过已被锁定的座位,直接取可用的 ->get(); // 检查是否获取到足够座位 if ($assignedSeats->count() < 2) { DB::rollBack(); // 返回座位不足的提示 return ['code' => 400, 'msg' => '剩余座位不足']; } // 更新座位状态 VenueSeat::whereIn('id', $assignedSeats->pluck('id')) ->update(['status' => 'booked']); // 提交事务
优势:避免了事务间的等待阻塞,高并发下能显著提升系统吞吐量,用户不会因为锁等待而长时间卡顿。
方案3:批次化锁范围(适合超大型场次)
如果单场次座位数量极多(比如上千个),可以提前将座位划分成小批次(比如每10个座位一组),给VenueSeat表添加batch_id字段标记批次。
// 开启事务 // 1. 先找到有可用座位的批次 $targetBatch = VenueSeat::available() ->where('event_id', 1) ->select('batch_id') ->groupBy('batch_id') ->havingRaw('COUNT(*) >= 2') ->first(); if (!$targetBatch) { DB::rollBack(); return ['code' => 400, 'msg' => '剩余座位不足']; } // 2. 锁定该批次内的可用座位 $assignedSeats = VenueSeat::available() ->where('event_id', 1) ->where('batch_id', $targetBatch->batch_id) ->take(2) ->lockForUpdate() ->get(); // 3. 更新座位状态 VenueSeat::whereIn('id', $assignedSeats->pluck('id')) ->update(['status' => 'booked']); // 提交事务
原理:将锁的范围从"所有可用座位"缩小到"单个批次的座位",即使高并发下,锁冲突的概率也会大幅降低,同时保证批次内的座位分配不会重复。
关于"先查ID再锁定"的顾虑
这种方式确实存在竞态风险:查询ID时未加锁,其他事务可能在你锁定前就抢走了这些座位。正确的做法是在查询座位的同时加锁(如方案1、2),确保查询到的座位从那一刻起就被当前事务独占,直到事务结束。
内容的提问来源于stack exchange,提问作者Alief
相关产品推荐
相关产品推荐

