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个包:

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

测试

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

官方文档

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值