کنترل همزمانی خوش بینانه از از دست دادن داده ها به دلیل به روز رسانی های آخرین برد با دسترسی های متعدد (SQL Server) جلوگیری می کند

صفحه به روز شده :
تاریخ ایجاد صفحه :

محیط عملیاتی

ویژوال استودیو
  • ویژوال استودیو 2022
.خالص
  • دات نت 8
هسته فریمورک Entity
  • Entity Framework Core 8.0
SQL Server
  • SQL Server 2022

* موارد فوق یک محیط تأیید است، اما ممکن است با نسخه های دیگر کار کند.

درباره برنده شدن به روز رسانی ها پس از بدون کنترل

در یک برنامه وب یا یک برنامه مشتری-سرور، چندین نفر ممکن است به داده ها دسترسی داشته باشند و از یک پایگاه داده واحد به آن به روز کنند. اگر هیچ کاری به طور خاص انجام نشود ، داده های شخصی که بعدا آن را به روز کرده است ، به عنوان آخرین در پایگاه داده منعکس می شود.

به طور معمول ، هیچ مشکل خاصی وجود ندارد که داده های شخصی که بعدا به روز شده است به عنوان آخرین منعکس شود. زمانی ممکن است مشکلاتی ایجاد شود که چندین نفر سعی کنند به طور همزمان به داده های یکسان دسترسی داشته باشند و آنها را به روز کنند.

به عنوان مثال، فرض کنید داده های کتاب زیر را دارید.

مقدار نام پارامتر
نام کتاب کتاب های پایگاه داده
قيمت 1000

اگر دو نفر صفحه را باز کنند تا همزمان این داده ها را ویرایش کنند، مقدار بالا نمایش داده می شود. آقای/خانم الف در تلاش است تا قیمت این کتاب را 500 ین افزایش دهد. بعدا به آقای/خانم ب دستور داده می شود که قیمت این کتاب را 300 ین دیگر افزایش دهد.

اگر هر دو به جای همزمان قیمت را به طور جداگانه افزایش دهند، قیمت کتاب 1800 ین خواهد بود. اگر همزمان به آن دسترسی داشته باشید، به ترتیب 1500 ین و 1300 ین ثبت می شود، بنابراین مهم نیست کدام یک ثبت نام کند، 1800 ین نخواهد بود.

مشکل اینجاست که شخصی که بعدا به روز شده است می تواند بدون دانستن اطلاعاتی که قبلا به روز شده است، آن را به روز کند.

همزمانی خوش بینانه

مشکل فوق را می توان با انجام "کنترل همزمانی خوش بینانه" این بار حل کرد. برای توضیح ساده، چه نوع کنترلی است که "اگر سعی کنید داده ها را همزمان ویرایش کنید، ابتدا برنده شوید". کسانی که بعدا اقدام به تمدید کنند، در زمان به روز رسانی با خطا مواجه می شوند و نمی توانند ثبت نام کنند.

ممکن است فکر کنید که نمی توانید داده های جدید را با این ثبت کنید، اما این فقط "زمانی است که سعی می کنید همزمان آن را تغییر دهید". این امکان وجود دارد که دو نفر در زمان های کاملا متفاوت ویرایش کنند. البته در این صورت داده های آخرین فرد به روز شده جدیدترین خواهد بود.

به طور خاص، چه نوع کنترل پردازشی را می توان با "داشتن نسخه ای از داده ها" به دست آورد. به عنوان مثال، در مثال بالا، داده های زیر را خواهید داشت.

مقدار نام پارامتر
نام کتاب کتاب های پایگاه داده
قيمت 1000
نسخهٔ 1

نسخه برای هر به روز رسانی رکورد 1 افزایش می یابد. به عنوان مثال، اگر Mr./Ms. A قیمت را روی 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 برای بررسی مقادیر موجود در پایگاه داده است. من واقعا به آن نیاز ندارم زیرا A یا 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))}");

با فرض اینکه به طور جداگانه به آنها دسترسی داشته باشد، آنها یکی را از زمینه 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 کتاب A به روز شده است. همچنین، اگر مقدار DB را مستقیما بخوانید، مقدار به روز شده با مقدار به روز شده یکسان خواهد بود. من توانستم آن را به روز کنم زیرا هر دو در 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 را روی مقدار خوانده شده از DB در زمان به روز رسانی تنظیم کنید،

bookB.RowVersion = <古い RowVersion>;

حتی اگر به شرح زیر تنظیم شود، به درستی به عنوان "کنترل همزمانی خوش بینانه" قضاوت نخواهد شد. RowVersion حتی اگر مقدار را روی عادی تنظیم کنید، فقط به عنوان مقدار تغییر یافته شناخته می شود، بنابراین موارد زیر است:

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

لازم است مقدار را قبل از تغییر بازنویسی کنید.