EF Core并發(fā)控制
并發(fā)控制概念
- 并發(fā)控制:避免多個用戶同時操作資源造成的并發(fā)沖突問題。
- 最好的解決方案:非數(shù)據(jù)庫解決方案
- 數(shù)據(jù)庫層面的兩種策略:悲觀、樂觀
悲觀鎖
悲觀并發(fā)控制一般采用行鎖 ,表鎖等排他鎖對資源進行鎖定,確保同時只有一個使用者操作被鎖定的資源。
EF Core沒有封裝悲觀并發(fā)控制的使用,需要開發(fā)人員編寫原生SQL語句來使用悲觀并發(fā)控制。不同數(shù)據(jù)庫語法不一樣。
MySQL方案:
select * from T_Houses where Id = 1 for update
如果有其他查詢操作也使用for update來查詢Id=1的這條數(shù)據(jù)的話,那些查詢就會被掛起,一直到針對這條數(shù)據(jù)的更新操作完成從而釋放這個行鎖,代碼才會繼續(xù)執(zhí)行。
代碼實現(xiàn)
根據(jù)數(shù)據(jù)庫安裝對應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);
}
}
遷移數(shù)據(jù)庫
然后執(zhí)行數(shù)據(jù)庫遷移
安裝Nuget:Microsoft.EntityFrameworkCore.Design
,Microsoft.EntityFrameworkCore.Tools
- Add-Migration Init
- Update-database
隨便給數(shù)據(jù)庫添加幾條信息
沒有悲觀版本
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("房子已經(jīng)被你搶到了");
}
else
{
Console.WriteLine($"房子已經(jīng)被【{h.Owner}】占了");
}
return;
}
h.Owner = name;
Thread.Sleep(10000);
Console.WriteLine("恭喜你,搶到了");
db.SaveChanges();
Console.ReadLine();
}
}
可以看到實際上是jack搶到了,但是tom也打印了搶到!
有悲觀鎖的版本
鎖和事務是相關的,因此通過BeginTransactionAsync()創(chuàng)建一個事務,并且在所有操作完成后調(diào)用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("房子已經(jīng)被你搶到了");
}
else
{
Console.WriteLine($"房子已經(jīng)被【{h.Owner}】占了");
}
Console.ReadKey();
return;
}
h.Owner = name;
Thread.Sleep(5000);
Console.WriteLine("恭喜你,搶到了");
db.SaveChanges();
Console.WriteLine($"{DateTime.Now}保存完成");
//提交事務
tx.Commit();
Console.ReadKey();
}
可以看到tom 在27:58秒的時候完成了鎖,所以程序提交的時候是tom搶到了,而不是jack,當執(zhí)行SaveChanges()之前,行的鎖會一直存在,直到Commit()事務提交之后才會釋放鎖,這時jack才會完成鎖。
問題
- 悲觀并發(fā)控制的使用比較簡單。
- 鎖是獨占、排他的,如果系統(tǒng)并發(fā)量很大的話,會嚴重影響性能,如果使用不當?shù)脑挘踔習е滤梨i。
- 不同數(shù)據(jù)庫的語法不一樣。
樂觀鎖
原理
Update T_House set Owner = 新值 where Id = 1 and Owner = 舊值
當Update的時候,如果數(shù)據(jù)庫中的Owner值已經(jīng)被其他操作更新為其他值了,那么where語句的值就會為false,因此這個Update語句影響的行數(shù)就是0,EF Core就知道發(fā)生并發(fā)沖突了,因此SaveChanges()
方法就會拋出DbUpdateConcurrencyException
異常。
EF Core配置
-
把被并發(fā)修改的屬性使用IsConcurrencyToken()設置為并發(fā)令牌,
-
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(); //這里設置列 } }
-
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("房子已經(jīng)被你搶到了"); } else { Console.WriteLine($"房子已經(jīng)被【{h.Owner}】占了"); } Console.ReadKey(); return; } h.Owner = name; Thread.Sleep(5000); try { db.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { Console.WriteLine("并發(fā)訪問沖突"); var entry1 = ex.Entries.First(); string newValue = entry1.GetDatabaseValues().GetValue<string>("Owner"); Console.WriteLine($"被{newValue}搶先了"); } Console.ReadLine(); }
效果截圖
EF 生成的sql語句
多字段RowVersion
- SQLServer數(shù)據(jù)庫可以用一個byte[]類型的屬性做并發(fā)令牌屬性,然后使用IsRowVersion()把這個屬性設置為RowVersion類型,這樣這個屬性對應的數(shù)據(jù)庫列就會被設置為ROWVERSION類型。對于這個類型的列,在每次插入或更新行時,數(shù)據(jù)庫會自動為這一行的ROWVERSION類型的列其生成新值。
- 在SQLServer中,timestamp和rowversion是同一種類型的不同別名而已。
注意這里換成SQLServer數(shù)據(jù)庫了!
實體類及配置
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();
效果截圖
概念
- 在MySQL(某些版本)等數(shù)據(jù)庫中雖然也有類似的timestamp類型,但是由于timestamp類型的精度不夠,并不適合在高并發(fā)的系統(tǒng)。
- 非SQLServer中,可以將并發(fā)令牌列的值更新為Guid的值
- 修改其他屬性值的同時,使用h1.Rowver = Guid.NewGuid()手動更新并發(fā)令牌屬性的值。
總結(jié)
- 樂觀并發(fā)控制能夠避免悲觀鎖帶來的性能、死鎖等問題,因此推薦使用樂觀并發(fā)控制而不是悲觀鎖。
- 如果有一個確定的字段要被進行并發(fā)控制,那么使用IsConcurrencyToken()把這個字段設置為并發(fā)令牌即可。
- 如果無法確定一個唯一的并發(fā)令牌列,那么就可以引入一個額外的屬性設置為并發(fā)令牌,并且在每次更新數(shù)據(jù)的時候,手動更新這一列的值。如果用的是SQLServer數(shù)據(jù)庫,那么也可以采用RowVersion列,這樣就不用開發(fā)者手動來在每次更新數(shù)據(jù)的時候,手動更新并發(fā)令牌的值了。
參考鏈接
- NuGet Gallery | Pomelo.EntityFrameworkCore.MySql 7.0.0 (https://www.nuget.org/packages/Pomelo.EntityFrameworkCore.MySql
- 【.NET 6教程,.Net Core 2022視頻教程,楊中科主講】 https://www.bilibili.com/video/BV1pK41137He/?p=89&share_source=copy_web&vd_source=fce337a51d11a67781404c67ec0b5084
每日一道面試題
-
什么是裝箱和拆箱?
答:從值類型接口轉(zhuǎn)換到引用類型裝箱。從引用類型轉(zhuǎn)換到值類型拆箱。
-
抽象類和接口的相同點和不同點有哪些?何時必須聲明一個類為抽象類?
相同點:
- 都是用來實現(xiàn)抽象和多態(tài)的機制。
- 都不能被實例化,只能被繼承或?qū)崿F(xiàn)。
- 都可以包含抽象方法,即沒有具體實現(xiàn)的方法。
- 都可以被子類繼承或?qū)崿F(xiàn),并在子類中實現(xiàn)抽象方法。
不同點:
- 抽象類可以包含非抽象方法,而接口只能包含抽象方法。
- 類只能繼承一個抽象類,但可以實現(xiàn)多個接口。
- 抽象類的子類可以選擇性地覆蓋父類的方法,而接口的實現(xiàn)類必須實現(xiàn)接口中定義的所有方法。
- 抽象類可以有構造方法,而接口不能有構造方法。、
一個類必須聲明為抽象類的情況:文章來源:http://www.zghlxwxcb.cn/news/detail-692286.html
- 當類中存在一個或多個抽象方法時,類必須聲明為抽象類。
- 當類需要被繼承,但不能被實例化時,類必須聲明為抽象類。
- 當類中的某些方法需要在子類中實現(xiàn),而其他方法已經(jīng)有了具體實現(xiàn)時,類可以聲明為抽象類。
總結(jié):抽象類和接口都是實現(xiàn)抽象和多態(tài)的機制,但抽象類更適合用于一些具有公共實現(xiàn)的類,而接口更適合用于定義一組相關的方法,供多個類實現(xiàn)。抽象類可以包含非抽象方法和構造方法,而接口只能包含抽象方法。文章來源地址http://www.zghlxwxcb.cn/news/detail-692286.html
到了這里,關于EF Core并發(fā)控制的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!