Optimistische gelijktijdigheidscontrole voorkomt gegevensverlies als gevolg van last-win-updates met meerdere toegangen (SQL Server)

Pagina bijgewerkt :
Aanmaakdatum van pagina :

Werkomgeving

Visuele Studio
  • Visuele Studio 2022
.NET
  • .NET 8
Kern van het entiteitskader
  • Entiteit Framework Kern 8.0
SQL Server
  • SQL Server 2022

* Het bovenstaande is een verificatieomgeving, maar het kan werken met andere versies.

Over winnende updates zonder controle

In een webtoepassing of een client-servertoepassing kunnen meerdere personen toegang krijgen tot gegevens uit één database en deze bijwerken. Als er niets bijzonders wordt gedaan, worden de gegevens van de persoon die het later heeft bijgewerkt, als de meest recente in de database weergegeven.

Normaal gesproken is er geen bijzonder probleem dat de gegevens van de persoon die later heeft bijgewerkt, als de laatste worden weergegeven. Er kunnen problemen ontstaan wanneer meerdere mensen tegelijkertijd toegang proberen te krijgen tot dezelfde gegevens en deze proberen bij te werken.

Stel dat u de volgende boekgegevens heeft.

Parameter Naam Waarde
Naam van het boek Database Boeken
prijs 1000

Als twee mensen tegelijkertijd het scherm openen om deze gegevens te bewerken, wordt de bovenstaande waarde weergegeven. Dhr./Ms. A probeert de prijs van dit boek met 500 yen te verhogen. Dhr./mevrouw B krijgt later de opdracht om de prijs van dit boek met nog eens 300 yen te verhogen.

Als de twee afzonderlijk de prijs verhogen in plaats van tegelijkertijd, zal de prijs van het boek 1800 yen zijn. Als u het tegelijkertijd opent, wordt het geregistreerd als respectievelijk 1500 yen en 1300 yen, dus het zal geen 1800 yen zijn, ongeacht welke zich registreert.

Het probleem hier is dat de persoon die later heeft bijgewerkt, deze kan bijwerken zonder de informatie te kennen die eerder is bijgewerkt.

Optimistische gelijktijdigheid

Het bovengenoemde probleem kan worden opgelost door deze keer "optimistische gelijktijdigheidscontrole" uit te voeren. Om eenvoudig uit te leggen, wat voor soort controle is om "eerst te winnen als u de gegevens tegelijkertijd probeert te bewerken". Degenen die later proberen te verlengen, krijgen een foutmelding op het moment van de update en kunnen zich niet registreren.

Je denkt misschien dat je hier geen nieuwe gegevens mee kunt registreren, maar dit is alleen "wanneer je het tegelijkertijd probeert te wijzigen". Het is mogelijk voor twee personen om op totaal verschillende tijdstippen te bewerken. In dat geval zijn de gegevens van de laatst bijgewerkte persoon natuurlijk de meest recente.

In het bijzonder, wat voor soort verwerkingscontrole kan worden bereikt door "een versie van de gegevens te hebben". In het bovenstaande voorbeeld beschikt u bijvoorbeeld over de volgende gegevens.

Parameter Naam Waarde
Naam van het boek Database Boeken
prijs 1000
Versie 1

De versie wordt verhoogd met 1 voor elke recordupdate. Als meneer / mevrouw A de prijs bijvoorbeeld instelt op 1500 yen, is de versie 2. Op dat moment is de voorwaarde dat de update kan worden doorgevoerd dat de versie voor de update hetzelfde is als de versie op de database. Wanneer de heer / mevrouw het bijwerkt, is de versie in de database 1 en de versie van de originele gegevens die momenteel wordt bewerkt is 1, zodat deze kan worden bijgewerkt.

Ga op basis van deze specificatie uit van een situatie waarin de heer/mevrouw A en de heer/mevrouw B dezelfde gegevens bewerken. Toen de heer / mevrouw voor het eerst de prijs op 1500 yen stelde en probeerde de database bij te werken, was de versie hetzelfde, dus deze kon worden bijgewerkt. In dat geval is de versie op de database 2. Dhr./Mevr. B probeert de gegevens van 1000 yen te bewerken en bij te werken naar de database als 1300 yen. De update mislukt omdat de versie bij de hand 1 is, maar de versie in de database al 2 is. Dit is hoe optimistische gelijktijdigheid werkt.

Entity Framework Core bevat deze "optimistische gelijktijdigheid" out-of-the-box, waardoor het relatief eenvoudig te implementeren is.

Trouwens, "optimistische gelijktijdigheid" wordt ook wel "optimistisch slot" of "optimistisch slot" genoemd, en het wordt soms onder deze naam onderzocht en besproken. Het is een vergrendelingsmethode waarbij de gegevens niet kunnen worden bijgewerkt, maar de gegevens kunnen worden gelezen. Er is ook een controle die "pessimistische vergrendeling" wordt genoemd als een andere vergrendelingsmethode dan "optimistische vergrendeling". Dit is een methode die het laden van gegevens vergrendelt wanneer de eerste persoon de gegevens leest en zelfs geen bewerkingsbewerkingen toestaat. Het kan het probleem oplossen dat de gegevens niet kunnen worden bijgewerkt, ook al zijn deze gewijzigd, maar wanneer iemand de gegevens bewerkt, kunnen andere mensen het scherm voor het bewerken van gegevens niet openen en als het ontgrendelen mislukt, worden de gegevens voor altijd vergrendeld. Beide hebben voor- en nadelen, dus het hangt af van de operatie welke u moet toepassen.

