Kiểm soát đồng thời lạc quan ngăn ngừa mất dữ liệu do cập nhật cuối cùng với nhiều truy cập (SQL Server)

Trang Cập Nhật :
Ngày tạo trang :

Môi trường hoạt động

Visual Studio
  • Visual Studio 2022
.MẠNG
  • .NET 8
Entity Framework Core
  • Khung thực thể Core 8.0
Máy chủ SQL
  • Máy chủ SQL 2022

* Trên đây là môi trường xác minh, nhưng nó có thể hoạt động với các phiên bản khác.

Giới thiệu về các bản cập nhật chiến thắng sau khi không kiểm soát

Trong một ứng dụng web hoặc một ứng dụng máy khách-máy chủ, nhiều người có thể truy cập và cập nhật dữ liệu từ một cơ sở dữ liệu duy nhất. Nếu không có gì được thực hiện cụ thể, dữ liệu của người cập nhật nó sau này sẽ được phản ánh trong cơ sở dữ liệu là mới nhất.

Thông thường, không có vấn đề cụ thể nào mà dữ liệu của người cập nhật sau được phản ánh là mới nhất. Vấn đề có thể phát sinh khi nhiều người cố gắng truy cập và cập nhật cùng một dữ liệu cùng một lúc.

Ví dụ: giả sử bạn có dữ liệu sách sau.

Giá trị tên tham số
Tên cuốn sách Sách cơ sở dữ liệu
giá 1000

Nếu hai người mở màn hình để chỉnh sửa dữ liệu này cùng một lúc, giá trị trên sẽ được hiển thị. Ông / Cô A đang cố gắng tăng giá của cuốn sách này thêm 500 yên. Ông / bà B sau đó được hướng dẫn tăng giá của cuốn sách này thêm 300 yên.

Nếu hai người họ tăng giá riêng thay vì cùng một lúc, giá của cuốn sách sẽ là 1800 yên. Nếu bạn truy cập nó cùng một lúc, nó sẽ được đăng ký lần lượt là 1500 yên và 1300 yên, vì vậy nó sẽ không phải là 1800 yên cho dù đăng ký cái nào.

Vấn đề ở đây là người cập nhật sau có thể cập nhật nó mà không cần biết thông tin đã được cập nhật trước đó.

Đồng thời lạc quan

Vấn đề nói trên có thể được giải quyết bằng cách thực hiện "kiểm soát đồng thời lạc quan" lần này. Để giải thích đơn giản, loại kiểm soát nào là "giành chiến thắng đầu tiên nếu bạn cố gắng chỉnh sửa dữ liệu cùng một lúc". Những người cố gắng gia hạn sau này sẽ nhận được lỗi tại thời điểm cập nhật và sẽ không thể đăng ký.

Bạn có thể nghĩ rằng bạn không thể đăng ký dữ liệu mới với dữ liệu này, nhưng đây chỉ là "khi bạn cố gắng thay đổi nó cùng một lúc". Hai người có thể chỉnh sửa vào những thời điểm hoàn toàn khác nhau. Tất nhiên, trong trường hợp đó, dữ liệu của người cập nhật cuối cùng sẽ là mới nhất.

Cụ thể, loại kiểm soát xử lý nào có thể đạt được bằng cách "có phiên bản dữ liệu". Ví dụ, trong ví dụ trên, bạn sẽ có dữ liệu sau.

Giá trị tên tham số
Tên cuốn sách Sách cơ sở dữ liệu
giá 1000
Phiên bản 1

Phiên bản được tăng thêm 1 cho mỗi bản ghi cập nhật. Ví dụ: nếu ông / bà A đặt giá thành 1500 yên, phiên bản sẽ là 2. Tại thời điểm đó, điều kiện mà bản cập nhật có thể được thực hiện là phiên bản trước khi cập nhật giống như phiên bản trên cơ sở dữ liệu. Khi Mr./Ms. cập nhật nó, phiên bản trên cơ sở dữ liệu là 1 và phiên bản của dữ liệu gốc hiện đang được chỉnh sửa là 1, vì vậy nó có thể được cập nhật.

