Оптимистичное управление параллелизмом предотвращает потерю данных из-за обновлений Last-Win с несколькими обращениями (SQL Server)
Операционная среда
- Визуальная студия
-
- Визуальная студия 2022
- .СЕТЬ
-
- .NET 8
- Ядро Entity Framework
-
- Ядро Entity Framework 8.0
- SQL Server
-
- SQL Server 2022
* Вышеуказанная среда является средой верификации, но она может работать и с другими версиями.
О выигрышных обновлениях после отсутствия контроля
В веб-приложении или клиент-серверном приложении несколько человек могут получать доступ к данным из одной базы данных и обновлять их. Если ничего конкретного не предпринимать, то данные человека, который его обновил позже, будут отражены в базе данных как последние.
Как правило, нет особой проблемы, что данные человека, который обновился позже, отражаются как последние. Проблемы могут возникнуть, когда несколько человек пытаются получить доступ к одним и тем же данным и обновить их одновременно.
Например, предположим, что у вас есть следующие данные книги.
Значение имени параметра | |
---|---|
Название книги | Базы данных книг |
цена | 1000 |
Если два человека одновременно откроют экран для редактирования этих данных, то будет отображено указанное выше значение. Г-н А. пытается поднять цену на эту книгу на 500 иен. Позже г-ну Б. было поручено поднять цену на эту книгу еще на 300 иен.
Если они оба поднимут цену по отдельности, а не одновременно, цена книги составит 1800 иен. Если вы получите к нему доступ в одно и то же время, он будет зарегистрирован как 1500 иен и 1300 иен соответственно, поэтому это не будет 1800 иен, независимо от того, какой из них зарегистрируется.
Проблема здесь в том, что человек, который обновился позже, может обновить его, не зная ранее обновленной информации.
Оптимистичный параллелизм
Вышеупомянутая проблема может быть решена путем выполнения на этот раз «оптимистичного управления параллелизмом». Если объяснить просто, то что такое контроль – «сначала победить, если вы попытаетесь одновременно редактировать данные». Те, кто попытается продлить подписку позже, получат ошибку в момент обновления и не смогут зарегистрироваться.
Вы можете подумать, что с этим не сможете зарегистрировать новые данные, но это только «когда вы пытаетесь изменить их одновременно». Возможно редактирование двумя людьми в совершенно разное время. Конечно, в этом случае данные последнего обновленного человека будут последними.
В частности, какого рода контроль над обработкой может быть достигнут за счет «наличия версии данных». Например, в приведенном выше примере у вас будут следующие данные.
Значение имени параметра | |
---|---|
Название книги | Базы данных книг |
цена | 1000 |
Версия | 1 |
Версия увеличивается на 1 для каждого обновления записи. Например, если Mr./Ms. A установит цену 1500 иен, версия будет 2. В это время условием возможности обновления является то, что версия до обновления должна совпадать с версией в базе данных. Когда Mr./Ms. обновляет его, версия базы данных равна 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
книга А.
Кроме того, если вы прочитаете значение DB напрямую, обновленное значение будет таким же, как и обновленное.
Я смог обновить его, потому что оба попали в RowVersion
A и RowVersion
в DB были AAAAAAAAH2k=
.
Версия изменилась AAAAAAAAH2o=
в связи с обновлением.
После обновления А обновите Б таким же образом.
// そのあと 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=
is , но 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>;
Необходимо перезаписать значение перед изменением.