EF Core并发控制

EF Core并发控制

并发控制概念

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

悲观锁

悲观并发控制一般采用行锁 ,表锁等排他锁对资源进行锁定,确保同时只有一个使用者操作被锁定的资源。

EF Core没有封装悲观并发控制的使用,需要开发人员编写原生SQL语句来使用悲观并发控制。不同数据库语法不一样。

MySQL方案:select * from T_Houses where Id = 1 for update

如果有其他查询操作也使用for update来查询Id=1的这条数据的话,那些查询就会被挂起,一直到针对这条数据的更新操作完成从而释放这个行锁,代码才会继续执行。

代码实现

根据数据库安装对应Nuget包,Mysql如下:

也可以使用官方的,没什么影响

Pemelo.EntityFrameworkCore.MySql

House类

class House
{
	public long Id { get; set; }
	public string Name {get;set;}	
	public string Owner {get;set;}
}

HouseConfig类

public class HouseConfig:IEntityTypeConfiguration<House>
{
    public void Configure(EntityTypeBuilder<House> builder)
    {
        builder.ToTable("T_Houses");
        builder.Property(b => b.Name).IsRequired();
    }
}

DbContext类

public class MyDbContext:DbContext
{
     public DbSet<House> Houses { get; set; }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);
        var connString = "server=localhost;user=root;password=root;database=ef1";
        var serverVersion = new MySqlServerVersion(new Version(5, 7, 35));
        optionsBuilder.UseMySql(connString, serverVersion);
    }
}

迁移数据库

然后执行数据库迁移

安装Nuget:Microsoft.EntityFrameworkCore.Design,Microsoft.EntityFrameworkCore.Tools

  • Add-Migration Init
  • Update-database

随便给数据库添加几条信息

没有悲观版本

    public static void Main(string[] args)
    {
        Console.WriteLine("请输入您的名字");
        string name = Console.ReadLine();
        using (MyDbContext db = new MyDbContext())
        {
            var h = db.Houses.Single(h => h.Id == 1);
            if (!string.IsNullOrEmpty(h.Owner))
            {
                if (h.Owner == name)
                {
                    Console.WriteLine("房子已经被你抢到了");
                }
                else
                {
                    Console.WriteLine($"房子已经被【{h.Owner}】占了");
                }
                return;
            }
            h.Owner = name;
            Thread.Sleep(10000);
            Console.WriteLine("恭喜你,抢到了");
            db.SaveChanges();
            Console.ReadLine();
        }
    }

image

image

可以看到实际上是jack抢到了,但是tom也打印了抢到!

有悲观锁的版本

锁和事务是相关的,因此通过BeginTransactionAsync()创建一个事务,并且在所有操作完成后调用CommitAsync()提交事务

Console.WriteLine("请输入您的名字");
string name = Console.ReadLine();
using MyDbContext db = new MyDbContext();
using (var tx = db.Database.BeginTransaction())
{
    Console.WriteLine($"{DateTime.Now}准备select from update");
    //加锁
    var h = db.Houses.FromSqlInterpolated($"select * from T_houses where Id = 1 for update").Single();
    Console.WriteLine($"{DateTime.Now}完成select from update");
    if (!string.IsNullOrEmpty(h.Owner))
    {
        if (h.Owner == name)
        {
            Console.WriteLine("房子已经被你抢到了");
        }
        else
        {
            Console.WriteLine($"房子已经被【{h.Owner}】占了");
        }
        Console.ReadKey();
        return;
    }
    h.Owner = name;
    Thread.Sleep(5000);
    Console.WriteLine("恭喜你,抢到了");
    db.SaveChanges();
    Console.WriteLine($"{DateTime.Now}保存完成");
    //提交事务
    tx.Commit();
    Console.ReadKey();
}

image

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

image

问题

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

乐观锁

原理

Update T_House set Owner = 新值 where Id = 1 and Owner = 旧值

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

EF Core配置

  1. 把被并发修改的属性使用IsConcurrencyToken()设置为并发令牌,

  2. public class HouseConfig:IEntityTypeConfiguration<House>
    {
        public void Configure(EntityTypeBuilder<House> builder)
        {
            builder.ToTable("T_Houses");
            builder.Property(b => b.Name).IsRequired();
            builder.Property(h => h.Owner).IsConcurrencyToken(); //这里设置列
        }
    }
    
  3. Console.WriteLine("请输入您的名字");
    string name = Console.ReadLine();
    using (MyDbContext db = new MyDbContext())
    {
        var h = db.Houses.Single(h => h.Id == 1);
        if (!string.IsNullOrEmpty(h.Owner))
        {
            if (h.Owner == name)
            {
                Console.WriteLine("房子已经被你抢到了");
            }
            else
            {
                Console.WriteLine($"房子已经被【{h.Owner}】占了");
            }
    
            Console.ReadKey();
            return;
        }
        h.Owner = name;
        Thread.Sleep(5000);
        try
        {
            db.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex)
        {
            Console.WriteLine("并发访问冲突");
            var entry1 = ex.Entries.First();
            string newValue = entry1.GetDatabaseValues().GetValue<string>("Owner");
            Console.WriteLine($"被{newValue}抢先了");
        }
        Console.ReadLine();
    }
    

效果截图

EF 生成的sql语句

image

image

