处理并发冲突Handling Concurrency Conflicts

备注

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

提示

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

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

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

并发控制在 EF Core 中的工作原理How concurrency control works in EF Core

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

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

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

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

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

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

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

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

解决并发冲突Resolving concurrency conflicts

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

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

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

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

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

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

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

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

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

  1. using (var context = new PersonContext())
  2. {
  3. // Fetch a person from database and change phone number
  4. var person = context.People.Single(p => p.PersonId == 1);
  5. person.PhoneNumber = "555-555-5555";
  6. // Change the person's name in the database to simulate a concurrency conflict
  7. context.Database.ExecuteSqlRaw(
  8. "UPDATE dbo.People SET FirstName = 'Jane' WHERE PersonId = 1");
  9. var saved = false;
  10. while (!saved)
  11. {
  12. try
  13. {
  14. // Attempt to save changes to the database
  15. context.SaveChanges();
  16. saved = true;
  17. }
  18. catch (DbUpdateConcurrencyException ex)
  19. {
  20. foreach (var entry in ex.Entries)
  21. {
  22. if (entry.Entity is Person)
  23. {
  24. var proposedValues = entry.CurrentValues;
  25. var databaseValues = entry.GetDatabaseValues();
  26. foreach (var property in proposedValues.Properties)
  27. {
  28. var proposedValue = proposedValues[property];
  29. var databaseValue = databaseValues[property];
  30. // TODO: decide which value should be written to database
  31. // proposedValues[property] = <value to be saved>;
  32. }
  33. // Refresh original values to bypass next concurrency check
  34. entry.OriginalValues.SetValues(databaseValues);
  35. }
  36. else
  37. {
  38. throw new NotSupportedException(
  39. "Don't know how to handle concurrency conflicts for "
  40. + entry.Metadata.Name);
  41. }
  42. }
  43. }
  44. }
  45. }