Dựa trên đặc điểm kỹ thuật này, giả sử một tình huống mà ông / bà A và ông / bà B chỉnh sửa cùng một dữ liệu. Khi ông / bà lần đầu tiên đặt giá thành 1500 yên và cố gắng cập nhật cơ sở dữ liệu, phiên bản vẫn như cũ, vì vậy nó có thể được cập nhật. Trong trường hợp đó, phiên bản trên cơ sở dữ liệu sẽ là 2. Ông / Bà B cố gắng chỉnh sửa dữ liệu 1000 yên và cập nhật nó vào cơ sở dữ liệu là 1300 yên. Cập nhật không thành công vì phiên bản trong tay là 1, nhưng phiên bản trên cơ sở dữ liệu đã là 2. Đây là cách hoạt động đồng thời lạc quan.

Entity Framework Core bao gồm "sự đồng thời lạc quan" này ra khỏi hộp, làm cho nó tương đối dễ thực hiện.

Nhân tiện, "đồng thời lạc quan" còn được gọi là "khóa lạc quan" hoặc "khóa lạc quan", và đôi khi nó được kiểm tra và nói về cái tên này. Đây là một phương pháp khóa mà dữ liệu không thể được cập nhật, nhưng dữ liệu có thể được đọc. Ngoài ra còn có một điều khiển gọi là "khóa bi quan" như một phương pháp khóa khác ngoài "khóa lạc quan". Đây là một phương pháp khóa tải dữ liệu khi người đầu tiên đọc dữ liệu và thậm chí không cho phép các thao tác chỉnh sửa. Nó có thể giải quyết vấn đề là dữ liệu không thể được cập nhật mặc dù nó đã được thay đổi, nhưng khi ai đó đang chỉnh sửa dữ liệu, người khác không thể mở màn hình chỉnh sửa dữ liệu và nếu mở khóa không thành công, dữ liệu sẽ bị khóa vĩnh viễn. Cả hai đều có ưu điểm và nhược điểm, vì vậy nó phụ thuộc vào hoạt động mà cái nào để áp dụng.

Tạo cơ sở dữ liệu

Trong bài viết này, tôi sẽ giải thích cách tạo cơ sở dữ liệu cho SQL Server trước và sau đó tự động tạo mã. Nếu bạn muốn triển khai nó theo cách đầu tiên mã, vui lòng tham khảo mã được tạo tự động lần này và triển khai nó trong quy trình ngược lại.

Tạo cơ sở dữ liệu

Bạn cũng có thể tạo nó bằng SQL, nhưng việc tạo nó bằng GUI sẽ dễ dàng hơn, vì vậy lần này tôi sẽ tạo nó bằng GUI. Ngoại trừ tên cơ sở dữ liệu, nó được tạo theo mặc định.

Tạo bảng

Tạo nó với SQL sau:

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

Bạn không phải lo lắng về hầu hết các thông số vì chúng chỉ dành cho cập nhật dữ liệu. Tham số quan tâm lần này là RowVersion cột mô tả . Đây là phiên bản ghi. Bằng cách chỉ định là timestamp loại, phiên bản được tự động tăng lên mỗi khi bản ghi được cập nhật. Ngoài ra, vì phiên bản này được quản lý trên cơ sở mỗi bảng, về cơ bản không có bản ghi nào của cùng một phiên bản trừ khi bạn đặt nó theo cách thủ công.

Thêm bản ghi

Bạn có thể thêm nó bằng SQL sau:

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 Bạn không cần đặt các cột vì chúng được đặt tự động.

Tạo dự án và tự động tạo mã

Lần này, chúng tôi sẽ kiểm tra hoạt động với ứng dụng bảng điều khiển. Các bước để tạo một dự án và tự động tạo mã được mô tả trong các mẹo sau, vì vậy tôi sẽ không đi sâu vào chúng ở đây.

Mã được tạo như sau:

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

Kiểm tra hoạt động kiểm soát đồng thời lạc quan

Vì chúng tôi đang chạy nó trong một ứng dụng lần này, chúng tôi không truy cập nghiêm ngặt vào nó cùng một lúc, nhưng chúng tôi muốn triển khai nó ở dạng gần với nó.

