处理并发冲突

注意

此页面记录了并发在 EF Core 中的工作原理以及如何处理应用程序中的并发冲突。 有关如何配置模型中的并发令牌的详细信息,请参阅并发令牌

提示

可在 GitHub 上查看此文章的示例

数据库并发指的是多个进程或用户同时访问或更改数据库中的相同数据的情况。 并发控制指的是用于在发生并发更改时确保数据一致性的特定机制。

EF Core 实现乐观并发控制,这意味着它将允许多个进程或用户独立进行更改而不产生同步或锁定的开销。 在理想情况下,这些更改将不会相互干扰,因此都能够成功。 在最坏的情况下,两个或更多进程将尝试进行冲突更改,其中只有一个进程应该成功。

并发控制在 EF Core 中的工作原理

配置为并发令牌的属性用于实现乐观并发控制:每当在 SaveChanges 期间执行更新或删除操作时,会将数据库上的并发令牌值与通过 EF Core 读取的原始值进行比较。

  • 如果这些值匹配,则可以完成该操作。
  • 如果这些值不匹配,EF Core 会假设另一个用户已执行冲突操作,并中止当前事务。

另一个用户已执行与当前操作冲突的操作的情况称为并发冲突

数据库提供程序负责实现并发令牌值的比较。

在关系数据库上,EF Core 包括对任何 UPDATEDELETE 语句的 WHERE 子句中的并发令牌值的检查。 执行这些语句后,EF Core 会读取受影响的行数。

如果未影响任何行,将检测到并发冲突,并且 EF Core 会引发 DbUpdateConcurrencyException

例如,我们可能希望将 Person 上的 LastName 配置为并发令牌。 则针对用户的任何更新操作将包括 WHERE 子句中的并发检查:

UPDATE [Person] SET [FirstName] = @p1
WHERE [PersonId] = @p0 AND [LastName] = @p2;

解决并发冲突

继续前面的示例,如果一个用户尝试保存对 Person 所做的某些更改,但另一个用户已更改 LastName,则将引发异常。

此时,应用程序可能只需通知用户由于发生冲突更改而导致更新未成功,然后继续操作。 但可能需要提示用户确保此记录仍表示同一实际用户并重试该操作。

此过程是解决并发冲突的一个示例。

解决并发冲突涉及将当前 DbContext 中挂起的更改与数据库中的值进行合并。 要合并的值将根据应用程序的不同而有所不同,并且可能由用户输入指示。

有三组值可用于帮助解决并发冲突:

  • “当前值”是应用程序尝试写入数据库的值。
  • “原始值”是在进行任何编辑之前最初从数据库中检索的值。
  • “数据库值”是当前存储在数据库中的值。

处理并发冲突的常规方法是:

  1. SaveChanges 期间捕获 DbUpdateConcurrencyException
  2. 使用 DbUpdateConcurrencyException.Entries 为受影响的实体准备一组新更改。
  3. 刷新并发令牌的原始值以反映数据库中的当前值。
  4. 重试该过程,直到不发生任何冲突。

在下面的示例中,将 Person.FirstNamePerson.LastName 设置为并发令牌。 在包括应用程序特定逻辑以选择要保存的值的位置处有一条 // TODO: 注释。

using var context = new PersonContext();
// Fetch a person from database and change phone number
var person = context.People.Single(p => p.PersonId == 1);
person.PhoneNumber = "555-555-5555";

// Change the person's name in the database to simulate a concurrency conflict
context.Database.ExecuteSqlRaw(
    "UPDATE dbo.People SET FirstName = 'Jane' WHERE PersonId = 1");

var saved = false;
while (!saved)
{
    try
    {
        // Attempt to save changes to the database
        context.SaveChanges();
        saved = true;
    }
    catch (DbUpdateConcurrencyException ex)
    {
        foreach (var entry in ex.Entries)
        {
            if (entry.Entity is Person)
            {
                var proposedValues = entry.CurrentValues;
                var databaseValues = entry.GetDatabaseValues();

                foreach (var property in proposedValues.Properties)
                {
                    var proposedValue = proposedValues[property];
                    var databaseValue = databaseValues[property];

                    // TODO: decide which value should be written to database
                    // proposedValues[property] = <value to be saved>;
                }

                // Refresh original values to bypass next concurrency check
                entry.OriginalValues.SetValues(databaseValues);
            }
            else
            {
                throw new NotSupportedException(
                    "Don't know how to handle concurrency conflicts for "
                    + entry.Metadata.Name);
            }
        }
    }
}

其他资源

有关包含冲突检测的 ASP.NET Core 示例,请参阅 EF Core 中的冲突检测