如何在 EF Core 中使用乐观并发控制

如何在 EF Core 中使用乐观并发控制 中使用乐观并发控制 Core 如何在 EF

iamge

什么是乐观并发控制?

乐观并发控制是一种处理并发访问的数据的方法,它基于一种乐观的假设,即认为并发访问的数据冲突的概率很低。在乐观并发控制中,系统不会立即对并发访问的数据进行加锁,而是在数据被修改时,再检查是否有其他并发操作已经修改了数据。如果检测到冲突,系统 再采取相应的措施来解决冲突。

EF Core 内置了使用并发令牌列实现的乐观并发控制,所谓的并发令牌列通常就是被并发操作影响的列。请看本文是如何在 EF Core 中使用乐观并发控制的……

使用步骤

  1. 创建一个 Asp.net console 项目,并从 Nuget 引用 EF 相关的包

    Microsoft.EntityFrameworkCore.SqlServer
    Microsoft.EntityFrameworkCore.Tools

  2. 配置并发冲突列

    using Microsoft.EntityFrameworkCore;
    using Microsoft.EntityFrameworkCore.Metadata.Builders;
    
    class HouseConfig : IEntityTypeConfiguration<House>
    {
    	public void Configure(EntityTypeBuilder<House> builder)
    	{
    		builder.ToTable("T_Houses");
    		builder.Property(p => p.Name).IsUnicode().IsRequired();
    		// 把 Owner 列配置为并发令牌
    		builder.Property(p => p.Owner).IsConcurrencyToken();
    	}
    }
    
  3. 在 Program.cs 编写以下代码:

    using Microsoft.EntityFrameworkCore;
    
    Console.WriteLine("请输入您的姓名");
    string name = Console.ReadLine()!;
    using TestDbContext ctx = new TestDbContext();
    
    // 1.获取数据
    var h1 = await ctx.Houses.SingleAsync(h => h.Id == 1);
    if (string.IsNullOrEmpty(h1.Owner))
    {
    	// 2.延迟5秒,方便测试
    	await Task.Delay(5000);
    
    	// 3.更新数据
    	h1.Owner = name;
    	try
    	{
    		await ctx.SaveChangesAsync();
    		Console.WriteLine("抢到手了");
    	}
    	catch(DbUpdateConcurrencyException ex)
    	{
    		// 4. 捕捉和处理并发冲突
    		var entry = ex.Entries.First();
    		var dbValues = await entry.GetDatabaseValuesAsync();
    		string newOwner = dbValues.GetValue<string>(nameof(House.Owner));
    		Console.WriteLine($"并发冲突,被{newOwner}提前抢走了");
    	}
    }
    // 5.处理数据已存在情况
    else
    {
    	if (h1.Owner == name)
    	{
    		Console.WriteLine("这个房子已经是你的了,不用抢");
    	}
    	else
    	{
    		Console.WriteLine($"这个房子已经被{h1.Owner}抢走了");
    	}
    }
    Console.ReadLine();
    
  4. 测试

    1. 清理 T_Houses 表数据,让 Owner 列等于 null
    2. 同时运行两个控制台程序
    3. 在第一个控制台程序输入 Tom 并运行
    4. 在第二个控制台程序输入 Jim 并运行
    5. 第一个控制台返回消息:抢到手了
    6. 第二个控制台则返回消息:并发冲突,被Tom提前抢走了

扩展

  1. 通常可以通过把并发修改的属性设置为并发令牌的方式启用乐观并发控制。

  2. 有时候无法确定到底哪个属性适合作为并发令牌,比如程序在不同的情况下会更新不同的列或者程序会更新多个列,在这种情况下,可以使用设置一个额外的并发令牌属性的方式来使用乐观并发控制。

  3. 如果使用Microsoft SQL Server数据库,可以用一个byte[]类型的属性作为并发令牌属性,然后使用IsRowVersion把这个属性设置为RowVersion类型,这个属性对应的数据库列就会被设置为ROWVERSION类型。对于ROWVERSION类型的列,在每次插入或更新行时,Microsoft SQL Server会自动为这一行的ROWVERSION类型的列生成新值。

    1. 增加一个额外的byte[]类型的属性
    class House
    {
       public long Id { get; set; }
       public string Name { get; set; }
       public string? Owner { get; set; }
       public byte[] RowVer { get; set; }
    }
    
    2. 配置并发令牌
    builder.ToTable("T_Houses");
    builder.Property(h => h.Name).IsUnicode();
    builder.Property(h => h.RowVer).IsRowVersion();
    
    3. Update 语句中的 Where 中使用 RowVer 列
    
  4. 其它数据库也可以使用 Guid 作为并发令牌控制

  5. 乐观并发控制能够避免悲观锁带来的性能下降、死锁等问题,推荐使用乐观并发控制而不是悲观锁

评论