SELECT 和 UPDATE 作为实体框架中的原子操作

问题描述 投票:0回答:1

我正在使用 Entity Framework 7,我有一个这样的案例: 具有列的实体:Id、TakenBy,映射到 SQL Server 2018 中的表。

现在我有几个进程在同一个表上工作(由于某些原因),每个进程都会查看该表并检查是否有 TakenBy = null 的行。如果是这样,那么它通过更新 TakenBy 值来负责该行并开始执行一些操作。

问题是:如何确保,理想情况下使用实体框架而不运行存储过程或类似的东西,一旦一个进程可以看到该行已准备好被获取,该进程将获取它,并且不会覆盖另一个进程(如果它)同时已经采取了吗?

c# .net sql-server entity-framework
1个回答
0
投票

在实体框架方面,使用潜在的高度并发系统时有两个关键考虑因素。

首先是处理任何可能的缓存。理想情况下,您希望并发应用程序中的 DbContext 尽可能“新鲜”,因此例如围绕操作为 DbContext 提供一个

using
范围。这可以确保当您去获取行时,DbContext 不会跟踪任何可能稍微过时的引用。然而,依赖注入之类的东西管理 DbContext 生命周期范围是很常见的。

可以使用以下方法缓解这种情况:(其中“Row”代表您要同时拉取的实体)

var row = _context.Rows.Local.FirstOrDefault(x => x.Id == rowId);
if (row != null)
    await _context.Entry(row).ReloadAsync(); // Force reloading from DB.
else
    row = _context.Rows.Single(x => x.Id == rowId); // Fetch from DB.

一旦您拥有并想要保留它,请设置您的标志并立即保存更改:

if(row.TakenBy == null)
{
   row.TakenBy = myTakenById;
   await _context.SaveChangesAsync();
} 
else
{ // TODO: Handle scenario where row cannot be reserved...
}

第二个考虑因素是并发性。仍然有一个非常短的竞争窗口,其中两个进程读取该行并都尝试更新 TakenBy。这必须在数据库的帮助下进行检测和强制执行。对于 SQL Server,这是 RowVersion/Timestamp 列。通过向表中添加 Timestamp 类型列并告诉 EF 它是 RowVerion/Concurrency 标记,在保存更改时数据库会自动更新该标记。然后,如果发生两次读取,并且第二次调用在第一次调用发生更改后尝试更新该行,则并发标记不匹配,因此更新被拒绝。

if(row.TakenBy == null)
{
   row.TakenBy = myTakenById;
   try
   {
       await _context.SaveChangesAsync();
   }
   catch (DbConcurrencyUpdateException)
   {
      // TODO: Handle scenario where missed reservation window.
   }
else
{ // TODO: Handle scenario where row cannot be reserved...
}

该异常不应像 TakenBy 检查返回为“已采取”那样频繁地发生在任何地方,但它会捕获几乎同时尝试两次更新的情况。

© www.soinside.com 2019 - 2024. All rights reserved.