并发冲突
并发冲突一般发生于多个操作同时对应于数据库表中的一行相同的记录。比方说,小明正在操作Id为1的person表中的一行记录,将这条记录的name属性更改为了“shit”,但这个时候他还没用savechanges,同时,小刘也在操作这个Id为1的行(他们操纵了相同的记录),将某一个属性做了修改。这种情况下,sqlserver的默认处理不会爆出异常,按照操作的先后顺序对这条记录进行了commit。但是这个一般情况下这样的操作会产生业务上的错误问题,比如个人账户的金额操作等。为了避免这样的操作产生,我们要给表中的关键字段设置一个并发标记,这样,当我们出现上述问题时,数据库爆出相应异常(如DbUpdateConcurrencyException)来对数据进行保护。
SqlServer的并发冲突特性
sqlserver有两个特性来记录并发冲突:
1、TimeStamp,在efcore中的表示是一个TimeStampAttribute属性,FluentApi表示为:
builder.Entity<User>(it => { it.Property(it => it.ConcurrencyStamp).IsRowVersion(); });
TimeStamp会自动变为一个并发标记
2、并发标记,用ConcurrencyCheckAttribute来表示,FluentApi表示为:
it.Property(it => it.ConcurrencyStamp).IsConcurrencyToken();
对于TimeStamp来说,会自动记录并发冲突,当并发冲突产生时,这个值会自动变更。对于ConcurrencyCheck来说,不会自动记录,除非你手动更改这个标记为ConcurrencyCheck特性的属性的值。
Efcore的并发冲突解决办法
EntityEntry记录了从数据库获取的实体的状态。
在EntityEntry中有三组值可用于帮助解决并发冲突:
- “当前值” 是应用程序尝试写入数据库的值(entry.CurrentValues)。
- “原始值” 是在进行任何编辑之前最初从数据库中检索的值(entry.OriginalValues)。
- “数据库值” 是当前存储在数据库中的值(entry.GetDataBaseValues)。
处理并发冲突的常规方法是:
- 在
SaveChanges
期间捕获DbUpdateConcurrencyException
。 - 使用
DbUpdateConcurrencyException.Entries
为受影响的实体准备一组新更改(一组EntityEntry)。 - 刷新并发令牌的原始值以反映数据库中的当前值(entry.OriginalValues.SetValues(......))。
- 重试该过程,直到不发生任何冲突。
下面是MSDN上的一段代码,这段代码用于解决并发冲突:
using (var context = serviceProvider.CreateScope().ServiceProvider.GetRequiredService<DbContext>()) { // Fetch a person from database and change phone number var person = context.Users.Single(p => p.Id == 1); person.Name = "zhengyanan"; // Change the person's name in the database to simulate a concurrency conflict context.Database.ExecuteSqlRaw( "UPDATE dbo.Users SET ConcurrencyStamp = 'changed',Name='pangxihe' WHERE Id = 1"); var saved = false; while (!saved) { try { // Attempt to save changes to the database context.SaveChanges(); saved = true; } catch (DbUpdateConcurrencyException ex) { foreach (EntityEntry entry in ex.Entries) { if (entry.Entity is User) { var proposedValues = entry.CurrentValues; var databaseValues = entry.GetDatabaseValues(); var originalValues = entry.OriginalValues; foreach (var property in proposedValues.Properties) { var proposedValue = proposedValues[property]; var databaseValue = databaseValues[property]; var originalValue = originalValues[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); } } } } }