İyimser Eşzamanlılık Denetimi, Birden Çok Erişimli Son Kazanç Güncelleştirmeleri Nedeniyle Veri Kaybını Önler (SQL Server)

Sayfa güncel :
Sayfa oluşturma tarihi :

Çalışma ortamı

Görsel Stüdyo
  • Görsel Studio 2022
.NET
  • .NET 8
Entity Framework Çekirdeği
  • Entity Framework Core 8.0
SQL Sunucusu
  • SQL Sunucu 2022

* Yukarıdakiler bir doğrulama ortamıdır, ancak diğer sürümlerle de çalışabilir.

Kontrol Olmadan Güncelleme Kazanma Hakkında

Bir web uygulamasında veya istemci-sunucu uygulamasında, birden çok kişi tek bir veritabanından verilere erişebilir ve bunları güncelleyebilir. Özellikle hiçbir şey yapılmazsa, daha sonra güncelleyen kişinin verileri veri tabanına en son olarak yansıtılacaktır.

Normalde, daha sonra güncelleme yapan kişinin verilerinin en son olarak yansıtılması gibi belirli bir sorun yoktur. Birden fazla kişi aynı verilere aynı anda erişmeye ve bunları güncellemeye çalıştığında sorunlar ortaya çıkabilir.

Örneğin, aşağıdaki kitap verilerine sahip olduğunuzu varsayalım.

Parametre Adı Değeri
Kitabın adı Veritabanı Kitapları
fiyat 1000

İki kişi bu verileri aynı anda düzenlemek için ekranı açarsa, yukarıdaki değer görüntülenecektir. Bay / Bayan A, bu kitabın fiyatını 500 yen artırmaya çalışıyor. Bay / Bayan B'ye daha sonra bu kitabın fiyatını 300 yen daha artırması talimatı verildi.

İkisi fiyatı aynı anda değil de ayrı ayrı yükseltirse, kitabın fiyatı 1800 yen olacaktır. Aynı anda erişirseniz, sırasıyla 1500 yen ve 1300 yen olarak kaydedilecektir, bu nedenle hangisi kayıt olursa olsun 1800 yen olmayacaktır.

Buradaki sorun, daha sonra güncelleme yapan kişinin daha önce güncellenen bilgileri bilmeden güncelleyebilmesidir.

İyimser eşzamanlılık

Bahsi geçen sorun, bu kez "iyimser eşzamanlılık kontrolü" yapılarak çözülebilir. Basitçe açıklamak gerekirse, "verileri aynı anda düzenlemeye çalışırsanız önce kazanmak" ne tür bir kontroldür. Daha sonra yenilemeye çalışanlar, güncelleme zamanlamasında bir hata alacak ve kayıt olamayacaklar.

Bununla yeni veriler kaydedemeyeceğinizi düşünebilirsiniz, ancak bu yalnızca "aynı anda değiştirmeye çalıştığınızda" geçerlidir. İki kişinin tamamen farklı zamanlarda düzenleme yapması mümkündür. Tabii ki, bu durumda, en son güncellenen kişinin verileri en son olacaktır.

Özellikle, "verilerin bir versiyonuna sahip olarak" ne tür bir işleme kontrolünün elde edilebileceği. Örneğin, yukarıdaki örnekte aşağıdaki verilere sahip olacaksınız.

Parametre Adı Değeri
Kitabın adı Veritabanı Kitapları
fiyat 1000
Sürüm 1

Sürüm, her kayıt güncelleştirmesi için 1 artırılır. Örneğin, Bay/Bayan A fiyatı 1500 yen olarak ayarlarsa, sürüm 2 olacaktır. O zaman güncellemenin yapılabilmesi şartı, güncellemeden önceki sürümün veritabanındaki sürüm ile aynı olmasıdır. Mr./Ms. bunu güncellediğinde, veritabanındaki sürüm 1'dir ve şu anda düzenlenmekte olan orijinal verinin sürümü 1'dir, bu nedenle güncellenebilir.

