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ệmTổ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ợpHã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ỏngThự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 quanSự 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 quanViệ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.Là];
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