前言
介绍EF Core如何处理多个用户同时更新同一实体数据时出现的冲突。
一、并发冲突的处理方式
1. 悲观并发(锁定)
在从数据库读取一行内容之前,请求锁定为只读或更新访问 如果将一行锁定为更新访问,则其他用户无法将该行锁定为只读或更新访问,因为他们得到的是正在更改的数据的副本。 如果将一行锁定为只读访问,则其他人也可将其锁定为只读访问,但不能进行更新。 缺点: 编程复杂; 性能消耗; 有的数据库管理系统不支持; Entity Framework Core 未提供对它的内置支持.
2. 乐观并发
乐观并发允许发生并发冲突,并在并发冲突发生时作出正确反应.
本文只介绍乐观并发的处理方式
二、乐观并发的处理步骤
按照并发冲突检测 -> 触发异常 -> 处理异常 的步骤进行处理. 在关系数据库中,EF Core 会从 UPDATE 和 DELETE 语句的 WHERE 子句中查看并发标记的值,以检测并发冲突。 因此必须将模型配置为启用冲突检测, EF 提供两种使用并发标记的方法:
1.在模型的属性上应用并发标记(不推荐)
将 [ConcurrencyCheck] 应用于模型上的属性。 代码如下(示例):
[Table("task_test")]
public class TaskTest {
public int Id { get; set; }
public string? Name { get; set; }
[ConcurrencyCheck]
public int Value { get; set; }
}
using (var context = new TaskContext())
{
var task = context.TaskTests.Single(p => p.Id == 1);
context.Database.ExecuteSqlRaw(
$"UPDATE task_test SET value = {task.Value + 1} WHERE Id = 1");
task.Value = task.Value + 2;
var saved = false;
while (!saved)
{
try
{
context.SaveChanges();
saved = true;
}
catch (DbUpdateConcurrencyException ex)
{
foreach (var entry in ex.Entries)
{
if (entry.Entity is TaskTest)
{
var proposedValues = entry.CurrentValues;
var databaseValues = entry.GetDatabaseValues();
foreach (var property in proposedValues.Properties)
{
var proposedValue = proposedValues[property];
var databaseValue = databaseValues[property];
Console.WriteLine($"属性名: {property.Name} 准备写入: {proposedValue} 数据库中: {databaseValue}");
}
entry.OriginalValues.SetValues(databaseValues);
}
else
{
throw new NotSupportedException(
"Don't know how to handle concurrency conflicts for "
+ entry.Metadata.Name);
}
}
}
}
}
2.在模型中新增时间戳作为并发标记(推荐)
新增属性用[Timestamp]标记 代码如下(示例):
[Table("task_row")]
public class TaskRow
{
public int Id { get; set; }
public string? Name { get; set; }
public int Value { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
}
using var context = new TaskContext();
var taskRow = context.TaskRows.Single(p => p.Id == 1);
context.Database.ExecuteSqlRaw(
$"UPDATE task_row SET value = {taskRow.Value + 1} WHERE Id = 1");
taskRow.Value = taskRow.Value + 2;
var saved = false;
while (!saved)
{
try
{
await context.SaveChangesAsync();
saved = true;
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValue = (TaskRow)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
var databaseValue = (TaskRow)databaseEntry.ToObject();
Console.WriteLine($"Proposed: {clientValue.Value} DB: {databaseValue.Value}");
context.Entry(taskRow).Property("RowVersion").OriginalValue = (byte[])databaseValue.RowVersion;
}
}
}
|