Bu belirtime dayanarak, Bay/Bayan A ve Bay/Bayan B'nin aynı verileri düzenlediği bir durumu varsayalım. Bay / Bayan fiyatı ilk kez 1500 yen olarak belirlediğinde ve veritabanını güncellemeye çalıştığında, sürüm aynıydı, bu yüzden güncellenebilirdi. Bu durumda, veritabanındaki sürüm 2 olacaktır. Bay / Bayan B, 1000 yen verilerini düzenlemeye ve veritabanına 1300 yen olarak güncellemeye çalışır. Eldeki sürüm 1 olduğu, ancak veritabanındaki sürüm zaten 2 olduğu için güncelleştirme başarısız olur. İyimser eşzamanlılık bu şekilde çalışır.

Entity Framework Core, bu "iyimser eşzamanlılığı" kullanıma hazır hale getirir ve uygulamayı nispeten kolaylaştırır.

Bu arada "iyimser eşzamanlılık", "iyimser kilit" veya "iyimser kilit" olarak da bilinir ve bazen bu isimle incelenir ve konuşulur. Verilerin güncellenemediği ancak verilerin okunabildiği bir kilitleme yöntemidir. "İyimser kilit" dışında bir diğer kilitleme yöntemi olarak da "kötümser kilit" adı verilen bir kontrol vardır. Bu, ilk kişi verileri okuduğunda veri yüklemesini kilitleyen ve düzenleme işlemlerine bile izin vermeyen bir yöntemdir. Verilerin değiştirilmiş olmasına rağmen güncellenememesi sorununu çözebilir, ancak birisi verileri düzenlerken, diğer kişiler veri düzenleme ekranını açamaz ve kilit açma başarısız olursa veriler sonsuza kadar kilitlenir. Her ikisinin de avantajları ve dezavantajları vardır, bu nedenle hangisinin benimseneceği operasyona bağlıdır.

Veritabanı oluşturma

Bu yazımda öncelikle SQL Server için bir veritabanı nasıl oluşturulur ve daha sonra otomatik olarak nasıl kod oluşturulur onu anlatacağım. Bunu kod öncelikli bir şekilde uygulamak istiyorsanız, lütfen bu sefer otomatik olarak oluşturulan koda bakın ve ters prosedürde uygulayın.

Veritabanı oluşturma

SQL'de de yapabilirsiniz, ancak bunu bir GUI ile yapmak daha kolaydır, bu yüzden bu sefer bir GUI ile yapıyorum. Veritabanı adı dışında, varsayılan olarak oluşturulur.

Tablo oluşturma

Aşağıdaki SQL ile oluşturun:

