1、高併發的情況,時常會發生資料不穩定的情況
在看本節內容之前,請先看上一章SqlServer 高併發的情況下,如何利用鎖保證資料的穩定性
本節內容,也是具體討論如何在EF中實現這些操作
2、場景模擬,同上一章,搶券
EF 不考慮高併發的情況下,搶券程式碼為:
string _currOwner = Console.ReadLine();//當前使用者 using var ctx = new MyDBContext(); var cop = ctx.Coupons.Single(x => x.Id == 2); if (!string.IsNullOrEmpty(cop.Owner)) { Console.WriteLine($"券被搶了"); } else { cop.Owner = _currOwner; Thread.Sleep(5000); ctx.SaveChanges(); Console.WriteLine($"恭喜{_currOwner}搶到券{cop.Id}了"); } Console.ReadLine();
開啟兩個程式,讓tom和jerry同時先後進行搶券,模擬出一個券同時被兩個使用者搶到的情況
上圖可用直觀看出,都提示搶券成功,但是owner是晚一點點執行update的jerry,在實際生產中,無法給tom一個交代
3、解決併發問題
3.1 透過updlock,悲觀併發控制
string _currOwner = Console.ReadLine();//當前使用者 using var ctx = new MyDBContext(); using var tx = ctx.Database.BeginTransaction(); FormattableString sql = $@"select * from Coupons with(updlock) where id=2"; var cop = ctx.Coupons.FromSqlInterpolated(sql).Single(); if (!string.IsNullOrEmpty(cop.Owner)) { Console.WriteLine($"券被搶了"); } else { cop.Owner = _currOwner; Thread.Sleep(5000); ctx.SaveChanges(); Console.WriteLine($"恭喜{_currOwner}搶到券{cop.Id}了"); } tx.Commit(); Console.ReadLine();
解決:但這個是排他鎖,有可能造成執行緒卡頓問題
3.2 透過定義鑑權欄位,樂觀併發控制
CouponConfig新增配置
builder.Property(x => x.Owner).IsConcurrencyToken();
搶券程式碼:
string _currOwner = Console.ReadLine();//當前使用者 using var ctx = new MyDBContext(); var cop = ctx.Coupons.Single(x => x.Id == 2); if (!string.IsNullOrEmpty(cop.Owner)) { Console.WriteLine($"券被搶了"); } else { Thread.Sleep(5000); try { cop.Owner = _currOwner; await ctx.SaveChangesAsync(); Console.WriteLine($"恭喜{_currOwner}搶到券{cop.Id}了"); } catch (DbUpdateConcurrencyException ex) { var entry = ex.Entries.First(); var dbValues = entry.GetDatabaseValues(); var newOwner = dbValues.GetValue<string>(nameof(Coupon.Owner)); Console.WriteLine($"併發衝突,{newOwner}已經搶到該券了"); } }
結果:
根據update語句,可用看出where加了owner=舊值,來判斷是否發生過更改
3.3 新增資料版本標識
如果無法定義一個明確的鑑權欄位,那麼可用透過新增一個欄位,來標識資料來進行鑑權
public class Coupon { public int Id { get; set; } public string Name { get; set; } public string? Description { get; set; } public string? Owner { get; set; } public byte[] RowVersion { get; set; } #遷移到資料庫,型別為rowversion,當資料更新時,版本會自動遞增 }
遷移後資料庫表程式碼
CREATE TABLE [dbo].[Coupons] ( [Id] INT IDENTITY (1, 1) NOT NULL, [Name] NVARCHAR (MAX) NOT NULL, [Description] NVARCHAR (MAX) NULL, [Owner] NVARCHAR (MAX) NULL, [RowVersion] ROWVERSION NOT NULL, CONSTRAINT [PK_Coupons] PRIMARY KEY CLUSTERED ([Id] ASC) );
CouponConfig新增配置
builder.Property(x => x.RowVersion).IsRowVersion();
搶券程式碼同3.2
結果:
理論和3.2相同,where會做一個rowversion的舊值判斷
總結:這三種方法由淺入深,各有利弊,在併發量不大的情況下使用3.1,併發量較大的情況下使用3.2&3.3
感謝關注!!