翼度科技»论坛 编程开发 .net 查看内容

EF Core并发控制

10

主题

10

帖子

30

积分

新手上路

Rank: 1

积分
30
EF Core并发控制

并发控制概念


  • 并发控制:避免多个用户同时操作资源造成的并发冲突问题。
  • 最好的解决方案:非数据库解决方案
  • 数据库层面的两种策略:悲观、乐观
悲观锁

悲观并发控制一般采用行锁 ,表锁等排他锁对资源进行锁定,确保同时只有一个使用者操作被锁定的资源。
EF Core没有封装悲观并发控制的使用,需要开发人员编写原生SQL语句来使用悲观并发控制。不同数据库语法不一样。
MySQL方案:select * from T_Houses where Id = 1 for update
如果有其他查询操作也使用for update来查询Id=1的这条数据的话,那些查询就会被挂起,一直到针对这条数据的更新操作完成从而释放这个行锁,代码才会继续执行。
代码实现

根据数据库安装对应Nuget包,Mysql如下:
也可以使用官方的,没什么影响
  1. Pemelo.EntityFrameworkCore.MySql
复制代码
House类
  1. class House
  2. {
  3.         public long Id { get; set; }
  4.         public string Name {get;set;}       
  5.         public string Owner {get;set;}
  6. }
复制代码
HouseConfig类
  1. public class HouseConfig:IEntityTypeConfiguration<House>
  2. {
  3.     public void Configure(EntityTypeBuilder<House> builder)
  4.     {
  5.         builder.ToTable("T_Houses");
  6.         builder.Property(b => b.Name).IsRequired();
  7.     }
  8. }
复制代码
DbContext类
  1. public class MyDbContext:DbContext
  2. {
  3.      public DbSet<House> Houses { get; set; }
  4.     protected override void OnModelCreating(ModelBuilder modelBuilder)
  5.     {
  6.         base.OnModelCreating(modelBuilder);
  7.         modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
  8.     }
  9.     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  10.     {
  11.         base.OnConfiguring(optionsBuilder);
  12.         var connString = "server=localhost;user=root;password=root;database=ef1";
  13.         var serverVersion = new MySqlServerVersion(new Version(5, 7, 35));
  14.         optionsBuilder.UseMySql(connString, serverVersion);
  15.     }
  16. }
复制代码
迁移数据库

然后执行数据库迁移
安装Nuget:Microsoft.EntityFrameworkCore.Design,Microsoft.EntityFrameworkCore.Tools

  • Add-Migration Init
  • Update-database
随便给数据库添加几条信息
没有悲观版本
  1.     public static void Main(string[] args)
  2.     {
  3.         Console.WriteLine("请输入您的名字");
  4.         string name = Console.ReadLine();
  5.         using (MyDbContext db = new MyDbContext())
  6.         {
  7.             var h = db.Houses.Single(h => h.Id == 1);
  8.             if (!string.IsNullOrEmpty(h.Owner))
  9.             {
  10.                 if (h.Owner == name)
  11.                 {
  12.                     Console.WriteLine("房子已经被你抢到了");
  13.                 }
  14.                 else
  15.                 {
  16.                     Console.WriteLine($"房子已经被【{h.Owner}】占了");
  17.                 }
  18.                 return;
  19.             }
  20.             h.Owner = name;
  21.             Thread.Sleep(10000);
  22.             Console.WriteLine("恭喜你,抢到了");
  23.             db.SaveChanges();
  24.             Console.ReadLine();
  25.         }
  26.     }
复制代码


可以看到实际上是jack抢到了,但是tom也打印了抢到!
有悲观锁的版本

锁和事务是相关的,因此通过BeginTransactionAsync()创建一个事务,并且在所有操作完成后调用CommitAsync()提交事务
  1. Console.WriteLine("请输入您的名字");
  2. string name = Console.ReadLine();
  3. using MyDbContext db = new MyDbContext();
  4. using (var tx = db.Database.BeginTransaction())
  5. {
  6.     Console.WriteLine($"{DateTime.Now}准备select from update");
  7.     //加锁
  8.     var h = db.Houses.FromSqlInterpolated($"select * from T_houses where Id = 1 for update").Single();
  9.     Console.WriteLine($"{DateTime.Now}完成select from update");
  10.     if (!string.IsNullOrEmpty(h.Owner))
  11.     {
  12.         if (h.Owner == name)
  13.         {
  14.             Console.WriteLine("房子已经被你抢到了");
  15.         }
  16.         else
  17.         {
  18.             Console.WriteLine($"房子已经被【{h.Owner}】占了");
  19.         }
  20.         Console.ReadKey();
  21.         return;
  22.     }
  23.     h.Owner = name;
  24.     Thread.Sleep(5000);
  25.     Console.WriteLine("恭喜你,抢到了");
  26.     db.SaveChanges();
  27.     Console.WriteLine($"{DateTime.Now}保存完成");
  28.     //提交事务
  29.     tx.Commit();
  30.     Console.ReadKey();
  31. }
复制代码

可以看到tom 在27:58秒的时候完成了锁,所以程序提交的时候是tom抢到了,而不是jack,当执行SaveChanges()之前,行的锁会一直存在,直到SaveChanges()运行完成才会释放锁,这时jack才会完成锁。

问题


  • 悲观并发控制的使用比较简单。
  • 锁是独占、排他的,如果系统并发量很大的话,会严重影响性能,如果使用不当的话,甚至会导致死锁。
  • 不同数据库的语法不一样。
乐观锁

原理