USE [TestDatabase]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Book](
	[ID] [int] NOT NULL,
	[Name] [nvarchar](100) NOT NULL,
	[Price] [money] NOT NULL,
	[RowVersion] [timestamp] NOT NULL,
 CONSTRAINT [PK_Book] PRIMARY KEY CLUSTERED 
(
	[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

Parametrelerin çoğu için endişelenmenize gerek yok çünkü bunlar yalnızca veri güncellemeleri içindir. Bu seferki ilgilenilen parametre, tanımlayan sütundur RowVersion . Bu, kayıt sürümü oluşturmadır. Tür olarak timestamp belirtildiğinde, kayıt her güncelleştirildiğinde sürüm otomatik olarak artırılır. Ayrıca, bu sürüm tablo bazında yönetildiğinden, manuel olarak ayarlamadığınız sürece temelde aynı sürümün kaydı yoktur.

Kayıt ekleme

Aşağıdaki SQL ile ekleyebilirsiniz:

begin transaction;
insert into Book ([ID],[Name],[Price]) values (1,'C#の本','1000.00');
insert into Book ([ID],[Name],[Price]) values (2,'VB.NETの本','1500.00');
insert into Book ([ID],[Name],[Price]) values (3,'SQL Serverの本','2000.00');
commit;

RowVersion Sütunlar otomatik olarak ayarlandığı için sütunları ayarlamanız gerekmez.

Bir proje oluşturun ve otomatik olarak kod oluşturun

Bu sefer konsol uygulaması ile işlemi kontrol edeceğiz. Bir proje oluşturma ve otomatik olarak kod oluşturma adımları aşağıdaki ipuçlarında açıklanmıştır, bu yüzden burada bunlara girmeyeceğim.

Oluşturulan kod aşağıdaki gibidir:

TestDatabaseDbContext.cs

using Microsoft.EntityFrameworkCore;

namespace ConcurrencySqlServer.Models.Database;

public partial class TestDatabaseDbContext : DbContext
{
  public TestDatabaseDbContext() { }

  public TestDatabaseDbContext(DbContextOptions<TestDatabaseDbContext> options) : base(options) { }

  public virtual DbSet<Book> Book { get; set; }

  protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263.
    => optionsBuilder.UseSqlServer("Data Source=<サーバー名>;Database=<データベース名>;user id=<ユーザー名>;password=<パスワード>;TrustServerCertificate=true");

  protected override void OnModelCreating(ModelBuilder modelBuilder)
  {
    modelBuilder.Entity<Book>(entity =>
    {
      entity.Property(e => e.ID).ValueGeneratedNever();
      entity.Property(e => e.RowVersion)
        .IsRowVersion()
        .IsConcurrencyToken();
    });

    OnModelCreatingPartial(modelBuilder);
  }

  partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}

Book.cs

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ConcurrencySqlServer.Models.Database;

public partial class Book
{
  [Key]
  public int ID { get; set; }

  [StringLength(100)]
  public string Name { get; set; } = null!;

  [Column(TypeName = "money")]
  public decimal Price { get; set; }

  public byte[] RowVersion { get; set; } = null!;
}

İyimser eşzamanlılık denetiminin çalışmasını denetleme

Bu sefer tek bir uygulamada çalıştırdığımız için aynı anda tam olarak erişmiyoruz ancak ona yakın bir formda uygulamak istiyoruz.

Başlangıçtaki örnekte olduğu gibi, iki veri parçası alınır ve her biri ilk verilere göre güncellendiğinde, ikinci güncelleyicinin bir hata alıp almayacağını kontrol edin.

Program.cs

using ConcurrencySqlServer.Models.Database;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;

Console.WriteLine("Hello, World!");

// 2人が別々にデータベースにアクセスする想定で2つのデータベースコンテキストを作成
Console.WriteLine("データベースコンテキストを作成します。");
using var dbContextA = new TestDatabaseDbContext();
using var dbContextB = new TestDatabaseDbContext();
Console.WriteLine("データベースコンテキストを作成しました。");
Console.WriteLine("");

// それぞれがデータを編集しようと読み込む
Console.WriteLine("ID:1 の Book を読み込みます。");
var bookA = dbContextA.Book.First(x => x.ID == 1);
var bookB = dbContextB.Book.First(x => x.ID == 1);
Console.WriteLine("ID:1 の Book を読み込みました。");
Console.WriteLine($"A  Book : {JsonSerializer.Serialize(bookA)}");
Console.WriteLine($"B  Book : {JsonSerializer.Serialize(bookB)}");
Console.WriteLine($"DB Book : {JsonSerializer.Serialize(dbContextC.Book.AsNoTracking().First(x => x.ID == 1))}");
Console.WriteLine("");


// A の人が最初にデータベースに更新する
bookA.Price += 500;
UpdateToDatabase(dbContextA, "A");

Console.WriteLine($"A  Book : {JsonSerializer.Serialize(bookA)}");
Console.WriteLine($"B  Book : {JsonSerializer.Serialize(bookB)}");
Console.WriteLine($"DB Book : {JsonSerializer.Serialize(dbContextC.Book.AsNoTracking().First(x => x.ID == 1))}");
Console.WriteLine("");

// そのあと B の人が最初にデータベースに更新する
bookB.Price += 300;
UpdateToDatabase(dbContextB, "B");

Console.WriteLine($"A  Book : {JsonSerializer.Serialize(bookA)}");
Console.WriteLine($"B  Book : {JsonSerializer.Serialize(bookB)}");
Console.WriteLine($"DB Book : {JsonSerializer.Serialize(dbContextC.Book.AsNoTracking().First(x => x.ID == 1))}");
Console.WriteLine("");

Console.WriteLine("処理を終了します。");


// データベースに更新するメソッド
void UpdateToDatabase(TestDatabaseDbContext dbContext, string updateUser)
{
  try
  {
    Console.WriteLine($"{updateUser} のデータベースコンテキストを変更保存します。");
    dbContext.SaveChanges();
    Console.WriteLine($"{updateUser} のデータベースコンテキストを変更保存しました。");
  }
  catch (DbUpdateConcurrencyException ex)
  {
    Console.WriteLine($"{updateUser} の更新でエラーが発生しました。");
    Console.WriteLine(ex.Message);
  }
}

Bu uzun bir hikaye, ama çoğu konsola yazılmış.

Yürütmenin sonucu aşağıdaki gibidir.

Hello, World!
データベースコンテキストを作成します。
データベースコンテキストを作成しました。

ID:1 の Book を読み込みます。
ID:1 の Book を読み込みました。
A  Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1000.0000,"RowVersion":"AAAAAAAAH2k="}
B  Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1000.0000,"RowVersion":"AAAAAAAAH2k="}
DB Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1000.0000,"RowVersion":"AAAAAAAAH2k="}

