EF Core 並發控制


並發令牌

將屬性配置為並發令牌來實現樂觀並發控制

數據注解

使用數據注解 ConcurrencyCheckAttribute 將屬性配置為並發令牌

public class Person
{
    [Key]
    public int Id { get; set; }

    [ConcurrencyCheck]
    [MaxLength(32)]
    public string FirstName { get; set; }

    [MaxLength(32)]
    public string LastName { get; set; }
}

Fluent Api

使用 Fluent Api 配置屬性為並發令牌

protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);

    builder.Entity<Person>().Property(s => s.FirstName).IsConcurrencyToken();
}

時間戳/行版本

數據庫新增或更新時會生成一個新的值賦予給配置為時間戳的屬性,此屬性也被視作為並發令牌。這樣做可以確保你在查詢一行數據后(ChangeTracker),嘗試更新此行,但在此時數據已經被其他人修改,會返回一個異常。

數據注解

使用數據注解 TimestampAttribute 將屬性標記為時間戳

public class Person
{
    [Key]
    public int Id { get; set; }

    [ConcurrencyCheck]
    [MaxLength(32)]
    public string FirstName { get; set; }

    [MaxLength(32)]
    public string LastName { get; set; }

    [Timestamp]
    public byte[] Timestamp { get; set; }
}

Fluent Api

使用 Fluent Api 標志屬性為時間戳

protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);

    builder.Entity<Person>()
        .Property(s => s.FirstName).IsConcurrencyToken();

    builder.Entity<Person>()
        .Property(s => s.Timestamp).IsRowVersion();
}

數據遷移腳本

添加遷移

 protected override void Up(MigrationBuilder migrationBuilder)
 {
     migrationBuilder.AddColumn<byte[]>(
         name: "Timestamp",
         table: "People",
         rowVersion: true,
         nullable: true);
 }

看看 ModelSnapshot 生成的遷移

modelBuilder.Entity("LearningEfCore.Person", b =>
                    {
                        b.Property<int>("Id")
                            .ValueGeneratedOnAdd()
                            .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);

                        b.Property<string>("FirstName")
                            .IsConcurrencyToken()
                            .HasMaxLength(32);

                        b.Property<string>("LastName")
                            .HasMaxLength(32);

                        b.Property<byte[]>("Timestamp")
                            .IsConcurrencyToken()
                            .ValueGeneratedOnAddOrUpdate();

                        b.HasKey("Id");

                        b.ToTable("People");
                    });

ValueGeneratedOnAddOrUpdate 正是表示數據在插入與更新時自動生成

處理並發沖突

EF Core 如何檢測並發沖突

配置實體屬性為並發令牌來實現樂觀並發控制:當更新或者刪除操作在 SaveChanges 過程中出現時,EF Core 將會把數據庫中並發令牌的值與 ChangeTracker 中跟蹤的值進行比較。

  • 如果兩值相同,則操作可以完成
  • 如果不同, EF Core 會假設其他用戶執行了與當前相沖突的操作,並會終止當前的事務。

其他用戶執行的與當前用戶相沖突的操作稱為並發沖突.

數據庫提供者復制實現並發令牌的比較.

在關系型數據庫中, EF Core 在更新與刪除操作中會通過在 WHERE 條件語句中包含對並發令牌的比較,當語句執行后,EF Core 會讀取影響的行數,如果沒有任何行受影響,則會檢測到並發沖突, EF Core 也會拋出 DbUpdateConcurrencyException

下面我們通過例子來演示一下:

先創建一個 Api 控制器:

	[Route("api/[controller]")]
    [ApiController]
    public class DefaultController : ControllerBase
    {
        private readonly TestDbContext _db;

        public DefaultController(TestDbContext db)
        {
            _db = db;
        }

        [HttpPost]
        public async Task<Person> Add([FromBody]Person person)
        {
            var entry = await _db.People.AddAsync(person);
            await _db.SaveChangesAsync();

            return entry.Entity;
        }

        [HttpPut]
        public async Task<Person> Update([FromBody]Person current)
        {

            var original = await _db.People.FindAsync(current.Id);
            original.FirstName = current.FirstName;
            original.LastName = current.LastName;
            await _db.SaveChangesAsync();

            return original;
        }
    }

以 POST 方式請求 http://localhost:5000/api/default , body 使用 json 串:

{
	"firstName": "James",
	"lastName": "Rajesh"
}

返回:

{
    "id": 1,
    "firstName": "James",
    "lastName": "Rajesh",
    "timestamp": "AAAAAAAAB9E="
}

