当行存在时,FirstOrDefault 返回 null

我遇到了一些竞争条件,其中数据库中的一行可能由两个线程同时创建。为了解决这个问题,我实现了重试,如下所示:


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 作用域。

添加但未保存的行保留在上下文中并继续尝试插入。我保留了对新行的引用并将其添加到 catch 块中:

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


潇潇雨雨
浏览 67回答 3
3回答

守着一只汪

你分享了吗DbContext?DbContext 不是线程安全的。尝试将插入操作包装在 的using块中DbContext,而不是重试:using(var context = new DbContext){  // Insert operation here}这种冲突很容易理解,但首先你需要知道,当你await调用时,线程立即返回到调用者。想象一下这个场景,您有两个线程正在运行您的代码。这是执行顺序:线程 1:FirstOrDefault返回null.线程 2:FirstOrDefault返回null.线程 1:Add运行。SQL 生成并在数据库服务器上排队。主题 1:await context.SaveChangesAsync(). 呼叫立即完成。数据库:线程 1 的调用已完成。线程2:Add运行。SQL 生成并在数据库服务器上排队。主题 2:await context.SaveChangesAsync(). 呼叫立即完成。数据库:尝试从线程 2 进行调用,但无法完成它,因为之前插入了具有相同键值的行。

侃侃无极

如果数据库中有一条记录作为val1键但val2不同,firstOrDefault()则不会返回值,并且您仍然无法插入新记录。这也可能是缓存问题。您可以尝试添加AsNoTracking()到您的查询中。

largeQ

重试不起作用,因为一旦您将条目添加到上下文并收到冲突错误,条目仍标记为已插入,因此您将在所有进一步的重试中尝试插入它。您需要使用新的上下文或将其分离才能使重试起作用。交易如果您想确保在尝试查找记录时没有人可以添加记录,那么您需要使用事务: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 -update或者insert将在单个事务中执行,这样您就不会再发生冲突。重试请注意,如果更新不是幂等的,您可能仍然存在竞争条件,但现在您将其移至数据库端:2 个线程找到一个项目,单独更新并保存。您可以按照本文所述使用并发令牌来避免此类冲突。请记住,如果您坚持重试选项,更新必须是幂等的,这意味着无论有多少线程都会更新实体 - 它将与第一次更新后相同。有一个很棒的框架Polly.NET对您来说非常方便:await Policy.Handle<DbUpdateException>()             .RetryAsync(5)             .ExecuteAsync(() => ...);我不建议在 DbContext (或其他任何东西)上使用任何进程内锁,因为这会限制您使用此逻辑运行单个进程,而当您需要高可用性时,情况并非如此。
打开App,查看更多内容
随时随地看视频慕课网APP