You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 10:55:35