Оптимистичният контрол на паралелността предотвратява загубата на данни поради последни печеливши актуализации с множество достъпи (SQL Server)

Страницата се актуализира :
Дата на създаване на страница :

Работна среда

Визуално студио
  • Visual Studio 2022
.НЕТЕН
  • .NET 8
Ядро на рамката на обекта
  • Entity Framework Core 8.0
SQL Server
  • SQL Server 2022

* Горното е среда за проверка, но може да работи и с други версии.

За печелившите актуализации след липса на контрол

В уеб приложение или клиент-сървър приложение множество хора могат да имат достъп и да актуализират данни от една база данни. Ако не се направи нищо конкретно, данните на лицето, което го е актуализирало по-късно, ще бъдат отразени в базата данни като последни.

Обикновено няма особен проблем, че данните на лицето, което е актуализирано по-късно, се отразяват като най-новите. Проблеми могат да възникнат, когато няколко души се опитат да получат достъп и да актуализират едни и същи данни едновременно.

Да предположим например, че имате следните данни за книгата.

Стойност на името на параметъра
Име на книгата Книги за бази данни
цена 1000

Ако двама души отворят екрана, за да редактират тези данни едновременно, ще се покаже горната стойност. Г-н/г-жа А се опитва да повиши цената на тази книга с 500 йени. Г-н/г-жа Б по-късно е инструктиран да повиши цената на тази книга с още 300 йени.

Ако двамата вдигнат цената поотделно, а не едновременно, цената на книгата ще бъде 1800 йени. Ако влезете в него едновременно, той ще бъде регистриран съответно като 1500 йени и 1300 йени, така че няма да бъде 1800 йени, независимо коя от тях се регистрира.

Проблемът тук е, че човекът, който е актуализирал по-късно, може да го актуализира, без да знае информацията, която е била актуализирана преди това.

Оптимистична паралелност

Гореспоменатият проблем може да бъде решен чрез извършване на "оптимистичен контрол на паралелността" този път. За да обясня просто, какъв вид контрол е да "спечелиш първи, ако се опиташ да редактираш данните едновременно". Тези, които се опитат да подновят по-късно, ще получат грешка по време на актуализацията и няма да могат да се регистрират.

Може да си мислите, че не можете да регистрирате нови данни с това, но това е само "когато се опитате да ги промените едновременно". Възможно е двама души да редактират по напълно различно време. Разбира се, в този случай данните на последното актуализирано лице ще бъдат най-новите.

По-конкретно, какъв вид контрол на обработката може да бъде постигнат чрез "наличие на версия на данните". Например в горния пример ще имате следните данни.

Стойност на името на параметъра
Име на книгата Книги за бази данни
цена 1000
версия 1

Версията се увеличава с 1 за всяка актуализация на записа. Например, ако г-н/г-жа А определи цената на 1500 йени, версията ще бъде 2. По това време условието актуализацията може да бъде направена е версията преди актуализацията да е същата като версията в базата данни. Когато г-н/г-жа го актуализира, версията в базата данни е 1, а версията на оригиналните данни, които се редактират в момента, е 1, така че може да бъде актуализирана.

Въз основа на тази спецификация да приемем, че г-н/г-жа А и г-н/г-жа Б редактират едни и същи данни. Когато г-н/г-жа за първи път постави цената на 1500 йени и се опита да актуализира базата данни, версията беше същата, така че можеше да бъде актуализирана. В този случай версията в базата данни ще бъде 2. Г-н/г-жа Б се опитва да редактира данните за 1000 йени и да ги актуализира в базата данни като 1300 йени. Актуализацията е неуспешна, защото версията на базата данни е 1, но версията в базата данни вече е 2. Ето как работи оптимистичната паралелност.

Entity Framework Core включва този "оптимистичен паралелизъм" извън кутията, което го прави сравнително лесен за изпълнение.

Между другото, "оптимистична паралелност" е известна още като "оптимистична ключалка" или "оптимистична ключалка" и понякога се разглежда и говори с това име. Това е метод на заключване, при който данните не могат да бъдат актуализирани, но данните могат да бъдат прочетени. Съществува и контрол, наречен "песимистично заключване" като друг метод за заключване, различен от "оптимистично заключване". Това е метод, който заключва зареждането на данните, когато първият човек прочете данните и дори не позволява операции по редактиране. Това може да реши проблема, че данните не могат да бъдат актуализирани, въпреки че са променени, но когато някой редактира данните, други хора не могат да отворят екрана за редактиране на данни и ако отключването е неуспешно, данните ще бъдат заключени завинаги. И двете имат предимства и недостатъци, така че зависи от операцията коя да приемете.

Създаване на база данни

