Optimistické riadenie súbežnosti zabraňuje strate údajov v dôsledku aktualizácií posledného víťazstva s viacerými prístupmi (SQL Server)

Stránka aktualizovaná :
Dátum vytvorenia strany :

Prevádzkové prostredie

Visual Studio
  • Visual Studio 2022
.SIEŤ
  • .NET 8
Jadro rámca entít
  • Entity Framework Core 8.0
SQL Server
  • SQL Server 2022

* Vyššie uvedené je overovacie prostredie, ale môže fungovať aj s inými verziami.

Informácie o vyhrávaní aktualizácií bez kontroly

Vo webovej aplikácii alebo aplikácii klient-server môžu viacerí ľudia pristupovať k údajom z jednej databázy a aktualizovať ich. Ak sa nič konkrétne neurobí, údaje osoby, ktorá ho neskôr aktualizovala, sa v databáze prejavia ako najnovšie.

Za normálnych okolností nie je žiadny zvláštny problém, že údaje osoby, ktorá aktualizovala neskôr, sa prejavia ako najnovšie. Problémy môžu nastať, keď sa viacerí ľudia pokúšajú získať prístup k rovnakým údajom a aktualizovať ich súčasne.

Predpokladajme napríklad, že máte nasledujúce údaje knihy.

Hodnota názvu parametra
Názov knihy Databázové knihy
cena 1000

Ak dvaja ľudia otvoria obrazovku a upravia tieto údaje súčasne, zobrazí sa vyššie uvedená hodnota. Pán / pani A sa snaží zvýšiť cenu tejto knihy o 500 jenov. Pán/pani B je neskôr inštruovaný, aby zvýšil cenu tejto knihy o ďalších 300 jenov.

Ak títo dvaja zvýšia cenu samostatne namiesto súčasne, cena knihy bude 1800 jenov. Ak k nemu pristupujete súčasne, bude zaregistrovaný ako 1500 jenov a 1300 jenov, takže to nebude 1800 jenov bez ohľadu na to, ktorý z nich sa zaregistruje.

Problém je v tom, že osoba, ktorá aktualizovala neskôr, ju môže aktualizovať bez toho, aby poznala informácie, ktoré boli predtým aktualizované.

Optimistická súbežnosť

Spomínaný problém je možné vyriešiť vykonaním "optimistickej kontroly súbežnosti" tentoraz. Zjednodušene vysvetliť, aký druh ovládania je "vyhrať ako prvý, ak sa pokúsite upraviť údaje v rovnakom čase". Tí, ktorí sa pokúsia obnoviť neskôr, dostanú chybu v čase aktualizácie a nebudú sa môcť zaregistrovať.

Možno si myslíte, že s tým nemôžete zaregistrovať nové údaje, ale je to len "keď sa ich pokúsite zmeniť súčasne". Je možné, aby dvaja ľudia upravovali v úplne odlišnom čase. Samozrejme, v takom prípade budú údaje poslednej aktualizovanej osoby najnovšie.

Konkrétne, aký druh kontroly spracovania možno dosiahnuť "verziou údajov". Napríklad vo vyššie uvedenom príklade budete mať nasledujúce údaje.

Hodnota názvu parametra
Názov knihy Databázové knihy
cena 1000
verzia 1

Verzia sa zvýši o 1 pre každú aktualizáciu záznamu. Napríklad, ak pán/pani A nastaví cenu na 1500 jenov, verzia bude 2. V tom čase je podmienkou, že aktualizáciu je možné vykonať, že verzia pred aktualizáciou je rovnaká ako verzia v databáze. Keď ho pán / pani aktualizuje, verzia v databáze je 1 a verzia pôvodných údajov, ktoré sa práve upravujú, je 1, takže ju možno aktualizovať.

Na základe tejto špecifikácie predpokladajme situáciu, keď pán/pani A a pán/pani B upravujú rovnaké údaje. Keď pán / pani prvýkrát stanovil cenu na 1500 jenov a pokúsil sa aktualizovať databázu, verzia bola rovnaká, takže ju bolo možné aktualizovať. V takom prípade bude verzia v databáze 2. Pán/pani B sa pokúša upraviť údaje za 1000 jenov a aktualizovať ich do databázy ako 1300 jenov. Aktualizácia zlyhá, pretože dostupná verzia je 1, ale verzia v databáze je už 2. Takto funguje optimistická súbežnosť.

Entity Framework Core obsahuje túto "optimistickú súbežnosť" hneď po vybalení, vďaka čomu je jeho implementácia relatívne jednoduchá.

Mimochodom, "optimistická súbežnosť" je známa aj ako "optimistická zámok" alebo "optimistická zmoka" a niekedy sa skúma a hovorí sa o nej pod týmto názvom. Ide o spôsob uzamknutia, že údaje nie je možné aktualizovať, ale údaje je možné prečítať. Existuje aj ovládací prvok nazývaný "pesimistický zámok" ako iná metóda uzamknutia ako "optimistický zámok". Ide o metódu, ktorá uzamkne načítanie údajov, keď ich prečíta prvá osoba, a neumožňuje ani operácie úprav. Môže vyriešiť problém, že údaje nie je možné aktualizovať, aj keď boli zmenené, ale keď niekto upravuje údaje, ostatní ľudia nemôžu otvoriť obrazovku úpravy údajov a ak odomknutie zlyhá, údaje budú navždy uzamknuté. Obe majú výhody a nevýhody, takže záleží na operácii, ktorú z nich prijme.

