Laravel中如何避免异步API并发触发重复SQL插入的完整性约束冲突?
解决MySQL并发插入重复中间表记录的问题
问题背景
我们的React应用通过向Laravel GraphQL API发起多份参数不同的异步请求,以此避免性能过慢并提升容错性。但有时即便在插入前检查记录是否存在,仍会向同一张中间表插入相同值的记录。推测原因是多个异步请求在绕过存在性检查后,同时执行插入操作,进而触发重复值导致的完整性约束错误。
目前我们采用串行请求的方式解决,但性能较慢;使用队列任务插入又过于复杂。除这两种方式外,是否有其他方法可确保MySQL不会同时执行这两条插入查询?我了解到使用upsert或try catch可掩盖错误,但无法解决核心问题。我重写了代码,不再使用sync或syncWithoutDetaching,因为这两个方法触发错误的频率更高。
触发错误的代码
/** * Add strategy_department record, if that key does not already exist * * @param Strategy $strategy * @param Task $task * */ private function addStrategyDepartmentIfDoesntExist(Strategy $strategy, Task $task): void { $this->handleErrorsForAddStrategyDepartmentIfDoesntExist($strategy, $task); $strategy_department_table_name = 'strategy_department'; /** * If error is not thrown, then we can add the department to the strategy if it doesn't exist */ $strategy_department_found = DB::table($strategy_department_table_name) ->where('strategy_id', $strategy->id) ->where('department_id', $task->service->department->id)->first(); if (!isset($strategy_department_found)) { DB::table($strategy_department_table_name)->insert([ 'strategy_id' => $strategy->id, 'department_id' => $task->service->department->id, 'created_at' => now(), 'updated_at' => now(), ]); } }
解决方案
1. 利用数据库唯一约束 + 原子化插入(替代先查后插)
核心问题是并发下的"检查-插入"非原子操作,直接把判断逻辑交给数据库,用原子化语句完成操作,从根源避免竞态:
方案A:使用INSERT IGNORE
Laravel中通过原生SQL执行,忽略约束冲突的插入请求:
DB::statement("INSERT IGNORE INTO {$strategy_department_table_name} (strategy_id, department_id, created_at, updated_at) VALUES (?, ?, ?, ?)", [ $strategy->id, $task->service->department->id, now(), now() ]);
全程是原子操作,不会出现多个请求同时绕过检查的情况。
方案B:使用INSERT ... ON DUPLICATE KEY UPDATE
如果需要在重复时更新字段(比如updated_at),可以用该语句:
DB::statement("INSERT INTO {$strategy_department_table_name} (strategy_id, department_id, created_at, updated_at) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE updated_at = ?", [ $strategy->id, $task->service->department->id, now(), now(), now() ]);
同样是原子操作,既避免重复插入,又能在重复时更新数据,比先查后插更可靠。
2. 事务加行锁控制并发
针对目标记录加行锁,确保同一时间只有一个请求能执行插入操作:
DB::transaction(function () use ($strategy, $task, $strategy_department_table_name) { // 先锁定目标行,其他请求需等待当前事务完成 DB::table($strategy_department_table_name) ->where('strategy_id', $strategy->id) ->where('department_id', $task->service->department->id) ->lockForUpdate() ->first(); // 再执行检查和插入 $strategy_department_found = DB::table($strategy_department_table_name) ->where('strategy_id', $strategy->id) ->where('department_id', $task->service->department->id) ->first(); if (!isset($strategy_department_found)) { DB::table($strategy_department_table_name)->insert([ 'strategy_id' => $strategy->id, 'department_id' => $task->service->department->id, 'created_at' => now(), 'updated_at' => now(), ]); } });
这种方式会有轻微性能损耗,但远优于串行请求。
关键前提:建立联合唯一约束
上述方案的基础是,strategy_department表必须在strategy_id和department_id上建立联合唯一约束,否则数据库无法识别重复记录。未添加的话,执行以下迁移:
Schema::table('strategy_department', function (Blueprint $table) { $table->unique(['strategy_id', 'department_id']); });
内容的提问来源于stack exchange,提问作者Drew Gallagher
相关产品推荐
相关产品推荐

