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 列, 留下标志为 ConcurrencyTokenFirstName

使用 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;

看得到这里会判断 FirstNameChangeTracker 中的值进行比较,执行之后没有受影响的行,所以才会检测到并发冲突。

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 数据保存原理详解

如何在 .NET Core Web API 中配置并发?

EF Core下利用Mysql进行数据存储在并发访问下的数据同步问题

EF实体框架之CodeFirst七

一文带你更方便的控制 goroutine