Bạn có thể giải thích khóa bi quan trong Entity Framework không?

Nói cách khác, câu hỏi như sau. làm cách nào để đảm bảo rằng ngay cả trong môi trường đa luồng, chúng tôi luôn có thể đảm bảo tính nhất quán ngay lập tức cho các quy tắc kinh doanh của mình?

Tốt nhất là mô tả vấn đề này bằng một ví dụ…

Vấn đề

Giả sử rằng miền của chúng ta có khái niệm về Đơn hàng và Dòng đơn hàng. Chuyên gia tên miền cho rằng

Một Lệnh có thể có tối đa 5 Dòng Lệnh

Ngoài ra, ông nói rằng quy tắc này phải được đáp ứng mọi lúc [không có ngoại lệ]. Chúng ta có thể mô hình hóa nó theo cách sau

Mô hình khái niệm

Tổng hợp

Một trong những mục tiêu chính của hệ thống của chúng tôi là thực thi các quy tắc kinh doanh. Một cách để thực hiện điều này là sử dụng khối xây dựng chiến thuật Thiết kế theo hướng miền – một Tổng hợp . Tổng hợp là một khái niệm được tạo ra để thực thi các quy tắc kinh doanh [bất biến]. Việc triển khai nó có thể khác nhau tùy thuộc vào mô hình mà chúng ta sử dụng, nhưng trong lập trình hướng đối tượng, nó là một biểu đồ hướng đối tượng như Martin Fowler mô tả.

Tập hợp DDD là một cụm các đối tượng miền có thể được coi là một đơn vị

Và Eric Evans trong tài liệu tham khảo DDD mô tả

Sử dụng cùng một ranh giới tổng hợp để quản lý các giao dịch và phân phối. Trong một ranh giới tổng hợp, hãy áp dụng đồng bộ các quy tắc nhất quán

Quay lại ví dụ. Làm thế nào chúng tôi có thể đảm bảo rằng Đơn đặt hàng của bạn sẽ không bao giờ vượt quá 5 Dòng đặt hàng? . Để làm được điều này, nó phải có thông tin về số Dòng lệnh hiện tại. Vì vậy, nó phải có một trạng thái và dựa trên trạng thái này, trách nhiệm của nó là quyết định xem bất biến có bị hỏng hay không

Trong trường hợp này, Order dường như là đối tượng hoàn hảo cho việc này. Nó sẽ trở thành gốc của Tổng hợp của chúng tôi, sẽ có Dòng thứ tự. Anh ta sẽ có trách nhiệm thêm dòng thứ tự và kiểm tra xem bất biến có bị hỏng không

Đặt hàng tổng hợp

Hãy xem cách triển khai một cấu trúc như vậy có thể trông như thế nào

Thực thể đặt hàng

C#

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

lớp công khai Thứ tự . AggregateRootBase

{

    công khai Hướng dẫn Id { get; private set; }

 

    riêng tư Danh sách _orderLines;

 

    riêng tư Ngày giờ? _modifyDate;

 

    riêng tư Đặt hàng[]

    {

        _orderLines = mới Danh sách[];

    }

 

    công khai vô hiệu AddOrderLine[string productCode]

    {

        if [_orderLines. Đếm >= 5]

        {

            ném mới Ngoại lệ["Order cannot have more than 5 order lines."];

        }

 

        _orderLines. Thêm[Dòng đặt hàng. Tạo mới[Mã sản phẩm]];

        _modifyDate = DateTime. Bây giờ;

        

        AddDomainEvent[new OrderLineAddedDomainEvent[this.Id]];

    }

}

Hiện tại mọi thứ đều ổn, nhưng đây chỉ là chế độ xem tĩnh của mô hình của chúng tôi. Hãy xem luồng hệ thống điển hình là gì khi bất biến không bị hỏng và khi nào bị hỏng

Quá trình thêm Dòng đặt hàng – thành côngQuá trình thêm Dòng đặt hàng – quy tắc bị hỏng

Thực hiện đơn giản dưới đây

Thêm dòng đặt hàng

