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

多线程数据库插入时如何避免重复值插入?

解决多线程插入数据库的主键重复问题(无需捕获异常)

这个问题核心是竞态条件搞的鬼——两个线程同时检查同一主键的记录,都判定“不存在”后同时执行插入,直接触发主键重复错误。既然不想靠捕获异常来兜底,咱们可以从「数据库原子操作」「应用层同步锁」「事务隔离级别」这几个方向来解决,给你具体的方案:

1. 数据库层面:用原子化的插入语句(最推荐)

把“检查是否存在+插入”的逻辑合并成数据库端的原子操作,让数据库帮你处理并发冲突,这是最可靠且性能损耗最小的方案。

MySQL 示例:INSERT ... ON DUPLICATE KEY UPDATE

如果你的数据库是MySQL,可以用这个语法:

INSERT INTO team (id, name) VALUES (1249, '银河战队')
ON DUPLICATE KEY UPDATE name = name; -- 重复时执行无意义的更新,或者按需调整逻辑

这条语句是数据库原子执行的,不管多少线程同时调用,只会有一条插入成功,其他的会执行UPDATE(部分MySQL版本支持DO NOTHING,可以直接跳过重复插入),完全不会抛出重复键异常。

SQL Server 示例:MERGE 语句

SQL Server可以用MERGE来实现同样的逻辑:

MERGE INTO team AS target
USING (SELECT 1249 AS id, '银河战队' AS name) AS source
ON target.id = source.id
WHEN NOT MATCHED THEN
    INSERT (id, name) VALUES (source.id, source.name)
WHEN MATCHED THEN
    UPDATE SET name = source.name; -- 或者什么都不做

同样是原子操作,彻底避免竞态条件。

2. 应用层面:加全局/细粒度锁

如果没法修改SQL语句,可以在应用层对需要操作的资源加锁,确保同一时间只有一个线程能执行“检查+插入”的逻辑。

全局锁(简单但性能一般)

定义一个静态的锁对象,在操作team记录时锁住:

// 全局锁对象,确保是静态且只读的
private static readonly object _teamLock = new object();

public void AddTeams()
{
    lock(_teamLock)
    {
        // 检查team 1249是否存在
        if (!IsTeamExist(1249))
        {
            InsertTeam(1249);
        }
        // 插入关联记录
        InsertTeamRelatedData(1249);
    }
}

注意:这种全局锁会让所有操作team的线程排队,并发量高的时候会影响性能。

细粒度锁(推荐,兼顾并发和安全)

如果有多个不同的team id需要处理,可以用ConcurrentDictionary来为每个id创建独立的锁,避免互相阻塞:

// 存储每个team id对应的锁对象
private static readonly ConcurrentDictionary<int, object> _teamLocks = new ConcurrentDictionary<int, object>();

public void AddTeams()
{
    // 获取或创建team 1249对应的锁对象
    var lockObj = _teamLocks.GetOrAdd(1249, key => new object());
    
    lock(lockObj)
    {
        if (!IsTeamExist(1249))
        {
            InsertTeam(1249);
        }
        InsertTeamRelatedData(1249);
    }
}

这样不同team id的操作可以并行执行,只有同一个id的操作会排队,性能友好很多。

3. 事务+高隔离级别(备选,注意性能)

把“检查+插入”放到一个事务里,并且设置较高的隔离级别(比如SERIALIZABLE),让数据库在事务期间锁住相关资源,防止其他线程修改。

以EF Core为例:

using(var transaction = dbContext.Database.BeginTransaction(System.Data.IsolationLevel.Serializable))
{
    try
    {
        // 检查team是否存在
        if (!dbContext.Teams.Any(t => t.Id == 1249))
        {
            dbContext.Teams.Add(new Team { Id = 1249, Name = "银河战队" });
            dbContext.SaveChanges();
        }
        // 插入关联记录
        dbContext.TeamRecords.Add(new TeamRecord { TeamId = 1249 });
        dbContext.SaveChanges();
        
        transaction.Commit();
    }
    catch
    {
        transaction.Rollback();
        throw; // 这里抛出的异常不会是主键重复,而是锁超时之类的,按需处理
    }
}

⚠️ 注意:SERIALIZABLE隔离级别会让数据库加更多锁,可能导致并发性能下降,适合并发量不高的场景。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 09:50:32