Optimistické řízení souběžnosti zabraňuje ztrátě dat v důsledku last-win aktualizací s více přístupy (SQL Server)

Stránky aktualizovány :
Datum vytvoření stránky :

Provozní prostředí

Visual Studio
  • Visual Studio 2022
.SÍŤ
  • .NET 8
Jádro Entity Framework
  • Entity Framework Core 8.0
SQL Server
  • SQL Server 2022

* Výše uvedené je ověřovací prostředí, ale může fungovat i s jinými verzemi.

O vítězných aktualizacích bez kontroly

Ve webové aplikaci nebo aplikaci klient-server může více lidí přistupovat k datům z jedné databáze a aktualizovat je. Pokud se neudělá nic konkrétního, údaje osoby, která je později aktualizovala, se v databázi projeví jako nejnovější.

Za normálních okolností není žádný zvláštní problém, že údaje osoby, která aktualizovala později, jsou zobrazeny jako nejnovější. Problémy mohou nastat, když se více lidí pokusí získat přístup ke stejným datům a aktualizovat je současně.

Předpokládejme například, že máte následující údaje o knize.

Název parametru Hodnota
Název knihy Databázové knihy
cena 1000

Pokud obrazovku pro úpravu těchto dat otevřou dvě osoby současně, zobrazí se výše uvedená hodnota. Pan a paní A se snaží zvýšit cenu této knihy o 500 jenů. Pan B. je později instruován, aby zvýšil cenu této knihy o dalších 300 jenů.

Pokud oba zvýší cenu odděleně, a ne současně, cena knihy bude 1800 jenů. Pokud k němu přistoupíte současně, bude zaregistrován jako 1500 jenů a 1300 jenů, takže to nebude 1800 jenů bez ohledu na to, který z nich se zaregistruje.

Problém je v tom, že osoba, která jej aktualizovala později, jej může aktualizovat, aniž by znala informace, které byly dříve aktualizovány.

Optimistická souběžnost

Výše uvedený problém lze vyřešit tím, že tentokrát provedete "optimistické řízení souběžnosti". Zjednodušeně si vysvětlíme, co je to za kontrolu, která "vyhrajete jako první, pokud se pokusíte upravit data současně". Těm, kteří se pokusí o obnovení později, se při načasování aktualizace zobrazí chyba a nebudou se moci zaregistrovat.

Možná si myslíte, že s tím nemůžete registrovat nová data, ale to je pouze "když se to pokusíte změnit současně". Je možné, aby dva lidé upravovali ve zcela odlišných časech. Samozřejmě v takovém případě budou údaje poslední aktualizované osoby nejnovější.

Konkrétně, jakého druhu kontroly zpracování lze dosáhnout tím, že "budete mít verzi dat". Například ve výše uvedeném příkladu budete mít následující data.

Název parametru Hodnota
Název knihy Databázové knihy
cena 1000
verze 1

Verze se zvýší o 1 pro každou aktualizaci záznamu. Pokud například pan / paní A nastaví cenu na 1500 jenů, verze bude 2. V tomto okamžiku je podmínkou pro provedení aktualizace to, že verze před aktualizací je stejná jako verze v databázi. Když jej pan/paní aktualizuje, verze v databázi je 1 a verze původních dat, která jsou aktuálně upravována, je 1, takže je lze aktualizovat.

Na základě této specifikace předpokládejme situaci, kdy p./paní A a pan/paní B upravují stejná data. Když pan / paní poprvé nastavili cenu na 1500 jenů a pokusili se aktualizovat databázi, verze byla stejná, takže ji bylo možné aktualizovat. V takovém případě bude verze v databázi 2. Pan / paní B se pokusí upravit data 1000 jenů a aktualizovat je do databáze jako 1300 jenů. Aktualizace se nezdařila, protože k dispozici je verze 1, ale verze v databázi je již 2. Takto funguje optimistická souběžnost.

Entity Framework Core obsahuje tuto "optimistickou souběžnost" hned po vybalení, takže je relativně snadné ji implementovat.

Mimochodem, "optimistická souběžnost" je také známá jako "optimistický zámek" nebo "optimistický zámek" a někdy se pod tímto názvem zkoumá a mluví se o ní. Jedná se o metodu zamykání, při které nelze data aktualizovat, ale lze je číst. K dispozici je také ovládací prvek nazvaný "pesimistický zámek" jako další metoda zamykání jiná než "optimistický zámek". Jedná se o metodu, která uzamkne načítání dat, když data čte první osoba, a neumožňuje ani operace úprav. Může vyřešit problém, že data nelze aktualizovat, i když byla změněna, ale když někdo upravuje data, ostatní lidé nemohou otevřít obrazovku pro úpravu dat, a pokud se odemknutí nezdaří, data budou navždy uzamčena. Oba mají výhody a nevýhody, takže záleží na operaci, kterou z nich přijmout.

Vytvoření databáze

V tomto článku vysvětlím, jak nejprve vytvořit databázi pro SQL Server a poté automaticky vygenerovat kód. Pokud jej chcete implementovat nejprve kódem, podívejte se tentokrát na automaticky generovaný kód a implementujte jej v opačném postupu.

