为了账号安全,请及时绑定邮箱和手机立即绑定

当行存在时,FirstOrDefault 返回 null

当行存在时,FirstOrDefault 返回 null

PHP
潇潇雨雨 2024-01-20 15:48:37
我遇到了一些竞争条件,其中数据库中的一行可能由两个线程同时创建。为了解决这个问题,我实现了重试,如下所示: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;
查看完整描述

3 回答

?
守着一只汪

TA贡献1872条经验 获得超3个赞

你分享了吗DbContext?DbContext 不是线程安全的。


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


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 进行调用,但无法完成它,因为之前插入了具有相同键值的行。


查看完整回答
反对 回复 2024-01-20
?
侃侃无极

TA贡献2051条经验 获得超10个赞

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

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


查看完整回答
反对 回复 2024-01-20
?
largeQ

TA贡献2039条经验 获得超7个赞

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

交易

如果您想确保在尝试查找记录时没有人可以添加记录,那么您需要使用事务:

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 (或其他任何东西)上使用任何进程内锁,因为这会限制您使用此逻辑运行单个进程,而当您需要高可用性时,情况并非如此。


查看完整回答
反对 回复 2024-01-20
  • 3 回答
  • 0 关注
  • 58 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信