1、高併發的情況,時常會發生數據不穩定的情況 在看本節內容之前,請先看上一章SqlServer 高併發的情況下,如何利用鎖保證數據的穩定性 本節內容,也是具體討論如何在EF中實現這些操作 2、場景模擬,同上一章,搶券 EF 不考慮高併發的情況下,搶券代碼為: string _currOwner = ...
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
感謝關註!!