В тази статия ще обясня как първо да създадете база данни за SQL Server и след това автоматично да генерирате код. Ако искате да го внедрите по начин, основан на първо кода, моля, вижте автоматично генерирания код този път и го внедрете в обратната процедура.

Създаване на база данни

Можете да го направите и в SQL, но е по-лесно да го направите с графичен интерфейс, така че този път го правя с графичен интерфейс. С изключение на името на базата данни, тя се създава по подразбиране.

Създаване на таблица

Създайте го със следния 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

Не е нужно да се притеснявате за повечето параметри, защото те са само за актуализации на данни. Параметърът, който представлява интерес този път, е RowVersion колоната, която описва . Това е версии на записи. Чрез задаване като timestamp тип, версията автоматично се увеличава всеки път, когато записът се актуализира. Освен това, тъй като тази версия се управлява на база таблица, по принцип няма запис на същата версия, освен ако не я зададете ръчно.

Добавяне на запис

Можете да го добавите със следния 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 Не е необходимо да задавате колоните, защото те се задават автоматично.

Създайте проект и автоматично генерирайте код

Този път ще проверим работата с конзолното приложение. Стъпките за създаване на проект и автоматично генериране на код са описани в следващите съвети, така че няма да навлизам в тях тук.

Генерираният код е както следва:

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

Проверка на работата на оптимистичния контрол на паралелността

Тъй като този път го изпълняваме в едно приложение, не имаме стриктен достъп до него едновременно, но бихме искали да го внедрим във форма, близка до нея.

Както в примера в началото, се придобиват две части от данни и когато всяка се актуализира въз основа на първите данни, проверете дали последният актуализиращ ще получи грешка.

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

Това е дълга история, но по-голямата част от нея е написана на конзолата.

Резултатът от изпълнението е следният.

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

処理を終了します。

Ще го разделя на части.

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

Създавам два контекста на база данни, Ако споделяте един контекст на база данни, той ще бъде кеширан при четене на данните и ще бъде същият екземпляр. Ако приемем, че всеки от тях е достъпен поотделно, се създават два контекста на база данни. dbContextC е за проверка на стойностите в базата данни. Всъщност нямам нужда от него, защото А или Б могат да бъдат заменени.

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

Ако приемем, че те са достъпни отделно, те четат един от различни контексти на Book база данни. На този етап не сме променили нищо, така че всички са еднакви.

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

Първо, промених и актуализирах книгата на г-н/г-жа. Процесът на актуализиране се UpdateToDatabase обобщава в метод и при възникване на изключение се показва съобщение.

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

В резултат на това тя беше успешно актуализирана Price RowVersion , а и на Книга А бяха актуализирани. Освен това, ако прочетете стойността на БД директно, актуализираната стойност ще бъде същата като актуализираната стойност. Успях да го актуализирам, защото и двете влязоха в RowVersion A, а RowVersion в DB бяха AAAAAAAAH2k= . Версията се промени AAAAAAAAH2o= поради актуализацията.

След като актуализирате A, актуализирайте B по същия начин.

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

Резултатът е изключение DbUpdateConcurrencyException и актуализацията е неуспешна. bookB.RowVersion AAAAAAAAH2k= е , но RowVersion тъй като вече AAAAAAAAH2o= е , се преценява като несъответствие и възниква грешка при актуализиране. Можете да видите, че локалната Price променлива bookB е променена, но RowVersion Можете да видите, че стойностите от страна на базата данни изобщо не са се променили.

Резюме

Това е най-лесният за изпълнение от няколкото типа заключвания, тъй като когато използвате автоматично генерирания код в SQL Server и Entity Framework Core, оптимистичният паралел се реализира по подразбиране. Въпреки това, тъй като това е само за да се предотврати "актуализиране на повредата на данните", е необходимо правилно да се обработват изключенията, когато са включени други данни или са включени потребителски операции.

Освен това този път не успях да успя да се справя с нищо, защото го имплементирах RowVersion в просто конзолно приложение. Ако искате да вмъкнете екрана за редактиране след зареждане на данните в уеб приложение или клиентско приложение, RowVersion по някакъв начин, така че да може да бъде правилно определен при актуализиране.

Въпреки това, Entity Framework Core има функция за проследяване на промените, така че ако искате да зададете старото RowVersion на стойността, прочетена от базата данни по време на актуализацията,

bookB.RowVersion = <古い RowVersion>;

Дори и да е зададена по следния начин, тя няма да бъде правилно оценена като "оптимистичен контрол на паралелността". RowVersion Дори ако зададете стойността на нормално, тя ще бъде разпозната само като променената стойност, така че следното е

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

Необходимо е да пренапишете стойността преди промяната.