Laravel在线游戏并发请求绕过装备校验问题解决咨询
这是典型的并发竞态条件问题,和Laravel本身配置关系不大——核心原因是你的校验和更新操作不是原子性的:当玩家快速点击时,多个请求同时到达服务器,第一个请求还没完成equipped=1的更新,第二个请求已经通过了$alreadyequippeditem == false的校验,自然就绕过了限制。原生PHP没遇到这个问题,大概率是因为原生代码的请求处理逻辑更同步,或者并发量极低,没触发这个场景。
下面给你两个全站适用的解决方案,不需要依赖队列:
方案一:事务+行锁实现原子性校验与更新
利用Laravel的数据库事务和行锁,把“校验是否已装备”和“更新装备状态”变成一个不可分割的原子操作,确保同一时间只有一个请求能执行这个逻辑。
代码示例:
use Illuminate\Support\Facades\DB; // 包裹在事务中 DB::transaction(function () use ($item) { // 这里要根据你的业务,用user_id和物品类型查询已装备的物品,同时加排他锁 $alreadyEquipped = \App\Inventory::where('user_id', auth()->user()->id) ->where('item_type', $item->type) // 假设你的物品有type字段区分类型 ->where('equipped', 1) ->lockForUpdate() // 给符合条件的行加排他锁,其他请求必须等当前事务提交才能操作 ->exists(); if (!$alreadyEquipped) { $inventory = \App\Inventory::find($item->pivot->id); $inventory->equipped = 1; $inventory->save(); } });
lockForUpdate()会在查询时锁定对应的行,其他并发请求会被阻塞,直到当前事务提交,这样就彻底避免了“校验通过后,其他请求抢先更新”的情况。
方案二:数据库唯一约束兜底
如果你的业务逻辑明确要求同用户同类型只能装备一件物品,最稳妥的方式是在数据库层面加唯一约束,从根源上杜绝重复装备的可能。
步骤1:添加唯一索引
在Inventory表的迁移文件中添加组合唯一索引(如果是已存在的表,创建新的迁移文件):
public function up() { Schema::table('inventories', function (Blueprint $table) { // 组合唯一键:用户ID + 物品类型 + 已装备状态 $table->unique(['user_id', 'item_type', 'equipped'], 'unique_equipped_item'); }); }
这里把equipped也加入唯一键,因为只有当equipped=1时才需要约束,equipped=0的物品不受限制。
步骤2:代码中捕获约束冲突异常
当用户试图装备第二件同类型物品时,数据库会抛出唯一约束冲突的异常,你只需要在代码中捕获这个异常,返回友好提示即可:
use Illuminate\Support\Facades\DB; use Illuminate\Database\QueryException; try { DB::transaction(function () use ($item) { $inventory = \App\Inventory::find($item->pivot->id); $inventory->equipped = 1; $inventory->save(); }); } catch (QueryException $e) { // 不同数据库的约束冲突错误码不同,MySQL是1062,PostgreSQL是23505 if (in_array($e->getCode(), ['1062', '23505'])) { return response()->json(['message' => '你已经装备了同类型的物品'], 400); } // 其他异常正常抛出,不掩盖问题 throw $e; }
这个方案是全站通用的,任何涉及到“互斥状态”的操作都可以用这种方式兜底,即使代码层面有疏漏,数据库也会帮你拦住违规操作。
内容的提问来源于stack exchange,提问作者GalahadXVI