Een database maken

In dit artikel leg ik uit hoe je eerst een database voor SQL Server maakt en vervolgens automatisch code genereert. Als u het op een code-first manier wilt implementeren, raadpleeg dan deze keer de automatisch gegenereerde code en implementeer deze in de omgekeerde procedure.

Een database maken

Je kunt het ook in SQL maken, maar het is makkelijker om het met een GUI te maken, dus ik maak het deze keer met een GUI. Met uitzondering van de naam van de database, wordt deze standaard aangemaakt.

Een tabel maken

Maak het met de volgende 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

U hoeft zich geen zorgen te maken over de meeste parameters, omdat ze alleen voor gegevensupdates zijn. De parameter die deze keer van belang is, is RowVersion de kolom die beschrijft . Dit is recordversiebeheer. Door het timestamp type op te geven, wordt de versie automatisch verhoogd telkens wanneer de record wordt bijgewerkt. Aangezien deze versie per tabel wordt beheerd, is er in principe geen record van dezelfde versie, tenzij u deze handmatig instelt.

Een record toevoegen

U kunt het toevoegen met de volgende 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 U hoeft de kolommen niet in te stellen, omdat ze automatisch worden ingesteld.

Maak een project en genereer automatisch code

Deze keer zullen we de werking controleren met de console-applicatie. De stappen om een project aan te maken en automatisch code te genereren worden beschreven in de volgende tips, dus daar ga ik hier niet op in.

De gegenereerde code is als volgt:

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

Controle van de werking van optimistische gelijktijdigheidscontrole

Omdat we het deze keer in één applicatie uitvoeren, hebben we er niet strikt tegelijkertijd toegang toe, maar willen we het graag implementeren in een vorm die er dicht bij in de buurt komt.

Net als in het voorbeeld aan het begin worden twee gegevens verzameld, en wanneer elk wordt bijgewerkt op basis van de eerste gegevens, controleer dan of de laatste updater een foutmelding krijgt.

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

Het is een lang verhaal, maar het meeste is naar de console geschreven.

Het resultaat van de uitvoering is als volgt.

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

処理を終了します。

Ik zal het opsplitsen in secties.

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

Ik ben bezig met het maken van twee databasecontexten, Als u één databasecontext deelt, wordt deze in de cache opgeslagen wanneer de gegevens worden gelezen en is het dezelfde instantie. Ervan uitgaande dat elk afzonderlijk wordt geopend, worden twee databasecontexten gemaakt. dbContextC is voor het controleren van de waarden in de database. Ik heb het niet echt nodig omdat A of B kan worden vervangen.

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

Ervan uitgaande dat ze afzonderlijk toegankelijk zijn, lezen ze er een uit verschillende databasecontexten Book . Op dit moment hebben we niets veranderd, dus ze zijn allemaal hetzelfde.

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

Eerst heb ik het boek van meneer / mevrouw gewijzigd en bijgewerkt. Het updateproces wordt UpdateToDatabase samengevat in een methode en er wordt een bericht weergegeven wanneer er een uitzondering optreedt.

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

Als gevolg hiervan is het met succes bijgewerkt en Price RowVersion is Boek A bijgewerkt. Als u de DB-waarde rechtstreeks leest, is de bijgewerkte waarde hetzelfde als de bijgewerkte waarde. Ik was in staat om het bij te werken, omdat zowel in RowVersion A en RowVersion in DB waren AAAAAAAAH2k= . De versie is gewijzigd AAAAAAAAH2o= als gevolg van de update.

Na het updaten van A, update B op dezelfde manier.

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

Het resultaat is een uitzondering DbUpdateConcurrencyException en de update is mislukt. bookB.RowVersion AAAAAAAAH2k= is , maar RowVersion aangezien het al AAAAAAAAH2o= is, wordt het beoordeeld als een mismatch en treedt er een updatefout op. U kunt zien dat de Price lokale variabele bookB is gewijzigd, maar RowVersion de U kunt zien dat de waarden aan de databasezijde helemaal niet zijn veranderd.

Samenvatting

Dit is het gemakkelijkst te implementeren van de verschillende vergrendelingstypen, omdat wanneer u de automatisch gegenereerde code in SQL Server en Entity Framework Core gebruikt, optimistische gelijktijdigheid standaard wordt geïmplementeerd. Aangezien het echter alleen is om te voorkomen dat "gegevensbeschadiging moet worden bijgewerkt", is het noodzakelijk om uitzonderingen goed af te handelen wanneer er andere gegevens bij betrokken zijn of gebruikersbewerkingen betrokken zijn.

Ook heb ik deze keer niets voor elkaar gekregen omdat ik het in een eenvoudige console-applicatie heb geïmplementeerd RowVersion . Als u het bewerkingsscherm wilt invoegen na het laden van de gegevens in een webtoepassing of clienttoepassing, RowVersion op de een of andere manier, zodat goed kan worden bepaald wanneer het wordt bijgewerkt.

Entity Framework Core heeft echter een functie voor het bijhouden van wijzigingen, dus als u de oude RowVersion waarde wilt instellen op de waarde die uit de DB is gelezen op het moment van update,

bookB.RowVersion = <古い RowVersion>;

Zelfs als het als volgt is ingesteld, zal het niet correct worden beoordeeld als "optimistische gelijktijdigheidscontrole". RowVersion Zelfs als u de waarde instelt op normaal, wordt deze alleen herkend als de gewijzigde waarde, dus het volgende is

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

Het is noodzakelijk om de waarde vóór de wijziging te herschrijven.