# EF Core併發控制 # 併發控制概念 1. 併發控制:避免多個用戶同時操作資源造成的併發衝突問題。 2. 最好的解決方案:非資料庫解決方案 3. 資料庫層面的兩種策略:悲觀、樂觀 # 悲觀鎖 悲觀併發控制一般採用行鎖 ,表鎖等排他鎖對資源進行鎖定,確保同時只有一個使用者操作被鎖定的資源。 E ...
EF Core併發控制
併發控制概念
- 併發控制:避免多個用戶同時操作資源造成的併發衝突問題。
- 最好的解決方案:非資料庫解決方案
- 資料庫層面的兩種策略:悲觀、樂觀
悲觀鎖
悲觀併發控制一般採用行鎖 ,表鎖等排他鎖對資源進行鎖定,確保同時只有一個使用者操作被鎖定的資源。
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();
}
}
可以看到實際上是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();
}
可以看到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()設置為併發令牌,
-
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("房子已經被你搶到了"); } 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語句
多欄位RowVersion
- SQLServer資料庫可以用一個byte[]類型的屬性做併發令牌屬性,然後使用IsRowVersion()把這個屬性設置為RowVersion類型,這樣這個屬性對應的資料庫列就會被設置為ROWVERSION類型。對於這個類型的列,在每次插入或更新行時,資料庫會自動為這一行的ROWVERSION類型的列其生成新值。
- 在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();
效果截圖
概念
- 在MySQL(某些版本)等資料庫中雖然也有類似的timestamp類型,但是由於timestamp類型的精度不夠,並不適合在高併發的系統。
- 非SQLServer中,可以將併發令牌列的值更新為Guid的值
- 修改其他屬性值的同時,使用h1.Rowver = Guid.NewGuid()手動更新併發令牌屬性的值。
總結
- 樂觀併發控制能夠避免悲觀鎖帶來的性能、死鎖等問題,因此推薦使用樂觀併發控制而不是悲觀鎖。
- 如果有一個確定的欄位要被進行併發控制,那麼使用IsConcurrencyToken()把這個欄位設置為併發令牌即可。
- 如果無法確定一個唯一的併發令牌列,那麼就可以引入一個額外的屬性設置為併發令牌,並且在每次更新數據的時候,手動更新這一列的值。如果用的是SQLServer資料庫,那麼也可以採用RowVersion列,這樣就不用開發者手動來在每次更新數據的時候,手動更新併發令牌的值了。
參考鏈接
- 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
每日一道面試題
-
什麼是裝箱和拆箱?
答:從值類型介面轉換到引用類型裝箱。從引用類型轉換到值類型拆箱。
-
抽象類和介面的相同點和不同點有哪些?何時必須聲明一個類為抽象類?
相同點:
- 都是用來實現抽象和多態的機制。
- 都不能被實例化,只能被繼承或實現。
- 都可以包含抽象方法,即沒有具體實現的方法。
- 都可以被子類繼承或實現,併在子類中實現抽象方法。
不同點:
- 抽象類可以包含非抽象方法,而介面只能包含抽象方法。
- 類只能繼承一個抽象類,但可以實現多個介面。
- 抽象類的子類可以選擇性地覆蓋父類的方法,而介面的實現類必須實現介面中定義的所有方法。
- 抽象類可以有構造方法,而介面不能有構造方法。、
一個類必須聲明為抽象類的情況:
- 當類中存在一個或多個抽象方法時,類必須聲明為抽象類。
- 當類需要被繼承,但不能被實例化時,類必須聲明為抽象類。
- 當類中的某些方法需要在子類中實現,而其他方法已經有了具體實現時,類可以聲明為抽象類。
總結:抽象類和介面都是實現抽象和多態的機制,但抽象類更適合用於一些具有公共實現的類,而介面更適合用於定義一組相關的方法,供多個類實現。抽象類可以包含非抽象方法和構造方法,而介面只能包含抽象方法。