Vytvoření databáze

Můžete to udělat i v SQL, ale je jednodušší to udělat s GUI, takže to tentokrát dělám s GUI. S výjimkou názvu databáze je ve výchozím nastavení vytvořena.

Vytvoření tabulky

Vytvořte ho pomocí následujícího SQL:

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

O většinu parametrů se nemusíte starat, protože slouží pouze k aktualizaci dat. Parametrem, který nás tentokrát zajímá, je RowVersion sloupec, který popisuje . Jedná se o verzování záznamů. Zadáním jako timestamp typu se verze automaticky zvýší při každé aktualizaci záznamu. Vzhledem k tomu, že tato verze je spravována na základě jednotlivých tabulek, neexistuje v podstatě žádný záznam o stejné verzi, pokud ji nenastavíte ručně.

Přidání záznamu

Můžete ho přidat pomocí následujícího SQL:

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 Sloupce nemusíte nastavovat, protože se nastavují automaticky.

Vytvoření projektu a automatické generování kódu

Tentokrát zkontrolujeme fungování pomocí konzolové aplikace. Kroky k vytvoření projektu a automatickému generování kódu jsou popsány v následujících tipech, proto se jimi zde nebudu zabývat.

Vygenerovaný kód je následující:

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!;
}

Kontrola provozu optimistického řízení souběžnosti

Vzhledem k tomu, že ji tentokrát provozujeme v jedné aplikaci, nepřistupujeme k ní striktně současně, ale rádi bychom ji implementovali v podobě, která je jí blízká.

Stejně jako v příkladu na začátku jsou získány dvě části dat, a když je každá z nich aktualizována na základě prvních dat, zkontrolujte, zda druhý aktualizátor nezobrazí chybu.

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);
  }
}

Je to dlouhý příběh, ale většina z něj je napsána na konzoli.

Výsledek provedení je následující.

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="}

処理を終了します。

Rozdělím to na části.

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("データベースコンテキストを作成しました。");

Vytvářím dva databázové kontexty, Pokud sdílíte jeden databázový kontext, bude při čtení dat uložen do mezipaměti a bude se jednat o stejnou instanci. Za předpokladu, že se ke každému přistupuje samostatně, vytvoří se dva databázové kontexty. dbContextC slouží ke kontrole hodnot v databázi. Opravdu to nepotřebuji, protože A nebo B lze dosadit.

// それぞれがデータを編集しようと読み込む
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))}");

Za předpokladu, že se k nim přistupuje samostatně, čtou jeden z různých databázových kontextů Book . V tuto chvíli jsme nic nezměnili, takže jsou všechny stejné.

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="}

Nejprve jsem změnil a aktualizoval knihu pana a paní. Proces aktualizace je UpdateToDatabase shrnut v metodě a když dojde k výjimce, zobrazí se zpráva.

// 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="}

V důsledku toho byla úspěšně aktualizována a Price RowVersion kniha A byly aktualizovány. Pokud si přečtete hodnotu databáze přímo, aktualizovaná hodnota bude stejná jako aktualizovaná hodnota. Byl jsem schopen to aktualizovat, protože oba se dostali do RowVersion A a RowVersion do DB byli AAAAAAAAH2k= . Verze se kvůli aktualizaci změnila AAAAAAAAH2o= .

Po aktualizaci A aktualizujte B stejným způsobem.

// そのあと 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="}

Výsledkem je výjimka DbUpdateConcurrencyException a aktualizace se nezdařila. bookB.RowVersion AAAAAAAAH2k= je , ale RowVersion protože je již AAAAAAAAH2o= , je to posouzeno jako nesoulad a dojde k chybě aktualizace. Vidíte, že Price místní proměnná bookB se změnila, ale RowVersion Můžete vidět, že hodnoty na straně databáze se vůbec nezměnily.

Shrnutí

Jedná se o nejjednodušší z několika typů zámků, které lze implementovat, protože při použití automaticky generovaného kódu v SQL Server a Entity Framework Core je ve výchozím nastavení implementována optimistická souběžnost. Protože se však jedná pouze o to, aby se zabránilo "poškození dat, které je třeba aktualizovat", je nutné řádně zpracovávat výjimky, pokud se jedná o jiná data nebo uživatelské operace.

Také jsem tentokrát nic nespravoval, protože jsem to implementoval RowVersion v jednoduché konzolové aplikaci. Pokud chcete vložit obrazovku pro úpravy po načtení dat ve webové aplikaci nebo klientské aplikaci, RowVersion nějakým způsobem, aby bylo možné správně určit, kdy je aktualizován.

Entity Framework Core má ale funkci sledování změn, takže pokud chcete nastavit starou RowVersion hodnotu na hodnotu načtenou z databáze v době aktualizace,

bookB.RowVersion = <古い RowVersion>;

I když je nastaven následovně, nebude správně posouzen jako "optimistické řízení souběžnosti". RowVersion I když nastavíte hodnotu na normální, bude rozpoznána pouze jako změněná hodnota, takže je následující

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

Před změnou je nutné hodnotu přepsat.