A のデータベースコンテキストを変更保存します。
A のデータベースコンテキストを変更保存しました。
A  Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1500.0000,"RowVersion":"AAAAAAAAH2o="}
B  Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1000.0000,"RowVersion":"AAAAAAAAH2k="}
DB Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1500.0000,"RowVersion":"AAAAAAAAH2o="}

B のデータベースコンテキストを変更保存します。
B の更新でエラーが発生しました。
The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded. See https://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
A  Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1500.0000,"RowVersion":"AAAAAAAAH2o="}
B  Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1300.0000,"RowVersion":"AAAAAAAAH2k="}
DB Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1500.0000,"RowVersion":"AAAAAAAAH2o="}

処理を終了します。

Bunu bölümlere ayıracağım.

Console.WriteLine("Hello, World!");

// 2人が別々にデータベースにアクセスする想定で2つのデータベースコンテキストを作成
Console.WriteLine("データベースコンテキストを作成します。");
using var dbContextA = new TestDatabaseDbContext();
using var dbContextB = new TestDatabaseDbContext();
using var dbContextC = new TestDatabaseDbContext();
Console.WriteLine("データベースコンテキストを作成しました。");

İki veritabanı bağlamı oluşturuyorum, Bir veritabanı bağlamını paylaşırsanız, veriler okunduğunda önbelleğe alınır ve aynı örnek olur. Her birine ayrı ayrı erişildiği varsayılarak, iki veritabanı bağlamı oluşturulur. dbContextC veritabanındaki değerleri kontrol etmek içindir. Buna gerçekten ihtiyacım yok çünkü A veya B değiştirilebilir.

// それぞれがデータを編集しようと読み込む
Console.WriteLine("ID:1 の Book を読み込みます。");
var bookA = dbContextA.Book.First(x => x.ID == 1);
var bookB = dbContextB.Book.First(x => x.ID == 1);
Console.WriteLine("ID:1 の Book を読み込みました。");
Console.WriteLine($"A  Book : {JsonSerializer.Serialize(bookA)}");
Console.WriteLine($"B  Book : {JsonSerializer.Serialize(bookB)}");
Console.WriteLine($"DB Book : {JsonSerializer.Serialize(dbContextC.Book.AsNoTracking().First(x => x.ID == 1))}");

Ayrı ayrı erişildiklerini varsayarsak, farklı veritabanı bağlamlarından birini okuyorlar Book . Bu noktada, hiçbir şeyi değiştirmedik, bu yüzden hepsi aynı.

ID:1 の Book を読み込みます。
ID:1 の Book を読み込みました。
A  Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1000.0000,"RowVersion":"AAAAAAAAH2k="}
B  Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1000.0000,"RowVersion":"AAAAAAAAH2k="}
DB Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1000.0000,"RowVersion":"AAAAAAAAH2k="}

Önce Bay/Bayan'ın kitabını değiştirdim ve güncelledim. Güncelleştirme işlemi bir yöntemde özetlenir UpdateToDatabase ve bir özel durum oluştuğunda bir ileti görüntülenir.

// A の人が最初にデータベースに更新する
bookA.Price += 500;
UpdateToDatabase(dbContextA, "A");

