处理并发冲突

您所在的位置:网站首页 朝鲜咸镜北道清津市 处理并发冲突

处理并发冲突

#处理并发冲突| 来源: 网络整理| 查看: 265

处理并发冲突 项目 07/11/2023

提示

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

在大多数情况下,数据库由多个应用程序实例并发使用,每个实例对数据分别执行修改。 在同一时间修改相同的数据时,可能会出现不一致和数据损坏,例如,当两个客户端修改同一行中以某种方式关联的不同列时。 本页讨论确保数据在发生此类并发更改时保持一致的机制。

乐观并发

EF Core 实现 乐观并发,假定并发冲突相对较少。 与 悲观 方法(即先锁定数据,然后才继续修改数据)不同,乐观并发不需要锁定,而是安排数据修改在保存时失败(如果数据自查询后已更改)。 此并发故障将报告给应用程序,应用程序可能会通过对新数据重试整个操作来相应地处理它。

在 EF Core 中,乐观并发是通过将属性配置为 并发令牌来实现的。 在查询实体时加载和跟踪并发令牌,就像任何其他属性一样。 然后,在 期间 SaveChanges()执行更新或删除操作时,数据库上的并发令牌值与 EF Core 读取的原始值进行比较。

为了了解其工作原理,假设我们SQL Server,并使用特殊Version属性定义典型的 Person 实体类型:

public class Person { public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } [Timestamp] public byte[] Version { get; set; } }

在 SQL Server 中,这会配置一个并发令牌,该令牌在每次更改行时都会在数据库中自动更改 (更多详细信息,请参阅) 。 完成此配置后,让我们来看看一个简单的更新操作会发生什么情况:

var person = context.People.Single(b => b.FirstName == "John"); person.FirstName = "Paul"; context.SaveChanges(); 在第一步中,从数据库加载人员;这包括并发令牌,EF 将像往常一样跟踪该令牌以及其余属性。 然后,以某种方式修改 Person 实例 - 我们将更改 FirstName 属性。 然后,我们指示 EF Core 保留修改。 由于配置了并发令牌,EF Core 会将以下 SQL 发送到数据库: UPDATE [People] SET [FirstName] = @p0 WHERE [PersonId] = @p1 AND [Version] = @p2;

请注意,除了 PersonId WHERE 子句中的 之外,EF Core 还为 Version 添加了条件;仅当 Version 列自查询后未更改时,才会修改该行。

在正常 (“乐观”) 的情况下,不会发生并发更新,并且 UPDATE 成功完成,修改行:数据库向 EF Core 报告一行受 UPDATE 影响,如预期的那样。 但是,如果发生并发更新,则 UPDATE 无法找到任何匹配的行和零个受影响的报告。 因此,EF Core 的 SaveChanges() 会引发 , DbUpdateConcurrencyException应用程序必须正确捕获和处理。 下面在 解决并发冲突下详细介绍了执行此操作的方法。

虽然上述示例讨论了对现有实体的 更新 。 尝试删除已并发修改的行时,EF 也会引发DbUpdateConcurrencyException。 但是,添加实体时永远不会引发此异常;虽然如果插入具有相同键的行,数据库确实可能会引发唯一约束冲突,但这会导致引发提供程序特定的异常,而不是 DbUpdateConcurrencyException。

本机数据库生成的并发令牌

在上面的代码中,我们使用 [Timestamp] 特性将属性映射到SQL Serverrowversion列。 由于 rowversion 更新行时会自动更改,因此它作为保护整个行的最小工作量并发令牌非常有用。 将SQL Serverrowversion列配置为并发令牌如下所示:

数据注释 Fluent API public class Person { public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } [Timestamp] public byte[] Version { get; set; } } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .Property(p => p.Version) .IsRowVersion(); }

rowversion上面显示的类型是一个特定于SQL Server的功能;有关设置自动更新并发令牌的详细信息因数据库而异,并且某些数据库在 (完全不支持这些功能,例如 SQLite) 。 有关确切的详细信息,请参阅提供商文档。

应用程序管理的并发令牌

