首页 > 解决方案 > DbUpdateConcurrencyException 与 RemoveRange 上的 EntityFramework

问题描述

我们正在经历一些对 EntityFramework 非常好奇的事情,我们很难调试和理解。

我们有一项服务,可以消除索引更新的抖动,这样我们将在进行所有更改时更新索引,而不是每次更改。代码看起来像这样:

var messages = await _debouncingDbContext.DebounceMessages
            .Where(message => message.ElapsedTime <= now)
            .Take(_internalDebouncingOptionsMonitor.CurrentValue.ReturnLimit)
            .ToListAsync(stoppingToken)
            .ConfigureAwait(false);

if (!messages.Any())
    return;

var tasks = messages.Select(m =>
           _messageSession.Send(m.ReplyTo, new HandleDebouncedMessage {Message = m.OriginalMessage}))
            .ToList();
try
{
    await Task.WhenAll(tasks).ConfigureAwait(false);
}
catch (Exception e)
{
    //Exception handling
}

_debouncingDbContext.DebounceMessages.RemoveRange(messages);
await _debouncingDbContext.SaveChangesAsync().ConfigureAwait(false);

当它正在运行时,我们有另一个线程可以更新条目上的ElapsedTime。如果在去抖动计时器到期之前出现新事件,则会发生这种情况。

我们所经历的是await _debouncingDbContext.SaveChangesAsync().ConfigureAwait(false); 抛出DbUpdateConcurrencyException

结果是条目没有被删除,因此在初始查询中被一遍又一遍地查询。这导致我们的索引更新呈指数增长,其中相同的少数项目被一遍又一遍地更新。最终,系统死亡。

我们现在唯一的解决方法是重新启动服务。完成后,下一次迭代会很好地提取麻烦的消息,并且一切都会恢复正常。

我很难理解这是怎么发生的。似乎 dbcontext 认为这些条目已被删除,而实际上并没有。DBContext 不知何故与数据库状态分离。

我无法理解这是如何发生的,当数据库条目上唯一可能被更改的是时间戳而不是删除它的实际 ID 时。

编辑 11 月 18 日。

添加更多上下文。

数据库模型,如下所示:

[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Key { get; set; }
public string OriginalMessage { get; set; }
public string ReplyTo { get; set; }
public DateTimeOffset ElapsedTime { get; set; }

在 dbcontext 上配置的唯一内容是两个索引:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    modelBuilder.Entity<DebounceMessageWrapper>()
        .HasIndex(m => m.Key);
    modelBuilder.Entity<DebounceMessageWrapper>()
        .HasIndex(m => m.ElapsedTime);
}

流程非常简单。

我们有一个 dotnet 托管服务,它从 dotnet 核心扩展了抽象BackgroundService类。这在while(!stoppingToken.IsCancellationRequested)循环中运行,上面的初始代码和每个循环中的Task.Delay(Y)。上述所有代码的作用是查询所有ElapsedTime大于允许时间跨度的消息。对于这些消息中的每一个,它会将其返回给ReplyTo,然后删除所有相应的数据库条目。正是这个删除失败了。

其次,我们有一个 MessageHandler 监听 RabbitMQ 上的事件。这会在主机上的每个物理核心生成一个线程。这些线程中的每一个都接收消息并根据数据库模型上的查找消息。如果消息已经存在,则更新ElapsedTime,如果不存在,则将消息插入数据库中。

这为我们提供了 X+1 个线程,其中 X 等于主机上的物理内核数,这可能会改变数据库。这些线程中的每一个都使用它自己的范围,从而使用 DBContext 的唯一实例。

如前所述,该服务的想法是去抖动索引更新。我们系统的性质使这些索引更新成批进行,如果在所有更改完成后可以通过一次索引更新来完成每次更新,则没有理由更新索引。

标签: .net-coreentity-framework-core

解决方案


推荐阅读