EntityFramework Core的并发处理

什么叫并发

假设一个场景:

  1. 用户下了一个单,数据库的Order表存放这个订单数据,其中订单状态=待发货
  2. 仓库从数据库中查询出这个代发货订单,进行发货逻辑处理,比如:
    1. 判断订单状态
    2. 判断地址是否能到达
    3. 查询商品库存
    4. 获取快递单号
    5. 调用打印快递单服务
    6. 更新订单状态/商品库存等等
  3. 可以看出做发货逻辑处理耗时会比较长,正在这时候,顾客进行了退货申请,一个按钮点击申请退款,注意:业务逻辑要求已发货的订单不能申请退款,但是在顾客点击申请退款那一瞬间,发货流程还没走完,发货系统还在屁颠屁颠的处理发货逻辑,数据库里的订单状态还是待发货,这时候顾客申请退款,接口一下数据库,发现是待发货,就直接将数据库里的订单状态更新为申请退款,并反馈给用户操作成功
  4. 这时候苦逼的发货系统终于把所有发货逻辑全部计算完,兴高采烈得将数据库里得订单状态修改为已发货
  5. 那么请问,最终这个订单的状态应该是什么呢?待发货申请退款已发货

上面那个场景就是所谓的并发,多个地方在对同一条数据进行操作的时候,时常会出现这种情况

怎么解决

锁!

  • 悲观锁:是的,相当悲观,对整个世界都不信任的那种!就是假设我读取的数据一定会被修改,所以读数据之前我先把这些数据锁起来,外界拿不到,等我对数据操作完,再把锁释放掉,外界才可以继续用这些数据;
  • 乐观锁:相对来说乐观一些,读取数据的时候不对数据上锁,相信没人会来修改这些数据,但是在处理完数据要重新更新数据库的时候,不能盲目信任,要查一下这些数据有没有发生变化,如果变化了,则说明被别人修改了,于是悲伤的抛出个异常表示对这个世界的不满,如果没有变化,则正常的将数据更新进去;

EFCore是怎么做的

EFCore使用的是乐观锁,它选择相信这个世界!

EFCore乐观锁分两种粒度:ConcurrencyTokenRowVersion

  • ConcurrencyToken:这个针对表中的某个字段,为表中的某个字段指定为ConcurrencyToken,则当这个字段被并发修改了,则无法进行SaveChange,如果不是这个字段,而是这一行的其他字段被修改了,则可以正常进行SaveChange。以上面订单例子为例,如果将订单状态这个字段设置为ConcurrencyToken,那个在顾客申请退款之后,发货系统去更新订单状态则会失败,但是如果这个时候不是更新订单状态这个字段,而是更新发货员这个字段,则不会有任何影响,照样可以更新进去
  • RowVersion:这个针对表中的所有字段,指定表中某个字段为RowVersion,每一次更新都会修改RowVersion这个字段的值,在取出数据重新更新的时候,会查询RowVersion这个字段的值是否与刚刚取出来的值一致,如果不一致说明这个表中可能某个或多个字段被修改过,则无法进行SaveChange

Talk is cheap. Show me the code

创建项目

创建名字为EFCoreConcurrencyDemoASP.NET Core项目,类型为API,这里使用的是Sql Server数据库,所有需要引入以下3个包:

1
2
3
Microsoft.EntityFrameworkCore.SqlServer  
Microsoft.EntityFrameworkCore.Design
Microsoft.EntityFrameworkCore.Tools

创建数据库实体

在项目根目录创建以下路径和文件:

1
2
3
4
5
6
|--EFCoreConcurrencyDemo  
|-- DbModel
|-- ConcurrencyCheckDemo
|-- ConcurrencyCheckDemo.cs
|-- RowVersionDemo
|-- RowVersionDemo.cs

ConcurrencyCheckDemo.cs的内容如下:

1
2
3
4
5
6
7
8
9
10
11
namespace EFCoreConcurrencyDemo.DbModel.ConcurrencyCheckDemo  
{
public class ConcurrencyCheckDemo
{
public int Id { get; set; }

public string Name { get; set; }

public int Age { get; set; }
}
}

RowVersionDemo.cs的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace EFCoreConcurrencyDemo.DbModel.RowVersionDemo  
{
public class RowVersionDemo
{
public int Id { get; set; }

public string Name { get; set; }

public int Age { get; set; }

public byte[] RowVersion { get; set; }
}
}

配置实体映射规则(这里指定锁)

在项目根目录创建以下路径和文件

1
2
3
4
|--EFCoreConcurrencyDemo  
|-- DbModelConfiguration
|-- ConcurrencyCheckDemoConfiguration.cs
|-- RowVersionDemoConfiguration.cs

ConcurrencyCheckDemoConfiguration.cs的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using EFCoreConcurrencyDemo.DbModel.ConcurrencyCheckDemo;  
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace EFCoreConcurrencyDemo.DbModelConfiguration
{
public class ConcurrencyCheckDemoConfiguration : IEntityTypeConfiguration<ConcurrencyCheckDemo>
{
public void Configure(EntityTypeBuilder<ConcurrencyCheckDemo> builder)
{
builder.ToTable("ConcurrencyCheckDemo");
builder.Property(x => x.Name).IsConcurrencyToken();
}
}
}

