什么叫并发
假设一个场景:
- 用户下了一个单,数据库的
Order
表存放这个订单数据,其中订单状态=待发货
- 仓库从数据库中
查询出
这个代发货
订单,进行发货逻辑处理,比如:
- 判断订单状态
- 判断地址是否能到达
- 查询商品库存
- 获取快递单号
- 调用打印快递单服务
- 更新订单状态/商品库存等等
- 可以看出做发货逻辑处理耗时会比较长,正在这时候,顾客进行了退货申请,一个按钮点击申请退款,注意:业务逻辑要求已发货的订单不能申请退款,但是在顾客点击申请退款那一瞬间,发货流程还没走完,发货系统还在屁颠屁颠的处理发货逻辑,数据库里的订单状态还是
待发货
,这时候顾客申请退款,接口一下数据库,发现是待发货
,就直接将数据库里的订单状态更新为申请退款
,并反馈给用户操作成功
- 这时候苦逼的发货系统终于把所有发货逻辑全部计算完,兴高采烈得将数据库里得订单状态修改为
已发货
- 那么请问,最终这个订单的状态应该是什么呢?
待发货
?申请退款
?已发货
?
上面那个场景就是所谓的并发,多个地方在对同一条数据进行操作的时候,时常会出现这种情况
怎么解决
锁!
- 悲观锁:是的,相当悲观,对整个世界都不信任的那种!就是假设我读取的数据一定会被修改,所以读数据之前我先把这些数据锁起来,外界拿不到,等我对数据操作完,再把锁释放掉,外界才可以继续用这些数据;
- 乐观锁:相对来说乐观一些,读取数据的时候不对数据上锁,相信没人会来修改这些数据,但是在处理完数据要重新更新数据库的时候,不能盲目信任,要查一下这些数据有没有发生变化,如果变化了,则说明被别人修改了,于是悲伤的抛出个异常表示对这个世界的不满,如果没有变化,则正常的将数据更新进去;
EFCore是怎么做的
EFCore
使用的是乐观锁
,它选择相信这个世界!
EFCore
的乐观锁
分两种粒度:ConcurrencyToken
和RowVersion
ConcurrencyToken
:这个针对表中的某个字段,为表中的某个字段指定为ConcurrencyToken
,则当这个字段被并发修改了,则无法进行SaveChange
,如果不是这个字段,而是这一行的其他字段被修改了,则可以正常进行SaveChange
。以上面订单例子为例,如果将订单状态
这个字段设置为ConcurrencyToken
,那个在顾客申请退款之后,发货系统去更新订单状态则会失败,但是如果这个时候不是更新订单状态
这个字段,而是更新发货员
这个字段,则不会有任何影响,照样可以更新进去
RowVersion
:这个针对表中的所有字段,指定表中某个字段为RowVersion
,每一次更新都会修改RowVersion
这个字段的值,在取出数据重新更新的时候,会查询RowVersion
这个字段的值是否与刚刚取出来的值一致,如果不一致说明这个表中可能某个或多个字段被修改过,则无法进行SaveChange
Talk is cheap. Show me the code
创建项目
创建名字为EFCoreConcurrencyDemo
的ASP.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
命令直接迁移
测试
- 这里提供测试思路,将项目运行起来,先访问
/demo/SeedData
往数据库写入两条测试数据
- 分别测试
/demo/ConcurrencyCheck
和/demo/RowVersionCheck
,在赋值的那行代码打断点,取得数据之后,自己在Microsoft SQL Server Management Studio
中手动修改数据,然后继续运行代码,则可以看出效果
官方文档
https://docs.microsoft.com/zh-cn/ef/core/saving/concurrency