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

无唯一索引时如何实现线程安全的存在则更新/不存在则插入?

线程安全实现「存在则更新,不存在则创建」

问题描述

需要实现逻辑:检查某行记录是否存在(user_id、comment_type匹配且last_update在5分钟内),存在则更新last_update,不存在则创建新记录。但当前代码非线程安全,并发场景下两个线程可能同时查到无记录,导致插入重复数据。现有代码:

$row = DB::table('some_table')
    ->where('last_update', '>=', now()->subMinutes(5))
    ->where('user_id', '=', $user_id)
    ->where('comment_type', '=', $comment_type)
    ->first();

if ($row === null) {
     // record not found, create new
     DB::table('some_table')->insert([
        'user_id' => $user_id,        
        'comment_type' => $comment_type,     
        'created_at' => $created_at,     
        'last_update' => $last_update   
    ]);
} else {
     // record found, update existing
     DB::table('some_table')
          ->where('id', '=', $row->id)
          ->update(['last_update' => now()]);     
}

解决方案

方法一:事务+悲观锁(最贴合原有业务逻辑)

把查询和写入操作包裹在事务中,查询时加排他锁,确保其他线程无法同时操作相关记录:

DB::transaction(function () use ($user_id, $comment_type, $created_at, $last_update) {
    // 加排他锁锁定符合条件的行,避免其他线程并发修改
    $row = DB::table('some_table')
        ->where('last_update', '>=', now()->subMinutes(5))
        ->where('user_id', '=', $user_id)
        ->where('comment_type', '=', $comment_type)
        ->lockForUpdate()
        ->first();

    if ($row === null) {
        DB::table('some_table')->insert([
            'user_id' => $user_id,
            'comment_type' => $comment_type,
            'created_at' => $created_at,
            'last_update' => $last_update
        ]);
    } else {
        DB::table('some_table')
            ->where('id', '=', $row->id)
            ->update(['last_update' => now()]);
    }
});

原理:lockForUpdate()会对查询到的行加排他锁,其他线程需等待当前事务提交才能操作这些行;事务的原子性保证了查询与写入是不可分割的整体,不会被并发打断。

方法二:唯一约束+原子Upsert(适合调整业务逻辑场景)

如果业务允许同一个user_id+comment_type只保留一条最新记录,可通过唯一约束+Laravel的upsert实现完全原子化操作:

  1. 添加复合唯一索引
ALTER TABLE some_table ADD UNIQUE INDEX idx_user_comment (user_id, comment_type);
  1. 使用Upsert方法
DB::table('some_table')->upsert(
    [
        'user_id' => $user_id,
        'comment_type' => $comment_type,
        'created_at' => $created_at,
        'last_update' => now()
    ],
    ['user_id', 'comment_type'], // 触发唯一约束的字段
    ['last_update'] // 冲突时需要更新的字段
);

原理:upsert是数据库级别的原子操作,先尝试插入,若触发唯一约束则自动执行更新,彻底避免并发重复插入问题。

方法三:乐观锁(适合高并发低冲突场景)

若不想用悲观锁影响性能,可添加版本字段实现乐观锁,通过重试机制处理冲突:

  1. 添加版本字段
ALTER TABLE some_table ADD COLUMN version INT DEFAULT 1;
  1. 修改业务逻辑
do {
    $row = DB::table('some_table')
        ->where('last_update', '>=', now()->subMinutes(5))
        ->where('user_id', '=', $user_id)
        ->where('comment_type', '=', $comment_type)
        ->first();

    if ($row === null) {
        try {
            DB::table('some_table')->insert([
                'user_id' => $user_id,
                'comment_type' => $comment_type,
                'created_at' => $created_at,
                'last_update' => $last_update,
                'version' => 1
            ]);
            break;
        } catch (\Illuminate\Database\QueryException $e) {
            // 捕获插入冲突,重新查询
            continue;
        }
    } else {
        // 更新时校验版本号,确保未被其他线程修改
        $updated = DB::table('some_table')
            ->where('id', '=', $row->id)
            ->where('version', '=', $row->version)
            ->update([
                'last_update' => now(),
                'version' => $row->version + 1
            ]);

        if ($updated) break;
    }
} while (true);

原理:通过版本号判断记录是否被并发修改,若冲突则重试逻辑,避免锁竞争,适合高并发场景但会有少量重试开销。


内容的提问来源于stack exchange,提问作者pileup

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 04:20:29