【问题标题】:FirstOrDefault returning null, while row existsFirstOrDefault 返回 null,而行存在
【发布时间】:2019-10-03 11:23:04
【问题描述】:

我有一点竞争条件,DB 中的一行可能由两个线程同时创建。为了解决这个问题,我实现了重试,如下所示:

int retries = 0;
while (true)
{                
    try
    {
        var saved = context.Table.FirstOrDefault(x => x.field1 == val1 && x.field2 == val2);

        if (saved != null)
        {
            //edits saved
        }
        else
        {
            context.Table.Add(new Table
            {
                field1 = val1,
                field2 = val2
            });
        }
        await context.SaveChangesAsync();
        return Json(true);
    }
    catch (Exception e)
    {
        if (retries >= 5)
            throw (e);
        retries++;
    }
}

不知何故,这连续 5 次失败并出现相同的错误:

Microsoft.EntityFrameworkCore.DbUpdateException:发生错误 在更新条目时。有关详细信息,请参阅内部异常。 ---> System.Data.SqlClient.SqlException:无法在其中插入重复的键行 具有唯一索引“IX_Table_field1_field2”的对象“dbo.Table”。这 重复键值为 (val1, val2)。

为什么 FirstOrDefault 会返回 null,即使该行明确存在于数据库中? 我正在使用 Microsoft.AspNetCore.All v.2.1.4

编辑:澄清。上下文未在线程之间共享。当多个 HTTP 请求同时到达时,就会发生争用。上下文被注入控制器(此代码所在的位置)。它使用默认设置注册了 AddDbContext 调用,使其 ServiceLifetime 作用域。

解决方案:Fenixil 的评论给了我必要的提示。添加但未保存的行仍保留在上下文中并不断尝试插入。我保留了对新行的引用并将其添加到 catch 块中:

context.Entry(NewRow).State = EntityState.Detached;

【问题讨论】:

  • 请投票给对你有用的答案。

标签: c# .net-core entity-framework-core


【解决方案1】:

你分享了@98​​7654322@吗? DbContext is not thread safe.

尝试将插入操作包装在DbContextusing 块中,而不是重试:

using(var context = new DbContext)
{
  // Insert operation here
}

冲突很容易理解,但首先你要知道,当你await一个调用时,线程立即返回给调用者。

想象这个场景,你有两个线程,运行你的代码。 这是执行顺序:

  1. 线程 1:FirstOrDefault 返回 null
  2. 线程 2:FirstOrDefault 返回 null
  3. 线程 1:Add 运行。 SQL 在数据库服务器上生成并排队。
  4. 线程 1:await context.SaveChangesAsync()。通话立即完成。
  5. 数据库:已完成来自线程 1 的调用。
  6. 线程 2:Add 运行。 SQL 在数据库服务器上生成并排队。
  7. 线程 2:await context.SaveChangesAsync()。通话立即完成。
  8. 数据库:尝试从线程 2 调用,但无法完成,因为之前插入了具有相同键值的行。

【讨论】:

  • 不共享。它使用默认设置注入,因此 ServiceLifetime 是 Scoped 的。目标不是完全避免比赛。我只是不明白同一个线程如何失败 5 次 - 在第一次失败后,该行应该存在并且下一个 FirstOrDefault 调用应该返回它。
【解决方案2】:

如果数据库中有一条记录以val1 为键,但val2 不同,firstOrDefault() 将不会返回值,您仍然无法插入新记录。

这也可能是缓存问题。您可以尝试将AsNoTracking() 添加到您的查询中。

【讨论】:

  • 这两个字段都在键中。来自 OnModelCreating:modelBuilder.Entity().HasKey(x => new { x.field1, x.field2 }).ForSqlServerIsClustered(false);它不是聚集的,因为另一个索引更需要它。 AsNoTracking 会完成什么?它有一些缓存行为吗?我有点需要它来跟踪,因为如果检索到该行,它可能会被更新。
  • 我认为缓存是这种情况下的问题。因为上下文没有被更新,在这种情况下,具有相同键的第一个值将被加载到内存中并被缓存。因此 AsNoTracking() 将禁用此特定实体的对象级缓存。适合您的解决方案正是这样做的。还有其他处理 ef 缓存的方法,可以刷新实体、分离实体、重新创建 dbContext、调用 GetDatabaseValues 以获取更新的数据……或者使用 AsNoTracking()。如果您需要编辑和保存实体,您可以将已编辑的实体附加到上下文中。
【解决方案3】:

重试不起作用,因为一旦您将条目添加到上下文并收到冲突错误,条目仍标记为已插入,因此您将尝试在所有进一步的重试中插入它。您需要使用新的上下文或将其分离以进行重试。

交易

如果您想确保在尝试查找时没有人可以添加记录,那么您需要use transactions

using (var context = new MyContext())
using (var transaction = context.Database.BeginTransaction(IsolationLevel.Serializable)) {
        var saved = context.Table.FirstOrDefault(x => x.field1 == val1 && x.field2 == val2);
        if (saved != null)
        {
            //edits saved
        }
        else
        {
            context.Table.Add(new Table
            {
                field1 = val1,
                field2 = val2
            });
        }
        await context.SaveChangesAsync();
        transaction.Commit()
        return Json(true);
}

我在这里使用最孤立的级别来锁定表格并防止阅读中的竞争条件。这种方法对性能有影响,如果可以接受重试,您仍然可以采用这种方法。

更新插入

如果您拥有新实体所需的所有数据,那么您可以使用 FlexLabs.Upsert - updateinsert 将在单个事务中执行,这样您就不会再发生冲突了。

重试

请注意,如果更新不是幂等的,您可能仍然有竞争条件,但现在您将其移至数据库端:2 个线程找到一个项目,单独更新并保存。您可以按照this 文章中的说明使用concurrency tokens 来避免此类冲突。请记住,如果您坚持重试选项,更新必须是幂等的,这意味着无论有多少线程都会更新实体 - 它将与第一次更新后相同。

有一个很棒的框架Polly.NET 对您来说非常方便:

await Policy.Handle<DbUpdateException>()
            .RetryAsync(5)
            .ExecuteAsync(() => ...);

我不建议在您的 DbContext(或其他任何东西)上使用任何进程内锁,因为这会限制您使用此逻辑运行单个进程,而当您需要高可用性时,情况并非如此。

【讨论】:

  • 我不想确保这一点。我想在重试中检索由另一个线程创建的行。
  • 然后在重试之间使用新的上下文:一旦你在第一次重试时添加一个条目,它将尝试在所有进一步的重试中插入它并失败并出现相同的错误。
  • 添加了幂等性部分,请注意更新。如果它对你有用,请投票给答案,我会感谢 carma :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多