Console.WriteLine($"A  Book : {JsonSerializer.Serialize(bookA)}");
Console.WriteLine($"B  Book : {JsonSerializer.Serialize(bookB)}");
Console.WriteLine($"DB Book : {JsonSerializer.Serialize(dbContextC.Book.AsNoTracking().First(x => x.ID == 1))}");
A のデータベースコンテキストを変更保存します。
A のデータベースコンテキストを変更保存しました。
A  Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1500.0000,"RowVersion":"AAAAAAAAH2o="}
B  Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1000.0000,"RowVersion":"AAAAAAAAH2k="}
DB Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1500.0000,"RowVersion":"AAAAAAAAH2o="}

Sonuç olarak, başarıyla güncellendi ve Price RowVersion Kitap A'nın bir kısmı güncellendi. Ayrıca, DB değerini doğrudan okursanız, güncellenen değer güncellenen değerle aynı olacaktır. Güncelleyebildim çünkü hem A'ya RowVersion girdim hem RowVersion de DB'ye girdi. AAAAAAAAH2k= Güncelleme nedeniyle sürüm değişti AAAAAAAAH2o= .

A'yı güncelledikten sonra B'yi de aynı şekilde güncelleyin.

// そのあと B の人が最初にデータベースに更新する
bookB.Price += 300;
UpdateToDatabase(dbContextB, "B");

Console.WriteLine($"A  Book : {JsonSerializer.Serialize(bookA)}");
Console.WriteLine($"B  Book : {JsonSerializer.Serialize(bookB)}");
Console.WriteLine($"DB Book : {JsonSerializer.Serialize(dbContextC.Book.AsNoTracking().First(x => x.ID == 1))}");
B のデータベースコンテキストを変更保存します。
B の更新でエラーが発生しました。
The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded. See https://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
A  Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1500.0000,"RowVersion":"AAAAAAAAH2o="}
B  Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1300.0000,"RowVersion":"AAAAAAAAH2k="}
DB Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1500.0000,"RowVersion":"AAAAAAAAH2o="}

Sonuç bir özel durumdur DbUpdateConcurrencyException ve güncelleştirme başarısız olmuştur. bookB.RowVersion AAAAAAAAH2k=, ancak RowVersion zaten AAAAAAAAH2o= olduğu için , bir uyumsuzluk olduğuna karar verilir ve bir güncelleme hatası oluşur. Yerel değişkenin Price bookB değiştiğini görebilirsiniz, ancak RowVersion Veritabanı tarafındaki değerlerin hiç değişmediğini görebilirsiniz.

Özet

SQL Server ve Entity Framework Core'da otomatik olarak oluşturulan kodu kullandığınızda iyimser eşzamanlılık varsayılan olarak uygulandığından, bu, uygulanması en kolay olan birkaç kilit türüdür. Ancak, yalnızca "güncellenecek veri bozulmasını" önlemek olduğundan, diğer veriler söz konusu olduğunda veya kullanıcı işlemleri söz konusu olduğunda istisnaları uygun şekilde ele almak gerekir.

Ayrıca bu sefer basit bir konsol uygulamasında uyguladığım RowVersion için hiçbir şeyi başaramadım. Verileri bir web uygulamasına veya istemci uygulamasına yükledikten sonra düzenleme ekranını eklemek isterseniz, RowVersion bir şekilde, güncellendiğinde doğru bir şekilde belirlenebilmesi için.

Ancak, Entity Framework Core bir değişiklik izleme işlevine sahiptir, bu nedenle eskisini RowVersion güncelleştirme sırasında DB'den okunan değere ayarlamak istiyorsanız,

bookB.RowVersion = <古い RowVersion>;

Aşağıdaki gibi ayarlansa bile, "iyimser eşzamanlılık kontrolü" olarak doğru bir şekilde değerlendirilmeyecektir. RowVersion Değeri normal olarak ayarlasanız bile, yalnızca değiştirilen değer olarak tanınacaktır, bu nedenle aşağıdakiler

dbContextB.Entry(bookB).Property("RowVersion").OriginalValue = <古い RowVersion>;

Değişiklikten önce değeri yeniden yazmak gerekir.