我正在使用 Entity Framework 7,我有一个这样的案例: 具有列的实体:Id、TakenBy,映射到 SQL Server 2018 中的表。
现在我有几个进程在同一个表上工作(由于某些原因),每个进程都会查看该表并检查是否有 TakenBy = null 的行。如果是这样,那么它通过更新 TakenBy 值来负责该行并开始执行一些操作。
问题是:如何确保,理想情况下使用实体框架而不运行存储过程或类似的东西,一旦一个进程可以看到该行已准备好被获取,该进程将获取它,并且不会覆盖另一个进程(如果它)同时已经采取了吗?
在实体框架方面,使用潜在的高度并发系统时有两个关键考虑因素。
首先是处理任何可能的缓存。理想情况下,您希望并发应用程序中的 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 检查返回为“已采取”那样频繁地发生在任何地方,但它会捕获几乎同时尝试两次更新的情况。