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

EF Core并发处理咨询:FindOrCreate方法并发重复创建问题

处理并发场景下的FindOrCreate问题

你遇到的这个问题是并发场景里典型的检查-创建竞态条件:两个线程同时通过了“查找不存在”的检查,接着一个成功创建,另一个触发唯一索引异常。下面给你分析几种合适的解决方案,以及各自的适用场景:

方案1:捕获唯一索引异常并重试(推荐多实例场景)

既然已经在数据库层面加了唯一索引,那我们可以利用这个约束作为最终保障,捕获冲突异常后重试查找——这是分布式/多实例场景下最可靠的方案,因为应用层锁无法跨实例生效。

修改后的代码示例:

public async Task<Country> FindOrCreateAsync(string name)
{
    // 先尝试查找,无锁
    var country = await _context.Countries.FirstOrDefaultAsync(p => p.Name == name);
    if (country != null)
        return country;

    try
    {
        // 尝试创建
        country = new Country { Name = name };
        _context.Countries.Add(country);
        await _context.SaveChangesAsync();
        return country;
    }
    catch (DbUpdateException ex)
    {
        // 注意:不同数据库的唯一键冲突错误码不同,SQL Server是2601/2627,MySQL是1062
        if (ex.InnerException is SqlException sqlEx && (sqlEx.Number == 2601 || sqlEx.Number == 2627))
        {
            // 冲突发生,此时第一个线程已经创建完成,再次查找即可
            return await _context.Countries.FirstOrDefaultAsync(p => p.Name == name);
        }
        // 非唯一键异常,重新抛出
        throw;
    }
}

优点:跨实例有效,依赖数据库的天然约束,无需额外锁机制;缺点:会有少量预期内的异常,但重试逻辑可以完全处理,对业务无影响。

方案2:应用层内存锁(适合单实例场景)

如果你的服务是单实例部署的,那可以用应用层锁来确保同一时间只有一个线程执行创建逻辑。注意:必须在锁内再次执行查找,因为等待锁的过程中,可能已有其他线程完成了创建。

代码示例:

// 定义一个针对Country创建的专用锁
private readonly SemaphoreSlim _countryCreationLock = new SemaphoreSlim(1, 1);

public async Task<Country> FindOrCreateAsync(string name)
{
    // 先无锁查找
    var country = await _context.Countries.FirstOrDefaultAsync(p => p.Name == name);
    if (country != null)
        return country;

    // 进入锁
    await _countryCreationLock.WaitAsync();
    try
    {
        // 锁内再次检查!避免等待期间其他线程已创建
        country = await _context.Countries.FirstOrDefaultAsync(p => p.Name == name);
        if (country != null)
            return country;

        // 确认不存在,执行创建
        country = new Country { Name = name };
        _context.Countries.Add(country);
        await _context.SaveChangesAsync();
        return country;
    }
    finally
    {
        // 务必释放锁,避免死锁
        _countryCreationLock.Release();
    }
}

优点:避免了数据库异常,代码逻辑更“干净”;缺点:仅适合同一应用实例的并发,如果是多实例集群,不同实例的锁相互独立,无法阻止跨实例的竞态。

方案3:数据库事务+高隔离级别(慎用)

通过提升事务隔离级别到Serializable,或者使用UPDLOCK/HOLDLOCK查询提示,让数据库在查找时加排他锁,阻止其他线程插入符合条件的记录。

代码示例(EF Core + SQL Server):

public async Task<Country> FindOrCreateAsync(string name)
{
    using (var transaction = await _context.Database.BeginTransactionAsync(System.Data.IsolationLevel.Serializable))
    {
        try
        {
            // 使用Serializable隔离级别,确保读取时锁定范围,阻止插入
            var country = await _context.Countries
                .FirstOrDefaultAsync(p => p.Name == name);

            if (country != null)
            {
                await transaction.CommitAsync();
                return country;
            }

            country = new Country { Name = name };
            _context.Countries.Add(country);
            await _context.SaveChangesAsync();
            await transaction.CommitAsync();
            return country;
        }
        catch
        {
            await transaction.RollbackAsync();
            throw;
        }
    }
}

优点:从数据库层面彻底避免竞态;缺点:Serializable隔离级别会大幅增加数据库的锁竞争,降低并发性能,只适合并发量极低的场景,一般不推荐作为常规方案。

总结选择建议

  • 如果你是多实例部署(比如微服务集群):优先选方案1,这是最通用且可靠的方式;
  • 如果你是单实例部署:可以选方案2,避免异常;或者也可以用方案1,保持代码的通用性;
  • 方案3尽量少用,除非你必须彻底避免数据库异常,且能接受并发性能的下降。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.11 09:17:05