.net-core - 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 的唯一实例。
如前所述,该服务的想法是去抖动索引更新。我们系统的性质使这些索引更新成批进行,如果在所有更改完成后可以通过一次索引更新来完成每次更新,则没有理由更新索引。
解决方案
推荐阅读
- nginx - 获取静态文件的正确路径以通过 nginx 为它们提供服务
- javascript - javascript for 循环:问题
- javascript - React 组件在获取新数据之前渲染旧状态
- python - 在 Macbook 上运行 Anaconda 时使用终端命令
- javascript - 自动在 CPT 中添加分类
- azure-devops - 如何获取我的 Azure DevOps 发布管道以从 Azure 存储帐户获取工件
- amazon-web-services - Elastic Beanstalk 在环境变量更新时失败
- javascript - 如何根据最大值和最小值按时间戳顺序创建对象数组?
- excel - 从 IE 升级到 EDGE
- yii2 - Yii2 队列 TTR 属性不适用