可以在应用程序代码中管理并发令牌,而不是让数据库自动管理并发令牌。 这允许在不存在本机自动更新类型的数据库(如 SQLite)上使用乐观并发。 但是,即使在SQL Server,应用程序托管的并发令牌也可以对导致重新生成令牌的列更改提供精细控制。 例如,你可能有一个属性包含一些缓存或不重要的值,并且不希望对该属性进行更改来触发并发冲突。

下面将 GUID 属性配置为并发令牌:

数据注释 Fluent API public class Person { public int PersonId { get; set; } public string FirstName { get; set; } [ConcurrencyCheck] public Guid Version { get; set; } } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .Property(p => p.Version) .IsConcurrencyToken(); }

由于此属性不是数据库生成的,因此每当保留更改时,都必须在应用程序中分配它:

var person = context.People.Single(b => b.FirstName == "John"); person.FirstName = "Paul"; person.Version = Guid.NewGuid(); context.SaveChanges();

如果希望始终分配新的 GUID 值,可以通过侦听器执行此操作SaveChanges。 但是,手动管理并发令牌的一个优点是可以精确控制其重新生成时间,以避免不需要的并发冲突。

解决并发冲突

无论并发令牌是如何设置的,若要实现乐观并发,应用程序必须正确处理发生并发冲突并 DbUpdateConcurrencyException 引发的情况;这称为 解决并发冲突。

一种选择是仅通知用户更新由于发生冲突的更改而失败;然后,用户可以加载新数据并重试。 或者,如果应用程序正在执行自动更新,只需在重新查询数据后循环并立即重试。

解决并发冲突的一种更复杂的方法是将挂起的更改与数据库中的新值 合并 。 合并哪些值的确切详细信息取决于应用程序,该过程可能由用户界面指示,其中显示了两组值。

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

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

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

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

在下面的示例中,将 Person.FirstName 和 Person.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] = ; } // 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); } } } } 使用隔离级别进行并发控制

通过并发令牌实现的乐观并发不是确保数据在发生并发更改时保持一致的唯一方法。

确保一致性的一种机制是 可重复读取 事务隔离级别。 在大多数数据库中,此级别保证事务在数据库中看到的数据与事务启动时一样,而不会受到任何后续并发活动的影响。 以上述基本示例为例,当我们查询 Person 以某种方式更新它时,数据库必须确保在事务完成之前没有其他事务干扰该数据库行。 根据数据库实现情况,这可以通过以下两种方式之一进行:

查询行时,事务将对其进行共享锁。 任何尝试更新该行的外部事务都会被阻止,直到事务完成。 这是一种悲观锁定形式,由SQL Server“可重复读取”隔离级别实现。 数据库允许外部事务更新行,而不是锁定,但当你自己的事务尝试更新行时,将引发“序列化”错误,指示发生了并发冲突。 这是一种乐观锁定形式(与 EF 的并发令牌功能不同),它由 SQL Server 快照 隔离级别以及 PostgreSQL 可重复读取隔离级别实现。

请注意,“可序列化”隔离级别提供与可重复读取 (相同的保证,并在) 添加其他保证,因此它的工作方式与上述相同。

使用更高的隔离级别来管理并发冲突更简单,不需要并发令牌,并提供其他优势:例如,可重复读取可确保事务始终在事务内查询中看到相同的数据,从而避免不一致。 但是,此方法确实有其缺点。

首先,如果数据库实现使用锁定来实现隔离级别,则尝试修改同一行的其他事务必须阻止整个事务。 这可能会对并发性能产生不利影响, (使事务保持短!) ,但请注意,EF 的机制会引发异常并强制你重试,这也会产生影响。 这适用于SQL Server可重复读取级别,但不适用于不锁定查询行的快照级别。

更重要的是,此方法需要事务跨所有操作。 例如,如果查询 Person 以便向用户显示其详细信息,然后等待用户进行更改,则事务必须保持活动状态可能很长一段时间,在大多数情况下应避免这种情况。 因此,当所有包含的操作立即执行且事务不依赖于可能会增加其持续时间的外部输入时,此机制通常适用。

其他资源

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



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3