RowVersionDemoConfiguration.cs的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using EFCoreConcurrencyDemo.DbModel.RowVersionDemo;  
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace EFCoreConcurrencyDemo.DbModelConfiguration
{
public class RowVersionDemoConfiguration: IEntityTypeConfiguration<RowVersionDemo>
{
public void Configure(EntityTypeBuilder<RowVersionDemo> builder)
{
builder.ToTable("RowVersionDemo");
builder.Property(x => x.RowVersion).IsRowVersion();
}
}
}

创建DbContext

在项目根目录创建以下路径和文件

1
2
3
|--EFCoreConcurrencyDemo  
|-- DbContext
|-- MyDbContext.cs

MyDbContext.cs的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using EFCoreConcurrencyDemo.DbModel.ConcurrencyCheckDemo;  
using EFCoreConcurrencyDemo.DbModel.RowVersionDemo;
using EFCoreConcurrencyDemo.DbModelConfiguration;
using Microsoft.EntityFrameworkCore;

namespace EFCoreConcurrencyDemo.DbContext
{
public class MyDbContext:Microsoft.EntityFrameworkCore.DbContext
{
public MyDbContext(DbContextOptions<MyDbContext> options):base(options)
{
}

public DbSet<ConcurrencyCheckDemo> ConcurrencyCheckDemos { get; set; }

public DbSet<RowVersionDemo> RowVersionDemos { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new ConcurrencyCheckDemoConfiguration());
modelBuilder.ApplyConfiguration(new RowVersionDemoConfiguration());
}
}
}

修改Startup

修改Startup.ConfigureServices方法,具体内容如下:

1
2
3
4
5
6
7
8
9
public void ConfigureServices(IServiceCollection services)  
{
services.AddDbContext<MyDbContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("EFCoreConcurrencyDemo"));
options.EnableSensitiveDataLogging(false);
});
services.AddControllers();
}

添加数据库连接字符串

appsettings.json中添加数据库连接字符串,具体内容如下(连接字符串就换成你自己的数据库):

1
2
3
4
5
6
7
8
9
10
11
12
13
{  
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"ConnectionStrings": {
"EFCoreConcurrencyDemo": "Password=jiamiao.x.20.demo;Persist Security Info=True;User ID=sa;Initial Catalog=EFCoreConcurrencyDemo;Data Source=127.0.0.1"
},
"AllowedHosts": "\*"
}

添加测试控制器

Controllers中添加DemoController.cs,具体内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
using System.Threading.Tasks;  
using EFCoreConcurrencyDemo.DbContext;
using EFCoreConcurrencyDemo.DbModel.ConcurrencyCheckDemo;
using EFCoreConcurrencyDemo.DbModel.RowVersionDemo;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace EFCoreConcurrencyDemo.Controllers
{
[Route("[controller]/[action]")]
[ApiController]
public class DemoController : ControllerBase
{
private readonly MyDbContext _dbContext;

public DemoController(MyDbContext dbContext)
{
_dbContext = dbContext;
}

public async Task<int\> SeedData()
{
var concurrencyCheckDemo = new ConcurrencyCheckDemo()
{
Name = "ConcurrencyCheck测试",
Age = 20
};
await _dbContext.ConcurrencyCheckDemos.AddAsync(concurrencyCheckDemo);

var rowVersionDemo = new RowVersionDemo()
{
Name = "RowVersion测试",
Age = 24
};
await _dbContext.RowVersionDemos.AddAsync(rowVersionDemo);
var changedRow = await _dbContext.SaveChangesAsync();
return changedRow;
}

public async Task<int\> ConcurrencyCheck()
{
var dbValue = await _dbContext.ConcurrencyCheckDemos.FirstOrDefaultAsync();

dbValue.Age = 29;
var changedRow = await _dbContext.SaveChangesAsync();
return changedRow;
}

public async Task<int\> RowVersionCheck()
{
var dbValue = await _dbContext.RowVersionDemos.FirstOrDefaultAsync();

dbValue.Age = 36;
var changedRow = await _dbContext.SaveChangesAsync();
return changedRow;
}
}
}

迁移数据库

Visual Studio 2019中的程序包管理控制台中输入以下命令:

1
add-migration InitDemoDb  

得到迁移记录之后,用以下命令生成数据库脚本,去Microsoft SQL Server Management Studio中执行即可,或者你可以用EFCore中的update命令直接迁移

1
script-migration  

测试

  1. 这里提供测试思路,将项目运行起来,先访问/demo/SeedData往数据库写入两条测试数据
  2. 分别测试/demo/ConcurrencyCheck/demo/RowVersionCheck,在赋值的那行代码打断点,取得数据之后,自己在Microsoft SQL Server Management Studio中手动修改数据,然后继续运行代码,则可以看出效果

官方文档

https://docs.microsoft.com/zh-cn/ef/core/saving/concurrency