mysql 查询后, 不存在则插入记录, 但是在高并发时容易导致重复插入, 有什么解决办法吗

在高并发场景下,为了避免因并发请求导致的重复插入问题,可以采用以下几种策略:

  1. 使用 INSERT ... ON DUPLICATE KEY UPDATE:
    利用 MySQL 提供的 ON DUPLICATE KEY UPDATE 语句,可以在尝试插入数据时,如果发现唯一键(如主键或唯一索引)冲突,即数据已存在,自动转为执行更新操作。确保无论多少并发请求同时到达,只要涉及的关键字段值相同,只会执行一次插入或更新操作。

    INSERT INTO table (key_column, other_columns...)
    VALUES (value1, value2...)
    ON DUPLICATE KEY UPDATE
        column_to_update = value_for_update,
        another_column = another_value;
    

    需要确保表中有适当的唯一约束(如主键或唯一索引)定义在用于判断数据唯一性的列上。

  2. 使用 REPLACE INTO:
    REPLACE INTO 语句在插入时,如果发现存在与新数据主键或唯一索引相同的数据行,会先删除旧数据行,再插入新数据。这样可以保证数据的唯一性,但请注意,它会触发一次删除操作,可能导致额外的行级锁定和事务日志增长,且原数据会被替换,可能会丢失某些历史信息。

    REPLACE INTO table (key_column, other_columns...)
    VALUES (value1, value2...)
    
  3. 使用 INSERT IGNOREINSERT ... IGNORE ...:
    使用 INSERT IGNORE 可以在遇到唯一键冲突时忽略错误,即不执行插入也不更新,而是直接跳过。这样可以避免重复插入,但同时也可能掩盖其他可能的插入错误。

    INSERT IGNORE INTO table (key_column, other_columns...)
    VALUES (value1, value2...)
    
  4. 使用 INSERT ... SELECT ... WHERE NOT EXISTS:
    在插入之前先进行一次查询,确认数据不存在后再插入。这种方式需要在应用程序中控制并发,确保查询与插入作为一个原子操作完成,通常通过事务和适当的锁定机制实现。

    START TRANSACTION;
    
    IF NOT EXISTS (SELECT * FROM table WHERE key_column = value1)
    THEN
      INSERT INTO table (key_column, other_columns...)
      VALUES (value1, value2...);
    END IF;
    
    COMMIT;
    

    注意:在高并发场景下,单纯依赖这种查询+插入的方式可能仍无法避免竞态条件,因为查询和插入之间存在时间窗口,其他并发请求可能在此期间插入相同数据。

  5. 应用程序层面的同步控制:

    • 乐观锁:在表中添加一个版本号(或时间戳)字段,插入或更新时带上预期的版本号。如果实际版本号与预期不符(表明数据已被其他并发操作修改),则回滚事务并重试。
    • 悲观锁:在执行插入前,对目标数据范围加排他锁(如 SELECT ... FOR UPDATE),确保在锁定期间其他并发请求无法插入相同数据。但过度使用悲观锁可能会导致锁争用和性能下降。
    • 分布式锁:对于分布式系统,可以使用分布式锁服务(如 Redis、Zookeeper 等)在应用程序层面协调多个节点间的并发插入,确保同一时刻只有一个请求能够执行插入操作。

    结合以上方法,并结合具体的业务需求和系统架构,选择最合适的策略来避免高并发下的重复插入问题。在许多场景中,INSERT ... ON DUPLICATE KEY UPDATE 由于其简洁性和内建的并发控制能力,通常是首选方案。如果需要更复杂的业务逻辑或更强的隔离性,可能需要结合应用程序层面的同步控制措施。

在Laravel框架中的应用

场景: 假设是订单表, 外部不断地webhook推送订单进来, 本地数据库订单表shopify_order_id与webhook推送过来的订单信息中shopify_order_id对应

在 Laravel 框架中,可以结合上述提到的 SQL 方案,利用 Eloquent ORM 或查询构造器来处理高并发下的订单插入问题。假设你有一个名为 orders 的表,其中包含一个 shopify_order_id 字段作为唯一标识,且外部 webhook 不断推送订单信息,你需要确保每个订单只被插入一次。这里提供两种常用方法:

方法一:使用 Eloquent 的 firstOrCreate()updateOrCreate()

Eloquent 提供了便捷的方法 firstOrCreate()updateOrCreate(),它们内部已经封装了查找和插入/更新的操作,可以有效避免并发插入的问题。

use App\Models\Order;

// 接收到 webhook 推送的订单信息
$incomingOrderData = [
    'shopify_order_id' => $webhookOrderId,
    // 其他订单字段...
];

// 使用 firstOrCreate() 或 updateOrCreate()
$order = Order::updateOrCreate(
    ['shopify_order_id' => $incomingOrderData['shopify_order_id']], // 查找条件
    $incomingOrderData // 插入或更新的数据
);

// $order 现在包含了已存在或新创建的订单实例
  • firstOrCreate():如果找到匹配的 shopify_order_id,则返回该记录;否则,使用提供的数据创建一个新的记录。
  • updateOrCreate():如果找到匹配的 shopify_order_id,则使用提供的数据更新该记录;否则,创建一个新的记录。

这两种方法在内部都会通过查询锁定(如 FOR UPDATE)来确保在操作期间不会发生并发插入。

方法二:使用查询构造器的 insertOrIgnore()upsert()

如果你更倾向于使用查询构造器,可以使用 insertOrIgnore()upsert() 方法:

use Illuminate\Support\Facades\DB;

