EF Code 如何應對高併發

wskxy發表於2023-03-17

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

 

  感謝關注!!

 

 

 

 

 

 

 

 

 

   

 

相關文章