EF Core 并发控制
Posted rajesh
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了EF Core 并发控制相关的知识,希望对你有一定的参考价值。
并发令牌
将属性配置为并发令牌来实现乐观并发控制
数据注解
使用数据注解 ConcurrencyCheckAttribute
将属性配置为并发令牌
public class Person
[Key]
public int Id get; set;
[ConcurrencyCheck]
[MaxLength(32)]
public string FirstName get; set;
[MaxLength(32)]
public string LastName get; set;
Fluent Api
使用 Fluent Api 配置属性为并发令牌
protected override void OnModelCreating(ModelBuilder builder)
base.OnModelCreating(builder);
builder.Entity<Person>().Property(s => s.FirstName).IsConcurrencyToken();
时间戳/行版本
数据库新增或更新时会生成一个新的值赋予给配置为时间戳的属性,此属性也被视作为并发令牌。这样做可以确保你在查询一行数据后(ChangeTracker),尝试更新此行,但在此时数据已经被其他人修改,会返回一个异常。
数据注解
使用数据注解 TimestampAttribute
将属性标记为时间戳
public class Person
[Key]
public int Id get; set;
[ConcurrencyCheck]
[MaxLength(32)]
public string FirstName get; set;
[MaxLength(32)]
public string LastName get; set;
1597730687
public byte[] Timestamp get; set;
### Fluent Api
使用 Fluent Api 标志属性为时间戳
protected override void OnModelCreating(ModelBuilder builder)
base.OnModelCreating(builder);
builder.Entity<Person>()
.Property(s => s.FirstName).IsConcurrencyToken();
builder.Entity<Person>()
.Property(s => s.Timestamp).IsRowVersion();
数据迁移脚本
添加迁移
protected override void Up(MigrationBuilder migrationBuilder)
migrationBuilder.AddColumn<byte[]>(
name: "Timestamp",
table: "People",
rowVersion: true,
nullable: true);
看看 ModelSnapshot
生成的迁移
modelBuilder.Entity("LearningEfCore.Person", b =>
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("FirstName")
.IsConcurrencyToken()
.HasMaxLength(32);
b.Property<string>("LastName")
.HasMaxLength(32);
b.Property<byte[]>("Timestamp")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate();
b.HasKey("Id");
b.ToTable("People");
);
ValueGeneratedOnAddOrUpdate
正是表示数据在插入与更新时自动生成
处理并发冲突
EF Core 如何检测并发冲突
配置实体属性为并发令牌来实现乐观并发控制:当更新或者删除操作在 SaveChanges
过程中出现时,EF Core 将会把数据库中并发令牌的值与 ChangeTracker
中跟踪的值进行比较。
- 如果两值相同,则操作可以完成
- 如果不同, EF Core 会假设其他用户执行了与当前相冲突的操作,并会终止当前的事务。
其他用户执行的与当前用户相冲突的操作称为并发冲突.
数据库提供者复制实现并发令牌的比较.
在关系型数据库中, EF Core 在更新与删除操作中会通过在 WHERE
条件语句中包含对并发令牌的比较,当语句执行后,EF Core 会读取影响的行数,如果没有任何行受影响,则会检测到并发冲突, EF Core 也会抛出 DbUpdateConcurrencyException
下面我们通过例子来演示一下:
先创建一个 Api 控制器:
[Route("api/[controller]")]
[ApiController]
public class DefaultController : ControllerBase
private readonly TestDbContext _db;
public DefaultController(TestDbContext db)
_db = db;
[HttpPost]
public async Task<Person> Add([FromBody]Person person)
var entry = await _db.People.AddAsync(person);
await _db.SaveChangesAsync();
return entry.Entity;
[HttpPut]
public async Task<Person> Update([FromBody]Person current)
var original = await _db.People.FindAsync(current.Id);
original.FirstName = current.FirstName;
original.LastName = current.LastName;
await _db.SaveChangesAsync();
return original;
以 POST 方式请求 http://localhost:5000/api/default
, body 使用 json 串:
"firstName": "James",
"lastName": "Rajesh"
返回:
"id": 1,
"firstName": "James",
"lastName": "Rajesh",
"timestamp": "AAAAAAAAB9E="
可以看到 timestamp
如我们所愿,自动生成了一个并发令牌
下面我们尝试在 SaveChanges
时修改数据库中的值:
在 Update
接口中的 await _db.SaveChangesAsync();
此行下断点。
修改 Request Body 为:
"id": 1,
"firstName": "James1",
"lastName": "Rajesh1",
"timestamp": "AAAAAAAAB9E="
使用 PUT 方式请求 http://localhost:5000/api/default
, 命中断点后,
修改数据库中 LastName
的值为 Rajesh2,然后 F10,我们会得到如下并发异常:
DbUpdateConcurrencyException: Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded.
此时有人可能会疑惑了, EF Core 是如何检测到 LastName
变更了的呢? 其实不然, 是我们在修改数据库中数据的时候, RawVersion 列 Timestamp
自动会更新。而且每一次我们使用 EF Core 更新的时候, 产生的语句是这样的(通过控制台日志可以看到):
Executed DbCommand (68ms) [Parameters=[@p2=‘?‘ (DbType = Int32), @p0=‘?‘ (Size = 32), @p3=‘?‘ (Size = 32), @p1=‘?‘ (Size = 32), @p4=‘?‘ (Size = 8) (DbType = Binary)], CommandType=‘Text‘, CommandTimeout=‘30‘] SET NOCOUNT ON; UPDATE [People] SET [FirstName] = @p0, [LastName] = @p1 WHERE [Id] = @p2 AND [FirstName] = @p3 AND 1597730687 = @p4;
这里会使用 WHERE
条件进行判断 Timestamp
是否一致
下面去掉 Timestamp
列, 留下标志为 ConcurrencyToken
的 FirstName
使用 PUT 方式请求 http://localhost:5000/api/default
, body 为:
"id": 1,
"firstName": "James6",
"lastName": "Rajesh11"
再来在 SaveChanges
的时候修改数据库中对应记录的 LastName
的值为 Rajesh19 , 此时没报错,返回值为:
"id": 1,
"firstName": "James6",
"lastName": "Rajesh11"
数据库的值也被修改为 Rajesh11. 说明这里没有检测到并发,下面我们尝试修改 FirstName
为 James12, 同时在 SaveChanges
时修改为 Rajesh13, 此时就检测到了并发冲突, 我们看控制台的语句为:
Executed DbCommand (63ms) [Parameters=[@p1=‘?‘ (DbType = Int32), @p0=‘?‘ (Size = 32), @p2=‘?‘ (Size = 32)], CommandType=‘Text‘, CommandTimeout=‘30‘] SET NOCOUNT ON; UPDATE [People] SET [FirstName] = @p0 WHERE [Id] = @p1 AND [FirstName] = @p2;
看得到这里会判断 FirstName
与 ChangeTracker
中的值进行比较,执行之后没有受影响的行,所以才会检测到并发冲突。
EF Core 如何处理并发冲突
先来了解下帮助解决并发冲突的三组值:
- Current values 当前值: 应用程序尝试写入数据库的值
- Original values 原始值: 被 EF Core 从数据库检索出来的值,位于任何更新操作之前的值
- Database values 数据库值: 当前数据库实际存储的值
当 SaveChanges
时,如果捕获了 DbUpdateConcurrencyException
, 说明发生了并发冲突,使用 DbUpdateConcurrencyException.Entries
为受影响的实体准备一组新值,重新获取数据库中的值的并发令牌来刷新 Original values
, 然后重试直到没有任何冲突产生。
[HttpPut]
public async Task<Person> Update([FromBody]Person current)
Person original = null;
try
original = await _db.People.FindAsync(current.Id);
original.FirstName = current.FirstName;
original.LastName = current.LastName;
await _db.SaveChangesAsync();
catch (DbUpdateConcurrencyException e)
foreach (var entry in e.Entries)
var currentValues = entry.CurrentValues;
var databaseValues = await entry.GetDatabaseValuesAsync();
if (entry.Entity is Person person)
// 更新什么值取决于实际需要
person.FirstName = currentValues[nameof(Person.FirstName)]?.ToString();
person.LastName = currentValues[nameof(Person.LastName)]?.ToString();
// 这步操作是为了刷新当前 Tracker 的值, 为了通过下一次的并发检查
entry.OriginalValues.SetValues(databaseValues);
await _db.SaveChangesAsync();
return original;
这步操作也可以加入重试策略.
此时,即使在 SaveChange
的时候更新了数据库中的值(或者其他用户修改了同一实体的值),触发了并发冲突,也可以解决冲突修改为我们想要的数据。
代码地址 https://github.com/RajeshJs/Learning/tree/master/LearningEfCore
以上是关于EF Core 并发控制的主要内容,如果未能解决你的问题,请参考以下文章
Entity Framework Core 数据保存原理详解
Entity Framework Core 数据保存原理详解