Update T_House set Owner = 新值 where Id = 1 and Owner = 旧值
当Update的时候,如果数据库中的Owner值已经被其他操作更新为其他值了,那么where语句的值就会为false,因此这个Update语句影响的行数就是0,EF Core就知道发生并发冲突了,因此SaveChanges()方法就会抛出DbUpdateConcurrencyException异常。
EF Core配置


  • 把被并发修改的属性使用IsConcurrencyToken()设置为并发令牌,
    1. public class HouseConfig:IEntityTypeConfiguration<House>
    2. {
    3.     public void Configure(EntityTypeBuilder<House> builder)
    4.     {
    5.         builder.ToTable("T_Houses");
    6.         builder.Property(b => b.Name).IsRequired();
    7.         builder.Property(h => h.Owner).IsConcurrencyToken(); //这里设置列
    8.     }
    9. }
    复制代码
    1. Console.WriteLine("请输入您的名字");
    2. string name = Console.ReadLine();
    3. using (MyDbContext db = new MyDbContext())
    4. {
    5.     var h = db.Houses.Single(h => h.Id == 1);
    6.     if (!string.IsNullOrEmpty(h.Owner))
    7.     {
    8.         if (h.Owner == name)
    9.         {
    10.             Console.WriteLine("房子已经被你抢到了");
    11.         }
    12.         else
    13.         {
    14.             Console.WriteLine($"房子已经被【{h.Owner}】占了");
    15.         }
    16.         Console.ReadKey();
    17.         return;
    18.     }
    19.     h.Owner = name;
    20.     Thread.Sleep(5000);
    21.     try
    22.     {
    23.         db.SaveChanges();
    24.     }
    25.     catch (DbUpdateConcurrencyException ex)
    26.     {
    27.         Console.WriteLine("并发访问冲突");
    28.         var entry1 = ex.Entries.First();
    29.         string newValue = entry1.GetDatabaseValues().GetValue<string>("Owner");
    30.         Console.WriteLine($"被{newValue}抢先了");
    31.     }
    32.     Console.ReadLine();
    33. }
    复制代码
效果截图

EF 生成的sql语句


多字段RowVersion


  • SQLServer数据库可以用一个byte[]类型的属性做并发令牌属性,然后使用IsRowVersion()把这个属性设置为RowVersion类型,这样这个属性对应的数据库列就会被设置为ROWVERSION类型。对于这个类型的列,在每次插入或更新行时,数据库会自动为这一行的ROWVERSION类型的列其生成新值。
  • 在SQLServer中,timestamp和rowversion是同一种类型的不同别名而已。
注意这里换成SQLServer数据库了!
实体类及配置
  1. public class House
  2. {
  3.     public long Id { get; set; }
  4.     public string Name { get; set; }
  5.     public string? Owner {get;set;}
  6.     public byte[]? RowVer{get;set;}
  7. }
复制代码
  1. //builder.Property(h => h.Owner).IsConcurrencyToken(); //删除掉
  2. builder.Property(h=>h.RowVer).IsRowVersion();
复制代码
效果截图



概念


  • 在MySQL(某些版本)等数据库中虽然也有类似的timestamp类型,但是由于timestamp类型的精度不够,并不适合在高并发的系统。
  • 非SQLServer中,可以将并发令牌列的值更新为Guid的值
  • 修改其他属性值的同时,使用h1.Rowver = Guid.NewGuid()手动更新并发令牌属性的值。
总结


  • 乐观并发控制能够避免悲观锁带来的性能、死锁等问题,因此推荐使用乐观并发控制而不是悲观锁。
  • 如果有一个确定的字段要被进行并发控制,那么使用IsConcurrencyToken()把这个字段设置为并发令牌即可。
  • 如果无法确定一个唯一的并发令牌列,那么就可以引入一个额外的属性设置为并发令牌,并且在每次更新数据的时候,手动更新这一列的值。如果用的是SQLServer数据库,那么也可以采用RowVersion列,这样就不用开发者手动来在每次更新数据的时候,手动更新并发令牌的值了。
参考链接

每日一道面试题


  • 什么是装箱和拆箱?
    答:从值类型接口转换到引用类型装箱。从引用类型转换到值类型拆箱。
  • 抽象类和接口的相同点和不同点有哪些?何时必须声明一个类为抽象类?
    相同点:

    • 都是用来实现抽象和多态的机制。
    • 都不能被实例化,只能被继承或实现。
    • 都可以包含抽象方法,即没有具体实现的方法。
    • 都可以被子类继承或实现,并在子类中实现抽象方法。
    不同点:

    • 抽象类可以包含非抽象方法,而接口只能包含抽象方法。
    • 类只能继承一个抽象类,但可以实现多个接口。
    • 抽象类的子类可以选择性地覆盖父类的方法,而接口的实现类必须实现接口中定义的所有方法。
    • 抽象类可以有构造方法,而接口不能有构造方法。、
    一个类必须声明为抽象类的情况:

    • 当类中存在一个或多个抽象方法时,类必须声明为抽象类。
    • 当类需要被继承,但不能被实例化时,类必须声明为抽象类。
    • 当类中的某些方法需要在子类中实现,而其他方法已经有了具体实现时,类可以声明为抽象类。
    总结:抽象类和接口都是实现抽象和多态的机制,但抽象类更适合用于一些具有公共实现的类,而接口更适合用于定义一组相关的方法,供多个类实现。抽象类可以包含非抽象方法和构造方法,而接口只能包含抽象方法。


来源:https://www.cnblogs.com/ZYPLJ/archive/2023/09/03/17675494.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具