C#

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

[ApiController]

[Tuyến đường["[bộ điều khiển]"]]

lớp công khai Trình điều khiển đơn hàng . ControllerBase

{

    riêng tư chỉ đọc Bối cảnh đơn hàng _bối cảnh đơn hàng;

 

   công khai OrdersController[OrdersContext ordersContext]

    {

        _ordersContext = ordersContext;

    }

 

    [HttpPost]

    công khai không đồng bộ Tác vụ AddOrderLine[AddOrderLineRequest request]

    {

        var orderId = Hướng dẫn.Phân tích cú pháp["33d4201c-4a8e-40a2-ae1d-50bc64097085"];

        

        var đặt hàng = đang chờ _ordersContext.Đơn đặt hàng. FindAsync[orderId];

        

        Chủ đề. Ngủ[3000];

 

        đặt hàng. AddOrderLine[yêu cầu. Mã sản phẩm];

 

        chờ đợi _ordersContext. SaveChangesAsync[];

 

        return Bật[];

    }

}

vấn đề đồng thời

Mọi thứ hoạt động tốt và sạch sẽ nếu chúng ta hoạt động trong môi trường không tải nặng. Tuy nhiên, hãy xem điều gì có thể xảy ra khi 2 luồng gần như cùng lúc muốn thực hiện thao tác của chúng ta

Quy trình thêm Order Line – quy tắc nghiệp vụ bị phá vỡ

Như bạn có thể thấy trong sơ đồ trên, 2 luồng tải chính xác cùng một tập hợp tại cùng một thời điểm. Giả sử rằng Đơn đặt hàng có 4 Dòng đặt hàng. Tổng hợp có 4 Dòng lệnh sẽ được tải trong cả luồng thứ nhất và luồng thứ hai. Ngoại lệ sẽ không được đưa ra, vì 4 < 5. Cuối cùng, tùy thuộc vào cách chúng tôi duy trì tổng hợp, các tình huống sau có thể xảy ra

a] Nếu chúng ta có một cơ sở dữ liệu quan hệ và một bảng riêng cho các Dòng đặt hàng thì 2 Dòng đặt hàng sẽ được thêm vào với tổng số 6 Dòng đặt hàng – quy tắc kinh doanh bị phá vỡ

b] Nếu chúng tôi lưu trữ tổng hợp ở một vị trí nguyên tử [ví dụ: trong cơ sở dữ liệu tài liệu dưới dạng đối tượng JSON], chuỗi thứ hai sẽ ghi đè hoạt động đầu tiên và chúng tôi [và Người dùng] thậm chí sẽ không biết về điều này

Lý do cho hành vi này là luồng thứ hai đọc dữ liệu từ cơ sở dữ liệu [điểm 2. 1] trước khi người đầu tiên cam kết [điểm 1. 4. 3]

Hãy xem làm thế nào chúng ta có thể giải quyết vấn đề này

Dung dịch

đồng thời bi quan

Cách đầu tiên để đảm bảo rằng quy tắc kinh doanh của chúng tôi sẽ không bị phá vỡ là sử dụng Bi quan đồng thời . Theo cách tiếp cận đó, chúng tôi chỉ cho phép một luồng xử lý một Tổng hợp nhất định. Điều này dẫn đến việc luồng xử lý phải chặn việc đọc các luồng khác bằng cách tạo khóa. Chỉ khi giải phóng khóa, luồng tiếp theo mới có thể lấy đối tượng và xử lý nó.

đồng thời bi quan

Sự khác biệt chính so với cách tiếp cận trước đó là luồng thứ hai đợi luồng trước kết thúc [thời gian giữa các điểm 2. 1 và 1. 3]. Cách tiếp cận này khiến chúng tôi giảm hiệu suất vì chúng tôi chỉ có thể xử lý giao dịch lần lượt. Hơn nữa, nó có thể dẫn đến bế tắc

Làm cách nào chúng tôi có thể triển khai hành vi này bằng EntityFramework Core và máy chủ SQL?