Vytvorenie databázy

V tomto článku vysvetlím, ako najskôr vytvoriť databázu pre SQL Server a potom automaticky vygenerovať kód. Ak ho chcete implementovať spôsobom zameraným na kód, tentoraz sa pozrite na automaticky vygenerovaný kód a implementujte ho opačným postupom.

Vytvorenie databázy

Môžete to urobiť aj v SQL, ale je jednoduchšie to urobiť pomocou GUI, takže to tentoraz robím s GUI. Okrem názvu databázy je predvolene vytvorená.

Vytvorenie tabuľky

Vytvorte ho pomocou nasledujúceho 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äčšinu parametrov sa nemusíte starať, pretože slúžia len na aktualizáciu dát. Tentokrát RowVersion je zaujímavým parametrom stĺpec, ktorý popisuje . Toto je verzia záznamov. Zadaním timestamp typu sa verzia automaticky zvýši pri každej aktualizácii záznamu. Keďže táto verzia je spravovaná na základe jednotlivých tabuliek, v podstate neexistuje žiadny záznam o tej istej verzii, pokiaľ ju nenastavíte manuálne.

Pridanie záznamu

Môžete ho pridať pomocou nasledujúceho 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 Stĺpce nemusíte nastavovať, pretože sa nastavujú automaticky.

Vytvorenie projektu a automatické generovanie kódu

Tentokrát skontrolujeme fungovanie pomocou konzolovej aplikácie. Kroky na vytvorenie projektu a automatické generovanie kódu sú popísané v nasledujúcich tipoch, takže sa nimi tu nebudem zaoberať.

Vygenerovaný kód je nasledovný:

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 fungovania optimistickej kontroly súbežnosti

Keďže ho tentokrát spúšťame v jednej aplikácii, nepristupujeme k nemu striktne súčasne, ale radi by sme ho implementovali vo forme, ktorá je mu blízka.

Rovnako ako v príklade na začiatku, získajú sa dva dáta a keď sa každý aktualizuje na základe prvých údajov, skontrolujte, či 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 dlhý príbeh, ale väčšina z neho je napísaná na konzolu.

Výsledok popravy je nasledovný.

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

処理を終了します。

Rozdelím to na časti.

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áram dva databázové kontexty, Ak zdieľate jeden kontext databázy, pri čítaní údajov sa uloží do vyrovnávacej pamäte a bude to rovnaká inštancia. Za predpokladu, že sa ku každému pristupuje samostatne, vytvoria sa dva databázové kontexty. dbContextC slúži na kontrolu hodnôt v databáze. V skutočnosti to nepotrebujem, pretože A alebo B sa dajú nahradiť.

// それぞれがデータを編集しようと読み込む
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 predpokladu, že sa k nim pristupuje samostatne, čítajú jeden z rôznych databázových kontextov Book . V tejto chvíli sme nič nezmenili, takže sú všetky rovnaké.

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

Najprv som zmenil a aktualizoval knihu pána/pani. Proces aktualizácie je UpdateToDatabase zhrnutý v metóde a keď sa vyskytne výnimka, zobrazí sa hlásenie.

// 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ýsledkom bolo, že bola úspešne aktualizovaná a Price RowVersion kniha A bola aktualizovaná. Ak si priamo prečítate hodnotu DB, aktualizovaná hodnota bude rovnaká ako aktualizovaná hodnota. Mohol som to aktualizovať, pretože obaja sa dostali do RowVersion A a RowVersion do DB boli AAAAAAAAH2k= . Verzia sa v dôsledku aktualizácie zmenila AAAAAAAAH2o= .

Po aktualizácii A aktualizujte B rovnakým spôsobom.

// そのあと 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ýsledkom je výnimka DbUpdateConcurrencyException a aktualizácia zlyhala. bookB.RowVersion AAAAAAAAH2k= je , ale RowVersion keďže je už AAAAAAAAH2o= , posudzuje sa ako nesúlad a vyskytne sa chyba aktualizácie. Môžete vidieť, že lokálna Price premenná bookB sa zmenila, ale RowVersion Môžete vidieť, že hodnoty na strane databázy sa vôbec nezmenili.

Súhrn

Toto je najjednoduchší z niekoľkých typov zámkov na implementáciu, pretože keď použijete automaticky vygenerovaný kód v SQL Server a Entity Framework Core, optimistická súbežnosť je predvolene implementovaná. Keďže však ide len o zabránenie "poškodeniu údajov, ktoré je potrebné aktualizovať", je potrebné správne zaobchádzať s výnimkami, keď ide o iné údaje alebo operácie používateľa.

Tentokrát som tiež nič nezvládol, pretože som to implementoval RowVersion do jednoduchej konzolovej aplikácie. Ak chcete vložiť obrazovku úprav po načítaní údajov vo webovej aplikácii alebo klientskej aplikácii, RowVersion nejakým spôsobom, aby sa dal správne určiť pri aktualizácii.

Entity Framework Core má však funkciu sledovania zmien, takže ak chcete nastaviť starú RowVersion hodnotu načítanú z databázy v čase aktualizácie,

bookB.RowVersion = <古い RowVersion>;

Aj keď je nastavená nasledovne, nebude správne hodnotená ako "optimistická kontrola súbežnosti". RowVersion Aj keď nastavíte hodnotu na normálne, bude rozpoznaná iba ako zmenená hodnota, takže nasledujúci je

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

Pred zmenou je potrebné prepísať hodnotu.