EF Code 如何應(yīng)對高并發(fā)
1、高并發(fā)的情況,時(shí)常會發(fā)生數(shù)據(jù)不穩(wěn)定的情況
在看本節(jié)內(nèi)容之前,請先看上一章SqlServer 高并發(fā)的情況下,如何利用鎖保證數(shù)據(jù)的穩(wěn)定性
本節(jié)內(nèi)容,也是具體討論如何在EF中實(shí)現(xiàn)這些操作
2、場景模擬,同上一章,搶券
EF 不考慮高并發(fā)的情況下,搶券代碼為:

string _currOwner = Console.ReadLine();//當(dāng)前用戶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();

打開兩個進(jìn)程,讓tom和jerry同時(shí)先后進(jìn)行搶券,模擬出一個券同時(shí)被兩個用戶搶到的情況


上圖可用直觀看出,都提示搶券成功,但是owner是晚一點(diǎn)點(diǎn)執(zhí)行update的jerry,在實(shí)際生產(chǎn)中,無法給tom一個交代
3、解決并發(fā)問題
3.1 通過updlock,悲觀并發(fā)控制

string _currOwner = Console.ReadLine();//當(dāng)前用戶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 通過定義鑒權(quán)字段,樂觀并發(fā)控制
CouponConfig添加配置
? ? ? ? ? ?builder.Property(x => x.Owner).IsConcurrencyToken();
搶券代碼:

string _currOwner = Console.ReadLine();//當(dāng)前用戶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($"并發(fā)沖突,{newOwner}已經(jīng)搶到該券了"); ? ?} }

結(jié)果:

根據(jù)update語句,可用看出where加了owner=舊值,來判斷是否發(fā)生過更改
3.3 添加數(shù)據(jù)版本標(biāo)識
如果無法定義一個明確的鑒權(quán)字段,那么可用通過新增一個字段,來標(biāo)識數(shù)據(jù)來進(jìn)行鑒權(quán)

? ?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; } #遷移到數(shù)據(jù)庫,類型為rowversion,當(dāng)數(shù)據(jù)更新時(shí),版本會自動遞增 ? ?}

遷移后數(shù)據(jù)庫表代碼

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
結(jié)果:

理論和3.2相同,where會做一個rowversion的舊值判斷
?
總結(jié):這三種方法由淺入深,各有利弊,在并發(fā)量不大的情況下使用3.1,并發(fā)量較大的情況下使用3.2&3.3