多字段RowVersion

  1. SQLServer数据库可以用一个byte[]类型的属性做并发令牌属性,然后使用IsRowVersion()把这个属性设置为RowVersion类型,这样这个属性对应的数据库列就会被设置为ROWVERSION类型。对于这个类型的列,在每次插入或更新行时,数据库会自动为这一行的ROWVERSION类型的列其生成新值。
  2. 在SQLServer中,timestamp和rowversion是同一种类型的不同别名而已。

注意这里换成SQLServer数据库了!

实体类及配置

public class House
{
    public long Id { get; set; }
    public string Name { get; set; }
    public string? Owner {get;set;}
    public byte[]? RowVer{get;set;}
}
//builder.Property(h => h.Owner).IsConcurrencyToken(); //删除掉
builder.Property(h=>h.RowVer).IsRowVersion();

效果截图

image

image

概念

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

总结

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

参考链接

每日一道面试题

  1. 什么是装箱和拆箱?

    答:从值类型接口转换到引用类型装箱。从引用类型转换到值类型拆箱。

  2. 抽象类和接口的相同点和不同点有哪些?何时必须声明一个类为抽象类?

    相同点:

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

    不同点:

    1. 抽象类可以包含非抽象方法,而接口只能包含抽象方法。
    2. 类只能继承一个抽象类,但可以实现多个接口。
    3. 抽象类的子类可以选择性地覆盖父类的方法,而接口的实现类必须实现接口中定义的所有方法。
    4. 抽象类可以有构造方法,而接口不能有构造方法。、

    一个类必须声明为抽象类的情况:

    1. 当类中存在一个或多个抽象方法时,类必须声明为抽象类。
    2. 当类需要被继承,但不能被实例化时,类必须声明为抽象类。
    3. 当类中的某些方法需要在子类中实现,而其他方法已经有了具体实现时,类可以声明为抽象类。

    总结:抽象类和接口都是实现抽象和多态的机制,但抽象类更适合用于一些具有公共实现的类,而接口更适合用于定义一组相关的方法,供多个类实现。抽象类可以包含非抽象方法和构造方法,而接口只能包含抽象方法。

posted @   妙妙屋(zy)  阅读(576)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· 2分钟学会 DeepSeek API,竟然比官方更好用!
· .NET 使用 DeepSeek R1 开发智能 AI 客户端
· 10亿数据,如何做迁移?
· 推荐几款开源且免费的 .NET MAUI 组件库
· c# 半导体/led行业 晶圆片WaferMap实现 map图实现入门篇
  1. 1 够爱(翻自 曾沛慈) 是我呀卡司宝贝
  2. 2 老人と海 ヨルシカ
  3. 3 生生世世爱 黄霄雲
  4. 4 希望有羽毛和翅膀 imzat
生生世世爱 - 黄霄雲
00:00 / 00:00
An audio error has occurred, player will skip forward in 2 seconds.

作词 : 杨漩予

作曲 : 林毅

爱 还没来

天地间风云忽然变

有情有义的人都要回来

爱 总会来

生死注定的来世再爱

都等了太久哭尽无奈

rap

是谁站在三清山门外

回首看桃溪花正开

一入岁月江湖中来

再随之去追星辰山海

到底是那恩怨过往

或是爱恨情长 怎么收场

这些年的故事和秘密

都在逆水寒里珍藏

爱恨纠缠的生生世世

爱恨纠缠的生生世世

心底执着的信念为你存在

多遥远的路都阻挡不住

再次拥有没距离的温度

失去自由的生生世世

有爱不懂相拥错过了最爱

送一剑祝福再默默相助

恐怕没以后不自觉留退路

爱 还没来

爱 还没来

天地间风云忽然变

有情有义的人都要回来

爱 总会来

生死注定的来世再爱

都等了太久哭尽无奈

rap

遇过金风细雨楼的刀太快

也遇过六分半堂惊雷开

身处这江湖风雨事

却不知可曾有故人来

轻挥一剑千山过

再回望轻舟多澎湃

我欲乘风逍遥去

纵览天地入我怀

从此我心自在

爱恨纠缠的生生世世

爱恨纠缠的生生世世

心底执着的信念为你存在

多遥远的路都阻挡不住

再次拥有没距离的温度

失去自由的生生世世

有爱不懂相拥错过了最爱

送一剑祝福再默默相助

恐怕没以后不自觉留退路

为情所困的生生世世

为情所困的生生世世

伤也被伤命中成双的伤害

等不到日出一个人孤独

让星光代替我伴你远途

黑白轮回的生生世世

彻底放开成全永远的依赖

是乱世英雄或凡间俗梦

爱不离爱是把这感动留住

爱不离爱是把这感动留住

原唱 : 吴雨霏

OP原始版权人:北京大石音乐版权有限公司、CHANCES CREATIVE MUSIC LTD

SP代理权利人:北京大石音乐版权有限公司、百代音乐版权代理(北京)有限公司

编曲改编:1AN孙毅然

改编词(Rap词):焦糖

Rapper:阿茹汗

笛子实录:水玥儿

混音&母带工程师:王嘉屏

和声:曾雪祁

配唱制作人:沈小力

录音师:邢铜

录音棚:55TEC Studio

出品:逆水寒

企划营销:微梦传媒

点击右上角即可分享
微信分享提示