可以看到 timestamp 如我們所願,自動生成了一個並發令牌

下面我們嘗試在 SaveChanges 時修改數據庫中的值:

Update 接口中的 await _db.SaveChangesAsync(); 此行下斷點。

修改 Request Body 為:

{
    "id": 1,
    "firstName": "James1",
    "lastName": "Rajesh1",
    "timestamp": "AAAAAAAAB9E="
}

使用 PUT 方式請求 http://localhost:5000/api/default , 命中斷點后,

修改數據庫中 LastName 的值為 Rajesh2,然后 F10,我們會得到如下並發異常:

DbUpdateConcurrencyException: Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded.

此時有人可能會疑惑了, EF Core 是如何檢測到 LastName 變更了的呢? 其實不然, 是我們在修改數據庫中數據的時候, RawVersion 列 Timestamp 自動會更新。而且每一次我們使用 EF Core 更新的時候, 產生的語句是這樣的(通過控制台日志可以看到):

Executed DbCommand (68ms) [Parameters=[@p2='?' (DbType = Int32), @p0='?' (Size = 32), @p3='?' (Size = 32), @p1='?' (Size = 32), @p4='?' (Size = 8) (DbType = Binary)], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; UPDATE [People] SET [FirstName] = @p0, [LastName] = @p1 WHERE [Id] = @p2 AND [FirstName] = @p3 AND [Timestamp] = @p4;

這里會使用 WHERE 條件進行判斷 Timestamp 是否一致

下面去掉 Timestamp 列, 留下標志為 ConcurrencyTokenFirstName

使用 PUT 方式請求 http://localhost:5000/api/default, body 為:

{
    "id": 1,
    "firstName": "James6",
    "lastName": "Rajesh11"
}

再來在 SaveChanges 的時候修改數據庫中對應記錄的 LastName 的值為 Rajesh19 , 此時沒報錯,返回值為:

{
    "id": 1,
    "firstName": "James6",
    "lastName": "Rajesh11"
}

數據庫的值也被修改為 Rajesh11. 說明這里沒有檢測到並發,下面我們嘗試修改 FirstName為 James12, 同時在 SaveChanges 時修改為 Rajesh13, 此時就檢測到了並發沖突, 我們看控制台的語句為:

Executed DbCommand (63ms) [Parameters=[@p1='?' (DbType = Int32), @p0='?' (Size = 32), @p2='?' (Size = 32)], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; UPDATE [People] SET [FirstName] = @p0 WHERE [Id] = @p1 AND [FirstName] = @p2;

看得到這里會判斷 FirstNameChangeTracker 中的值進行比較,執行之后沒有受影響的行,所以才會檢測到並發沖突。

EF Core 如何處理並發沖突

先來了解下幫助解決並發沖突的三組值:

  • Current values 當前值: 應用程序嘗試寫入數據庫的值
  • Original values 原始值: 被 EF Core 從數據庫檢索出來的值,位於任何更新操作之前的值
  • Database values 數據庫值: 當前數據庫實際存儲的值

SaveChanges 時,如果捕獲了 DbUpdateConcurrencyException , 說明發生了並發沖突,使用 DbUpdateConcurrencyException.Entries 為受影響的實體准備一組新值,重新獲取數據庫中的值的並發令牌來刷新 Original values , 然后重試直到沒有任何沖突產生。

        [HttpPut]
        public async Task<Person> Update([FromBody]Person current)
        {
            Person original = null;

            try
            {
                original = await _db.People.FindAsync(current.Id);
                original.FirstName = current.FirstName;
                original.LastName = current.LastName;
                await _db.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException e)
            {
                foreach (var entry in e.Entries)
                {
                    var currentValues = entry.CurrentValues;
                    var databaseValues = await entry.GetDatabaseValuesAsync();

                    if (entry.Entity is Person person)
                    {
                        // 更新什么值取決於實際需要

                        person.FirstName = currentValues[nameof(Person.FirstName)]?.ToString();
                        person.LastName = currentValues[nameof(Person.LastName)]?.ToString();

                        // 這步操作是為了刷新當前 Tracker 的值, 為了通過下一次的並發檢查
                        entry.OriginalValues.SetValues(databaseValues);
                    }
                }

                await _db.SaveChangesAsync();
            }

            return original;
        }

這步操作也可以加入重試策略.

此時,即使在 SaveChange 的時候更新了數據庫中的值(或者其他用戶修改了同一實體的值),觸發了並發沖突,也可以解決沖突修改為我們想要的數據。

代碼地址 https://github.com/RajeshJs/Learning/tree/master/LearningEfCore


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM