Optimistični nadzor sočasnosti preprečuje izgubo podatkov zaradi posodobitev zadnje zmage z več dostopi (SQL Server)

Stran posodobljena :
Datum ustvarjanja strani :

Delovno okolje

Visual Studio
  • Visual Studio 2022
.MREŽA
  • .NET 8
Jedro ogrodja entitet
  • Jedro ogrodja entitet 8.0
SQL Server
  • SQL Server 2022

* Zgoraj navedeno je okolje za preverjanje, vendar lahko deluje z drugimi različicami.

O zmagovalnih posodobitvah brez nadzora

V spletni aplikaciji ali aplikaciji odjemalec-strežnik lahko več oseb dostopa do podatkov iz ene zbirke podatkov in jih posodablja. Če se ne stori nič posebnega, se podatki osebe, ki jo je pozneje posodobila, odražajo v bazi podatkov kot najnovejši.

Običajno ni posebnega problema, da se podatki osebe, ki je pozneje posodobila, odražajo kot najnovejši. Do težav lahko pride, ko več oseb poskuša dostopati do istih podatkov in jih posodobiti hkrati.

Recimo, da imate te podatke o knjigi.

Ime parametra Vrednost
Ime knjige Zbirke podatkov Knjige
cena 1000

Če dve osebi hkrati odpreta zaslon za urejanje teh podatkov, se prikaže zgornja vrednost. Gospod / gospa A poskuša dvigniti ceno te knjige za 500 jenov. Gospodu/gospe B je kasneje naročeno, naj dvigne ceno te knjige za dodatnih 300 jenov.

Če oba dvigneta ceno ločeno namesto hkrati, bo cena knjige 1800 jenov. Če dostopate do njega hkrati, bo registriran kot 1500 jenov oziroma 1300 jenov, tako da ne bo 1800 jenov, ne glede na to, kateri se registrira.

Težava je v tem, da jo lahko oseba, ki jo je pozneje posodobila, ne da bi poznala informacije, ki so bile prej posodobljene.

Optimistična sočasnost

Omenjeni problem je mogoče tokrat rešiti z izvajanjem "optimističnega nadzora sočasnosti". Preprosto razložiti, kakšen nadzor je "najprej zmagati, če poskušate hkrati urediti podatke". Tisti, ki poskušajo obnoviti pozneje, bodo ob posodobitvi prejeli napako in se ne bodo mogli registrirati.

Morda mislite, da s tem ne morete registrirati novih podatkov, vendar je to le "če jih poskušate spremeniti hkrati". Dve osebi lahko urejata ob popolnoma različnih časih. Seveda bodo v tem primeru podatki zadnje posodobljene osebe najnovejši.

Natančneje, kakšen nadzor obdelave je mogoče doseči z "različico podatkov". Na primer, v zgornjem primeru boste imeli naslednje podatke.

Ime parametra Vrednost
Ime knjige Zbirke podatkov Knjige
cena 1000
različica 1

Različica se poveča za 1 za vsako posodobitev zapisa. Na primer, če g. / gospa A nastavi ceno na 1500 jenov, bo različica 2. Takrat je pogoj, da je posodobitev mogoča, da je različica pred posodobitvijo enaka različici v zbirki podatkov. Ko ga gospod / gospa posodobi, je različica v bazi podatkov 1, različica izvirnih podatkov, ki se trenutno urejajo, pa je 1, tako da jo je mogoče posodobiti.

Na podlagi te specifikacije predpostavimo, da g. / gospa A in gospod / gospa B urejata iste podatke. Ko je gospod / gospa prvič določil ceno na 1500 jenov in poskušal posodobiti bazo podatkov, je bila različica enaka, tako da jo je bilo mogoče posodobiti. V tem primeru bo različica v bazi podatkov 2. G. / gospa B poskuša urediti podatke za 1000 jenov in jih posodobiti v bazo podatkov kot 1300 jenov. Posodobitev ne uspe, ker je razpoložljiva različica 1, različica v zbirki podatkov pa je že 2. Tako deluje optimistična sočasnost.

Entity Framework Core vključuje to "optimistično sočasnost", zaradi česar je razmeroma enostavno izvajati.

Mimogrede, »optimistična sočasnost« je znana tudi kot »optimistična ključavnica« ali »optimistična ključavnica« in se včasih preučuje in govori o njej s tem imenom. To je metoda zaklepanja, da podatkov ni mogoče posodobiti, lahko pa jih je mogoče prebrati. Obstaja tudi kontrola, imenovana "pesimistična ključavnica" kot druga metoda zaklepanja, ki ni "optimistična ključavnica". To je metoda, ki zaklene nalaganje podatkov, ko prva oseba prebere podatke, in ne dovoljuje niti postopkov urejanja. To lahko reši težavo, da podatkov ni mogoče posodobiti, čeprav so bili spremenjeni, ko pa nekdo ureja podatke, drugi ljudje ne morejo odpreti zaslona za urejanje podatkov, in če odklepanje ne uspe, bodo podatki za vedno zaklenjeni. Oba imata prednosti in slabosti, zato je odvisno od operacije, ki jo je treba sprejeti.

Ustvarjanje zbirke podatkov

V tem članku bom razložil, kako najprej ustvariti bazo podatkov za SQL Server in nato samodejno ustvariti kodo. Če ga želite implementirati na način kode, se tokrat obrnite na samodejno ustvarjeno kodo in jo izvedite v obratnem postopku.

Ustvarjanje zbirke podatkov

Lahko ga naredite tudi v SQL-ju, vendar ga je lažje narediti z GUI, zato ga tokrat naredim z GUI. Razen imena zbirke podatkov je privzeto ustvarjeno.

Ustvarjanje tabele

Ustvarite ga z naslednjim SQL-jem:

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

Za večino parametrov vam ni treba skrbeti, ker so namenjeni samo posodobitvi podatkov. Zanimiv parameter je RowVersion tokrat stolpec, ki opisuje . To je shranjevanje različic zapisov. Če določite timestamp vrsto različice, se različica samodejno poveča vsakič, ko je zapis posodobljen. Ker se ta različica upravlja na podlagi tabele, v bistvu ni zapisa o isti različici, razen če jo nastavite ročno.

Dodajanje zapisa

Dodate ga lahko z naslednjim SQL-jem:

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 Stolpcev vam ni treba nastaviti, ker so nastavljeni samodejno.

Ustvarjanje projekta in samodejno ustvarjanje kode

Tokrat bomo preverili delovanje s konzolno aplikacijo. Koraki za ustvarjanje projekta in samodejno generiranje kode so opisani v naslednjih nasvetih, zato se tukaj ne bom spuščal v njih.

Ustvarjena koda je naslednja:

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

Preverjanje delovanja optimističnega nadzora sočasnosti

Ker ga tokrat izvajamo v eni aplikaciji, do njega hkrati ne dostopamo striktno, vendar bi ga radi implementirali v obliki, ki ji je blizu.

Kot v primeru na začetku se pridobita dva podatka, in ko je vsak posodobljen na podlagi prvih podatkov, preverite, ali bo slednji posodabljalec dobil napako.

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

To je dolga zgodba, vendar je večina napisana na konzolo.

Rezultat izvršitve je naslednji.

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

処理を終了します。

Razdelil ga bom na dele.

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

Ustvarjam dva konteksta zbirke podatkov, Če imate v skupni rabi en kontekst zbirke podatkov, bo predpomnjen, ko bodo podatki prebrani, in bo to isti primerek. Ob predpostavki, da se do vsakega dostopa ločeno, se ustvarita dva konteksta zbirke podatkov. dbContextC je za preverjanje vrednosti v bazi podatkov. V resnici ga ne potrebujem, ker je mogoče nadomestiti A ali B.

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

Ob predpostavki, da se do njih dostopa ločeno, berejo enega iz različnih kontekstov baze Book podatkov. Na tej točki nismo ničesar spremenili, zato so vsi enaki.

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

Najprej sem spremenil in posodobil knjigo gospoda / gospe. Postopek posodabljanja je UpdateToDatabase povzet v metodi, ko pa pride do izjeme, se prikaže sporočilo.

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

Posledično je bila uspešno posodobljena in Price RowVersion knjiga A je bila posodobljena. Če neposredno preberete vrednost DB, bo posodobljena vrednost enaka posodobljeni vrednosti. Lahko sem ga posodobil, ker sta oba prišla v RowVersion A in RowVersion v DB AAAAAAAAH2k= . Različica se je zaradi posodobitve spremenila AAAAAAAAH2o= .

Po posodobitvi A posodobite B na enak način.

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

Rezultat je izjema DbUpdateConcurrencyException in posodobitev ni uspela. bookB.RowVersion AAAAAAAAH2k= je , vendar RowVersion ker je že AAAAAAAAH2o= , se oceni kot neusklajenost in pride do napake pri posodabljanju. Vidite lahko, da se je lokalna spremenljivka Price bookB spremenila, vendar RowVersion je Vidite lahko, da se vrednosti na strani zbirke podatkov sploh niso spremenile.

Povzetek

To je najlažja od več vrst zaklepanja, saj je pri uporabi samodejno ustvarjene kode v strežniku SQL Server in Entity Framework Core privzeto izvedena optimistična sočasnost. Ker pa gre le za preprečevanje »posodabljanja podatkov«, je treba ustrezno obravnavati izjeme, kadar gre za druge podatke ali uporabniške operacije.

Tudi tokrat nisem uspel ničesar, ker sem ga implementiral RowVersion v preprosto konzolno aplikacijo. Če želite vstaviti zaslon za urejanje po nalaganju podatkov v spletni aplikaciji ali odjemalski aplikaciji, RowVersion na nek način, da ga je mogoče pravilno določiti, ko je posodobljen.

Vendar pa ima Entity Framework Core funkcijo sledenja spremembam, tako da, če želite staro RowVersion nastaviti na vrednost, ki je bila prebrana iz zbirke podatkov v času posodobitve,

bookB.RowVersion = <古い RowVersion>;

Tudi če je nastavljen na naslednji način, ne bo pravilno ocenjen kot "optimistični nadzor sočasnosti". RowVersion Tudi če nastavite vrednost na normalno, bo prepoznana le kot spremenjena vrednost, zato je naslednje

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

Pred spremembo je treba ponovno napisati vrednost.