Thật không may, EF Core không hỗ trợ Đồng thời bi quan . Tuy nhiên, chúng ta có thể tự làm điều đó một cách dễ dàng bằng cách sử dụng SQL thô và cơ chế gợi ý truy vấn của công cụ SQL Server.

Đầu tiên, giao dịch cơ sở dữ liệu phải được thiết lập. Sau đó, khóa phải được thiết lập. Điều này có thể được thực hiện theo hai cách – đọc dữ liệu với gợi ý truy vấn [XLOCK, PAGELOCK] hoặc bằng cách cập nhật bản ghi ngay từ đầu

đồng thời bi quan

C#

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

công khai không đồng bộ Tác vụ AddOrderLine[AddOrderLineRequest request]

{

    var orderId = Hướng dẫn.Phân tích cú pháp["33d4201c-4a8e-40a2-ae1d-50bc64097085"];

 

    chờ sử dụng [var tran = await _ordersContext.Cơ sở dữ liệu. BeginTransactionAsync[]]

    {

        chờ đợi _ordersContext. Cơ sở dữ liệu

            . ExecuteSqlRawAsync[$"CẬP NHẬT đơn đặt hàng. Đơn đặt hàng VỚI [XLOCK] SET Id = Id  WHERE Id = '{orderId}'"];

 

        var đặt hàng = đang chờ _ordersContext.Đơn đặt hàng. FindAsync[orderId];

 

        Chủ đề. Ngủ[3000];

 

        đặt hàng. AddOrderLine[yêu cầu. Mã sản phẩm];

 

        chờ đợi _ordersContext. SaveChangesAsync[];

    }

 

    return Bật[];

}