Như trong ví dụ lúc đầu, hai phần dữ liệu được thu thập và khi mỗi phần được cập nhật dựa trên dữ liệu đầu tiên, hãy kiểm tra xem trình cập nhật sau có gặp lỗi hay không.

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

Đó là một câu chuyện dài, nhưng hầu hết nó được viết vào bảng điều khiển.

Kết quả của việc thực hiện như sau.

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

処理を終了します。

Tôi sẽ chia nó thành các phần.

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

Tôi đang tạo hai ngữ cảnh cơ sở dữ liệu, Nếu bạn chia sẻ một ngữ cảnh cơ sở dữ liệu, nó sẽ được lưu vào bộ nhớ cache khi dữ liệu được đọc và nó sẽ là cùng một phiên bản. Giả sử rằng mỗi được truy cập riêng biệt, hai ngữ cảnh cơ sở dữ liệu được tạo ra. dbContextC là để kiểm tra các giá trị trong cơ sở dữ liệu. Tôi không thực sự cần nó vì A hoặc B có thể được thay thế.

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

Giả sử rằng chúng được truy cập riêng biệt, chúng đang đọc một từ các ngữ cảnh Book cơ sở dữ liệu khác nhau . Tại thời điểm này, chúng tôi không thay đổi bất cứ điều gì, vì vậy tất cả chúng đều giống nhau.

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

Đầu tiên, tôi đã thay đổi và cập nhật cuốn sách của ông / bà. Quá trình Cập Nhật được UpdateToDatabase tóm tắt trong một phương pháp, và một thông báo được hiển thị khi một ngoại lệ xảy ra.

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

Kết quả là, nó đã được cập nhật thành công và Price RowVersion của Sách A đã được cập nhật. Ngoài ra, nếu bạn đọc trực tiếp giá trị CSDL, giá trị cập nhật sẽ giống như giá trị cập nhật. Tôi đã có thể cập nhật nó vì cả hai đều có trong RowVersion A và RowVersion trong DB là AAAAAAAAH2k= . Phiên bản đã thay đổi AAAAAAAAH2o= do bản cập nhật.

Sau khi cập nhật A, hãy cập nhật B theo cách tương tự.

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

Kết quả là một ngoại lệ DbUpdateConcurrencyException và cập nhật đã thất bại. bookB.RowVersion AAAAAAAAH2k= là , nhưng RowVersion vì đã AAAAAAAAH2o= , nó được đánh giá là không khớp và xảy ra lỗi cập nhật. Bạn có thể thấy rằng biến bookB cục bộ Price đã thay đổi, nhưng RowVersion Bạn có thể thấy rằng các giá trị ở phía cơ sở dữ liệu hoàn toàn không thay đổi.

Tóm tắt

Đây là cách dễ nhất trong một số loại khóa để thực hiện, bởi vì khi bạn sử dụng mã được tạo tự động trong SQL Server và Entity Framework Core, tính đồng thời lạc quan được thực hiện theo mặc định. Tuy nhiên, vì nó chỉ để ngăn chặn "tham nhũng dữ liệu được cập nhật", nên cần phải xử lý đúng các trường hợp ngoại lệ khi dữ liệu khác có liên quan hoặc hoạt động của người dùng có liên quan.

Ngoài ra, lần này tôi đã không quản lý bất cứ điều gì vì tôi đã triển khai RowVersion nó trong một ứng dụng giao diện điều khiển đơn giản. Nếu bạn muốn chèn màn hình chỉnh sửa sau khi tải dữ liệu trong ứng dụng web hoặc ứng dụng khách, RowVersion theo một cách nào đó để nó có thể được xác định chính xác khi nó được cập nhật.

Tuy nhiên, Entity Framework Core có chức năng theo dõi thay đổi, vì vậy nếu bạn muốn đặt giá trị cũ RowVersion thành giá trị đọc từ DB tại thời điểm cập nhật,

bookB.RowVersion = <古い RowVersion>;

Ngay cả khi nó được đặt như sau, nó sẽ không được đánh giá chính xác là "kiểm soát đồng thời lạc quan". RowVersion Ngay cả khi bạn đặt giá trị thành bình thường, nó sẽ chỉ được nhận dạng là giá trị đã thay đổi, vì vậy sau đây là

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

Cần phải viết lại giá trị trước khi thay đổi.