什么叫并发
假设一个场景:
- 用户下了一个单,数据库的
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个包:
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Design
Microsoft.EntityFrameworkCore.Tools
创建数据库实体
在项目根目录创建以下路径和文件:
|--EFCoreConcurrencyDemo
|-- DbModel
|-- ConcurrencyCheckDemo
|-- ConcurrencyCheckDemo.cs
|-- RowVersionDemo
|-- RowVersionDemo.cs
ConcurrencyCheckDemo.cs
的内容如下:
namespace EFCoreConcurrencyDemo.DbModel.ConcurrencyCheckDemo
{
public class ConcurrencyCheckDemo
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
}
RowVersionDemo.cs
的内容如下:
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; }
}
}
配置实体映射规则(这里指定锁)
在项目根目录创建以下路径和文件
|--EFCoreConcurrencyDemo
|-- DbModelConfiguration
|-- ConcurrencyCheckDemoConfiguration.cs
|-- RowVersionDemoConfiguration.cs
ConcurrencyCheckDemoConfiguration.cs
的内容如下:
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
的内容如下:
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
在项目根目录创建以下路径和文件
|--EFCoreConcurrencyDemo
|-- DbContext
|-- MyDbContext.cs
MyDbContext.cs
的内容如下:
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
方法,具体内容如下:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<MyDbContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("EFCoreConcurrencyDemo"));
options.EnableSensitiveDataLogging(false);
});
services.AddControllers();
}
添加数据库连接字符串
在appsettings.json
中添加数据库连接字符串,具体内容如下(连接字符串就换成你自己的数据库):
{
"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
,具体内容如下:
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.Name = "ConcurrencyCheck New Value";
dbValue.Age = 29;
var changedRow = await _dbContext.SaveChangesAsync();
return changedRow;
}
public async Task<int> RowVersionCheck()
{
var dbValue = await _dbContext.RowVersionDemos.FirstOrDefaultAsync();
//dbValue.Name = "RowVersion New Value";
dbValue.Age = 36;
var changedRow = await _dbContext.SaveChangesAsync();
return changedRow;
}
}
}
迁移数据库
在Visual Studio 2019
中的程序包管理控制台
中输入以下命令:
add-migration InitDemoDb
得到迁移记录之后,用以下命令生成数据库脚本,去Microsoft SQL Server Management Studio
中执行即可,或者你可以用EFCore
中的update
命令直接迁移
script-migration
测试
- 这里提供测试思路,将项目运行起来,先访问
/demo/SeedData
往数据库写入两条测试数据 - 分别测试
/demo/ConcurrencyCheck
和/demo/RowVersionCheck
,在赋值的那行代码打断点,取得数据之后,自己在Microsoft SQL Server Management Studio
中手动修改数据,然后继续运行代码,则可以看出效果
官方文档
https://docs.microsoft.com/zh-cn/ef/core/saving/concurrency