Bằng cách này, giao dịch trên chuỗi đầu tiên sẽ nhận được một Khóa độc quyền [khi ghi] và cho đến khi nó giải phóng nó [thông qua . Tất nhiên, giả sử rằng các truy vấn của chúng tôi hoạt động ở mức cô lập giao dịch đã cam kết ít nhất là đọc để tránh cái gọi là đọc bẩn.

đồng thời lạc quan

Một giải pháp thay thế và thường được ưu tiên nhất là sử dụng Đồng thời lạc quan . Trong trường hợp này, toàn bộ quá trình diễn ra mà không khóa dữ liệu. Thay vào đó, dữ liệu trong cơ sở dữ liệu được tạo phiên bản và trong quá trình cập nhật, nó được kiểm tra – liệu có thay đổi phiên bản trong thời gian chờ đợi hay không.

đồng thời lạc quan

Việc triển khai giải pháp này như thế nào? . Nó đủ để chỉ ra những trường nào cần được kiểm tra khi ghi vào cơ sở dữ liệu và nó sẽ được thêm vào câu lệnh Optimistic Concurrency out of the box. It is enough to indicate which fields should be checked when writing to the database and it will be added to the WHERE . Nếu hóa ra câu lệnh của chúng tôi đã cập nhật 0 bản ghi, điều đó có nghĩa là phiên bản của bản ghi đã thay đổi và chúng tôi cần thực hiện khôi phục. Mặc dù các trường hiện tại thường không được sử dụng để kiểm tra phiên bản và cột đặc biệt có tên “Phiên bản” hoặc “Dấu thời gian” được thêm vào.

Quay lại ví dụ của chúng tôi, việc thêm một cột có phiên bản và tăng nó mỗi khi thực thể Đơn hàng được thay đổi có giải quyết được sự cố không? . Tổng hợp phải được coi là một tổng thể, như một ranh giới của giao dịch và tính nhất quán

Do đó, việc tăng phiên bản phải diễn ra khi chúng tôi thay đổi bất kỳ thứ gì trong tổng hợp của mình. Nếu chúng tôi đã thêm Dòng đặt hàng và chúng tôi giữ nó trong một bảng riêng biệt, hỗ trợ ORM cho đồng thời lạc quan sẽ không giúp ích gì cho chúng tôi vì nó hoạt động trên các bản cập nhật và ở đây có các phần chèn và xóa còn lại

Làm cách nào để chúng tôi biết rằng trạng thái của Tổng hợp của chúng tôi đã thay đổi? . Nếu một Sự kiện miền bị ném khỏi Tổng hợp, điều đó có nghĩa là trạng thái đã thay đổi và chúng tôi cần tăng phiên bản Tổng hợp.

Việc thực hiện có thể trông như thế này

Đầu tiên, chúng tôi thêm trường _version vào mỗi Gốc tổng hợp và phương pháp để tăng phiên bản đó.

AggregateRootBase với phiên bản

C#

1

2

3

4

5

6

7

8

9

lớp công khai AggregateRootBase . Thực thể, IAggregateRoot

{

    riêng tư int _versionId;

 

    công khai vô hiệu IncreaseVersion[]

    {

        _versionId++;

    }

}

Thứ hai, chúng tôi thêm ánh xạ cho thuộc tính phiên bản và chỉ ra rằng đó là Mã thông báo đồng thời EF

Ánh xạ cấu hình loại thực thể đặt hàng

C#

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

nội bộ được niêm phong lớp OrderEntityTypeConfiguration . IEntityTypeConfiguration

{

    công khai vô hiệu Định cấu hình[EntityTypeBuilder builder]

    {

        người xây dựng. ToTable["Đơn hàng", "orders"];

 

        người xây dựng. HasKey[b => . b.];

        người xây dựng. Thuộc tính["_modifyDate"].HasColumnName["ModifyDate"];

        người xây dựng. Thuộc tính["_versionId"].HasColumnName["VersionId"].IsConcurrencyToken[];

 

        người xây dựng. Sở hữu nhiều["_orderLines", orderLine =>

        {

            orderLine. Với chủ sở hữu[]. HasForeignKey["OrderId"];

 

            orderLine. ToTable["OrderLines", "orders"];

 

            orderLine. Thuộc tính[ . "Id"].ValueGeneratedNever[];

            orderLine. HasKey["Id"];

            orderLine. Thuộc tính[ . "_productCode"].HasColumnName["ProductCode"];

        }];

    }

}

Điều cuối cùng cần làm là tăng phiên bản nếu có bất kỳ Sự kiện miền nào đã được xuất bản

Tăng phiên bản tổng hợp đơn hàng

C#

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

[HttpPost]

công khai không đồng bộ Tác vụ AddOrderLine[AddOrderLineRequest request]

{

    var orderId = Hướng dẫn.Phân tích cú pháp["33d4201c-4a8e-40a2-ae1d-50bc64097085"];

    

    var đặt hàng = chờ đợi _ordersContext.Đơn đặt hàng. FindAsync[orderId];

    

    Chủ đề. Ngủ[3000];

 

    đặt hàng. AddOrderLine[yêu cầu. Mã sản phẩm];

 

   var domainEvents = DomainEventsHelper.GetAllDomainEvents[đặt hàng];

 

    if [sự kiện tên miền. Bất kỳ[]]

    {

        đặt hàng. IncreaseVersion[];

    }

 

    await _ordersContext. SaveChangesAsync[];

 

    return Bật[];

}

Với thiết lập này, tất cả các bản cập nhật sẽ thực thi câu lệnh này

C#

1

2

3

4

5

6

thông tin. Microsoft. EntityFrameworkCore. Cơ sở dữ liệu. Lệnh[20101]

      Đã thực thi DbCommand [0ms] [Parameters=[@p2='33d4201c-4a8e-40a2-ae1d-50bc64097085', @p0='2020-05-14T21:02:41' [Không thể = true], @p1='8', @p3='7'], CommandType='Text', CommandTimeout='30']

      ĐẶT KHÔNG CÓ BẬT;

      CẬP NHẬT [đơn đặt hàng].[Đơn đặt hàng] ĐẶT [VersionId] = @p1

      Ở ĐÂU [Id] = @p2 AND [VersionId] = @p3;

      CHỌN @@ROWCOUNT;

Ví dụ của chúng tôi, đối với luồng thứ hai, sẽ không có bản ghi nào được cập nhật [ @@ ROWOCOUNT = 0 ], so EntityFramework will throw the following message:

Microsoft. Thực thểKhungLõi. DbUpdateConcurrencyException. Hoạt động cơ sở dữ liệu dự kiến ​​sẽ ảnh hưởng đến 1 hàng nhưng thực tế ảnh hưởng đến 0 hàng. Dữ liệu có thể đã bị sửa đổi hoặc bị xóa kể từ khi thực thể được tải. xem http. //đi. Microsoft. com/fwlink/?LinkId=527962 để biết thông tin về cách hiểu và xử lý các ngoại lệ đồng thời lạc quan

và Tổng hợp của chúng tôi sẽ nhất quán – Dòng lệnh thứ 6 sẽ không được thêm vào. Quy tắc kinh doanh không bị phá vỡ, nhiệm vụ đã hoàn thành

Tóm lược

Tóm lại, những vấn đề quan trọng nhất ở đây là

  • Nhiệm vụ chính của Aggregate là bảo vệ các bất biến [các quy tắc nghiệp vụ, ranh giới của tính nhất quán tức thời]
  • Trong môi trường đa luồng, khi nhiều luồng đang chạy đồng thời trên cùng một Tập hợp, quy tắc kinh doanh có thể bị phá vỡ
  • Một cách để giải quyết xung đột đồng thời là sử dụng các kỹ thuật đồng thời Bi quan hoặc Lạc quan
  • Đồng thời bi quan liên quan đến việc sử dụng giao dịch cơ sở dữ liệu và cơ chế khóa. Theo cách này, các yêu cầu được xử lý lần lượt, do đó về cơ bản tính đồng thời bị mất và có thể dẫn đến bế tắc.
  • Optimistic Concurrency dựa trên việc tạo phiên bản cho các bản ghi cơ sở dữ liệu và kiểm tra xem phiên bản đã tải trước đó có bị thay đổi bởi luồng khác hay không.
  • Entity Framework Core hỗ trợ Đồng thời lạc quan . Không hỗ trợ đồng thời bi quan is not supported
  • Tổng hợp phải luôn được xử lý và tạo phiên bản như một đơn vị duy nhất
  • Các sự kiện miền là một chỉ báo, trạng thái đó đã được thay đổi nên phiên bản Tổng hợp cũng sẽ được thay đổi
  • Kho lưu trữ mẫu GitHub

    Đặc biệt để phục vụ cho nhu cầu của bài viết này, tôi đã tạo một kho lưu trữ hiển thị việc triển khai 3 kịch bản

    Khóa bi quan là gì?

    Khóa bi quan . Những người dùng khác cố gắng cập nhật hồ sơ này được thông báo rằng một người dùng khác đang tiến hành cập nhật. Những người dùng khác phải đợi cho đến khi người dùng đầu tiên thực hiện xong các thay đổi của họ, do đó giải phóng khóa bản ghi. As soon as one user starts to update a record, a lock is placed on it. Other users who attempt to update this record are informed that another user has an update in progress. The other users must wait until the first user has finished committing their changes, thereby releasing the record lock.

    Đồng thời lạc quan trong Entity Framework là gì?

    Đồng thời lạc quan liên quan đến cố gắng lưu thực thể của bạn vào cơ sở dữ liệu một cách lạc quan với hy vọng rằng dữ liệu trong đó không thay đổi kể từ khi thực thể được tải . Nếu hóa ra dữ liệu đã thay đổi thì một ngoại lệ sẽ được đưa ra và bạn phải giải quyết xung đột trước khi thử lưu lại.

    Hai loại khóa trong cơ sở dữ liệu là gì?

    Có hai loại khóa Cơ sở dữ liệu. .
    Khóa chung
    Khóa độc quyền

    Điều khiển đồng thời lạc quan hay bi quan nào tốt hơn?

    Nói chung, đồng thời lạc quan sẽ hiệu quả hơn khi các xung đột cập nhật dự kiến ​​sẽ không xảy ra thường xuyên ; .

Chủ Đề