// 接收到 webhook 推送的订单信息
$incomingOrderData = [
    'shopify_order_id' => $webhookOrderId,
    // 其他订单字段...
];

// 使用 insertOrIgnore()
DB::table('orders')->insertOrIgnore($incomingOrderData);

// 或者使用 upsert()
DB::table('orders')->upsert(
    $incomingOrderData, // 插入或更新的数据
    ['shopify_order_id'], // 唯一/更新键
    // 如果需要更新其他字段,可以在此指定更新的字段和值
    // ['column1' => 'value1', 'column2' => 'value2']
);

// 这两个方法都不会返回订单实例,仅负责插入或忽略操作
  • insertOrIgnore():尝试插入数据,如果因唯一键冲突导致插入失败,则忽略错误,不会引发异常。
  • upsert():类似于 SQL 中的 INSERT ... ON DUPLICATE KEY UPDATE,如果找到匹配的 shopify_order_id,则根据提供的字段列表更新记录;否则,插入新的记录。

在使用这些方法时,请确保 shopify_order_id 字段在数据库表中已经被正确地定义为唯一键(主键或唯一索引)。这样,无论是 Eloquent 还是查询构造器的方法,都能在高并发场景下有效地防止重复插入订单。

Eloquent 实现mysqlINSERT … ON DUPLICATE KEY UPDATE` 怎么写?

在 Laravel 的 Eloquent ORM 中,你可以使用 updateOrCreate() 方法来实现类似 INSERT ... ON DUPLICATE KEY UPDATE 的效果。以下是具体的实现方式:

首先,假设你有一个名为 Order 的 Eloquent 模型,对应 orders 表,其中 shopify_order_id 是唯一的标识字段。当接收到 webhook 推送的订单信息时,你可以这样操作:

use App\Models\Order;

// 接收到 webhook 推送的订单信息
$incomingOrderData = [
    'shopify_order_id' => $webhookOrderId,
    // 其他订单字段...
];

// 使用 updateOrCreate()
$order = Order::updateOrCreate(
    ['shopify_order_id' => $incomingOrderData['shopify_order_id']], // 查找条件
    $incomingOrderData // 插入或更新的数据
);

// $order 现在包含了已存在或新创建的订单实例

在这个例子中:

  • updateOrCreate() 方法接受两个参数:
    • 第一个参数是一个数组,表示用于查找记录的条件。这里使用了 shopify_order_id 作为唯一标识。
    • 第二个参数也是数组,包含需要插入或更新的字段及其对应的值。如果找到了匹配的 shopify_order_id,则会使用这些数据更新已有记录;如果没有找到匹配的记录,则会使用这些数据创建一个新的订单记录。

注意:

  • 确保 shopify_order_id 字段在 orders 表中被正确地定义为唯一键(主键或唯一索引),这样才能确保在高并发场景下避免重复插入。
  • updateOrCreate() 方法返回的是一个 Eloquent 模型实例,代表已存在或新创建的订单。你可以继续对这个实例进行其他操作,如保存关联关系、触发事件等。

通过这种方式,Eloquent ORM 自动帮你处理了 INSERT ... ON DUPLICATE KEY UPDATE 的逻辑,无需手动编写 SQL 语句。在处理 webhook 推送的订单信息时,只需调用 updateOrCreate() 方法即可确保数据的一致性和完整性。

Golang的gorm如何使用INSERT … ON DUPLICATE KEY UPDATE来确保不会重复插入

使用 Clauses 函数实现 ON DUPLICATE KEY UPDATE
为了在一次操作中实现类似 INSERT ... ON DUPLICATE KEY UPDATE 的效果,可以使用 GORM 的 Clauses 函数配合自定义 SQL 语句。这样可以确保在高并发场景下避免重复插入:

func processWebhookOrderWithOnDuplicateUpdate(db *gorm.DB, webhookOrderData Order) error {
    // 构造 ON DUPLICATE KEY UPDATE 的 SQL 片段
    onDuplicateUpdate := fmt.Sprintf("ON DUPLICATE KEY UPDATE %s=VALUES(%s), %s=VALUES(%s)",
        db.QuoteColumnName("updated_at"), db.QuoteColumnName("updated_at"),
        db.QuoteColumnName("other_field"), db.QuoteColumnName("other_field"))

    // 执行带有 ON DUPLICATE KEY UPDATE 的插入操作
    result := db.Clauses(clause.OnConflict{
        Columns: []clause.Column{{Name: "shopify_order_id"}}, // 唯一键冲突列
        DoUpdates: clause.Assignments(map[string]interface{}{
            "updated_at": gorm.Expr("VALUES(updated_at)"), // 使用 VALUES() 函数保留插入时的值
            "other_field": gorm.Expr("VALUES(other_field)"), // 其他需要更新的字段
        }),
    }).Create(&webhookOrderData)

    return result.Error
}

在这个例子中:

使用 Clauses 函数传入 clause.OnConflict 结构体,指定在唯一键(这里是 shopify_order_id)冲突时的处理方式。
DoUpdates 字段指定了冲突时要更新的字段及其值。这里使用 gorm.Expr(“VALUES(field)”) 保留插入时的值,也可以直接指定新的值。
最后,调用 Create 方法执行带有 ON DUPLICATE KEY UPDATE 逻辑的插入操作。
通过上述方法,GORM 在高并发场景下可以有效地避免订单的重复插入,同时确保数据一致性。使用 Clauses 函数实现 ON DUPLICATE KEY UPDATE 语句的方式更为推荐,因为它在单次数据库操作中完成了插入或更新,减少了竞态条件的发生。

你可能感兴趣的:(mysql,数据库)