Ngày 15 tháng 9, 2026
Đọc trong 10 phút
Hầu hết mọi người đều gặp phải tình huống này trong sản phẩm: khách hàng thay đổi địa chỉ giao hàng trên một lô hàng, lưu lại, thấy xác nhận, và một giờ sau địa chỉ cũ lại trở lại.
Bạn bắt đầu điều tra, và bạn không thấy bất kỳ lỗi hoặc ngoại lệ nào trong nhật ký. Yêu cầu trả về 200 OK, nhưng dữ liệu lại sai.
Đây là một “mất cập nhật” (lost update), và đây là một trong những lỗi phổ biến nhất trong các hệ thống .NET sản phẩm. Nó xảy ra bất cứ khi nào hai yêu cầu đọc cùng một hàng, thay đổi nó trong bộ nhớ, và ghi lại.
Tôi đã gỡ lỗi lỗi chính xác này trong các hệ thống thanh toán, phần mềm kho bãi và quy trình đặt chỗ. Nó không bao giờ xuất hiện trong kiểm thử đơn vị hoặc tích hợp, vì các kiểm thử không chạy hai yêu cầu trong cùng một mili giây (kiểm thử tích hợp của bạn thực sự có thể kiểm tra các yêu cầu đồng thời).
Có hai cách kinh điển để giải quyết nó: đồng thời tích cực và đồng thời tiêu cực. Chúng giải quyết cùng một vấn đề với các giả định khác nhau, và chọn sai cách sẽ giết chết thông lượng của bạn hoặc để lại lỗi.
Trong bài viết này, chúng ta sẽ tìm hiểu:
- Vấn đề Mất cập nhật
- Đồng thời Tích cực: Phát hiện Xung đột
- Xử lý DbUpdateConcurrencyException
- Thử lại Xung đột với Pipeline Khả năng phục hồi
- Đồng thời Tiêu cực: Khóa Hàng trước
- Một Lựa chọn thứ ba: Một câu lệnh Nguyên tử
- Đồng thời Tích cực vs Tiêu cực: So sánh Chi tiết
- Cách Chọn: Danh sách Kiểm tra Quyết định
Hãy cùng tìm hiểu.
Mục lục
Vấn đề Mất cập nhật
Đây là lĩnh vực chúng ta sẽ sử dụng xuyên suốt bài viết: một lô hàng mà nhân viên kho có thể chỉnh sửa.
public class Shipment
{
public Guid Id { get; set; }
public string Number { get; set; }
public string Address { get; set; }
public string Carrier { get; set; }
public ShipmentStatus Status { get; set; }
public List<ShipmentItem> Items { get; set; } = [];
}
Và đây là bộ xử lý cập nhật mà hầu hết mọi người viết đầu tiên:
public async Task<Result> Handle(UpdateShipmentRequest request, CancellationToken ct)
{
var shipment = await dbContext.Shipments
.FirstOrDefaultAsync(s => s.Number == request.Number, ct);
if (shipment is null)
{
return Result.NotFound($"Shipment '{request.Number}' not found");
}
shipment.Address = request.Address;
shipment.Carrier = request.Carrier;
await dbContext.SaveChangesAsync(ct);
return Result.Success();
}
Đoạn mã này đọc một hàng, thay đổi nó trong bộ nhớ, và ghi lại.
Giữa việc đọc và ghi có một khoảng trống. Nó nhỏ, thường là vài mili giây, nhưng nó thực sự tồn tại. Bất cứ điều gì xảy ra trong khoảng trống đó đều không nhìn thấy được bởi bộ xử lý này.
Bây giờ hãy đặt hai nhân viên vào khoảng trống đó cùng lúc. Một người thay đổi địa chỉ, người kia thay đổi hãng vận chuyển:
Cả hai yêu cầu đều nhận được phản hồi thành công, và cả hai đều cập nhật chính xác một hàng. Không ai trong số họ làm gì sai ở khía cạnh riêng.
Nhưng hàng cuối cùng có địa chỉ “Amsterdam” và hãng vận chuyển “UPS”. Thay đổi địa chỉ của Yêu cầu A đã biến mất, và không ai được thông báo.
Đây là ý nghĩa của mất cập nhật: một người ghi âm lặng lẽ ghi đè lên thay đổi của người ghi khác vì nó không bao giờ thấy nó.
Phản ứng đầu tiên là bọc bộ xử lý trong giao dịch, và điều đó không khắc phục được.
Tại mức cô lập Read Committed, mặc định trong PostgreSQL và SQL Server, cả hai giao dịch đọc một hàng cam kết hợp lệ, và cả hai lần ghi đều thành công. Cơ sở dữ liệu đang làm chính xác những gì bạn yêu cầu. Bạn chỉ không bao giờ nói với nó rằng lần ghi thứ hai phụ thuộc vào lần đọc đầu tiên.
Mức cô lập Serializable bắt được điều này, với chi phí là các lỗi tuần tự hóa mà bạn phải thử lại. Nếu bạn muốn bức tranh đầy đủ về những gì mỗi mức bảo vệ, tôi đã trình bày trong [Hướng dẫn đầy đủ về các mức cô lập giao dịch trong SQL](https://antondevtips.com/blog/complete-guide-to-transaction-isolation-levels-in-sql).
Hai kỹ thuật bên dưới khắc phục vấn đề trực tiếp, và bạn có thể áp dụng bất kỳ cái nào tùy theo trường hợp sử dụng.
Đồng thời Tích cực: Phát hiện Xung đột
Đồng thời tích cực bắt đầu với giả định rằng xung đột là hiếm.
Vì vậy, nó không khóa bất cứ điều gì. Nó để cả hai yêu cầu chạy với tốc độ đầy đủ và, tại thời điểm ghi, khiến cơ sở dữ liệu kiểm tra xem có ai đã thay đổi hàng trong lúc đó không.
Cơ chế là một token đồng thời: một cột có giá trị thay đổi mỗi lần cập nhật. Bạn đọc nó cùng với hàng, và câu lệnh UPDATE của bạn mang nó trong mệnh đề WHERE.
Nếu token trong cơ sở dữ liệu không còn khớp với token bạn đã đọc, câu lệnh UPDATE của bạn khớp với zero hàng, và bạn biết có người đã đến trước.
EF Core hỗ trợ tính năng này ngay trong hộp, và bạn có ba cách để cấu hình nó.
PostgreSQL, sử dụng cột hệ thống xmin tích hợp:
modelBuilder.Entity<Shipment>()
.UseXminAsConcurrencyToken();
Đây là tùy chọn rẻ nhất trên PostgreSQL vì xmin đã tồn tại trên mỗi hàng. Bạn nhận được kiểm tra đồng thời mà không cần thêm cột hay viết di chuyển.
SQL Server, sử dụng cột rowversion:
modelBuilder.Entity<Shipment>()
.Property<byte[]>("Version")
.IsRowVersion();
SQL Server tự duy trì giá trị này mỗi lần cập nhật, vì vậy bạn không bao giờ gán nó trong mã.
Bất kỳ nhà cung cấp nào, sử dụng token của riêng bạn:
public class Shipment
{
// ...
public Guid Version { get; set; }
}
modelBuilder.Entity<Shipment>()
.Property(s => s.Version)
.IsConcurrencyToken();
Token thủ công là cái tôi sử dụng thường xuyên nhất, và không phải vì tính di động của nhà cung cấp. Nó là một cột Guid đơn giản, vì vậy nó tồn tại qua chuyến đi đến trình duyệt hoặc ứng dụng di động, điều mà API web không trạng thái cần.
Bạn phải tự thay đổi giá trị. Nơi sạch sẽ nhất là ghi đè SaveChangesAsync trên DbContext:
public override Task<int> SaveChangesAsync(CancellationToken ct = default)
{
var entries = ChangeTracker.Entries<Shipment>()
.Where(e => e.State is EntityState.Added or EntityState.Modified);
foreach (var entry in entries)
{
entry.Entity.Version = Guid.NewGuid();
}
return base.SaveChangesAsync(ct);
}
Bất kể bạn chọn tùy chọn nào, EF Core giờ tạo một câu lệnh UPDATE khác:
UPDATE shipments
SET address = @p0, carrier = @p1, version = @p2
WHERE id = @p3 AND version = @p4;
Mệnh đề version = @p4 là toàn bộ mẹo. EF Core gửi giá trị nó đã đọc, đếm số hàng thực tế mà câu lệnh đã thay đổi, và ném DbUpdateConcurrencyException khi câu trả lời là zero.
Đây là cuộc đua tương tự như trước, với token đã đặt:
Yêu cầu A vẫn thắng cuộc đua, chính xác như trước. Sự khác biệt là Yêu B giờ phát hiện ra thay vì lặng lẽ thay thế giá trị.
Có một phần dễ bỏ qua trong API web. Hai yêu cầu của bạn không chia sẻ DbContext, và chúng thậm chí không chồng chéo về thời gian. Nhân viên mở biểu mẫu chỉnh sửa, suy nghĩ trong hai phút, và gửi.
Để kiểm tra có ý nghĩa, phiên bản phải đi đến client và quay lại:
public sealed record ShipmentResponse(
string Number,
string Address,
string Carrier,
string Version);
public sealed record UpdateShipmentRequest(
string Number,
string Address,
string Carrier,
string Version);
Sau đó bạn nói EF Core sử dụng phiên bản của client thay vì phiên bản bạn vừa đọc từ cơ sở dữ liệu:
dbContext.Entry(shipment)
.Property(s => s.Version)
.OriginalValue = Guid.Parse(request.Version);
shipment.Address = request.Address;
shipment.Carrier = request.Carrier;
await dbContext.SaveChangesAsync(ct);
Đặt OriginalValue là điều đưa phiên bản của client vào mệnh đề WHERE. Nếu không có dòng này, bạn đang so sánh hàng với giá trị bạn đọc cách đây vài mili giây, và hai phút khi nhân viên nhập liệu hoàn toàn không được kiểm tra.
Bây giờ lần ghi thất bại khi nó nên. Câu hỏi tiếp theo là phải làm gì.
Xử lý DbUpdateConcurrencyException
DbUpdateConcurrencyException không phải là lỗi bạn ghi lại và quên đi. Thay đổi của ai đó sắp bị loại bỏ, và bạn phải quyết định của ai.
try
{
await dbContext.SaveChangesAsync(ct);
return Result.Success();
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var databaseValues = await entry.GetDatabaseValuesAsync(ct);
if (databaseValues is null)
{
return Result.Conflict("The shipment was deleted by another user");
}
// giải quyết xung đột ở đây
}
Kết quả null có nghĩa là hàng đã biến mất hoàn toàn. Ai đó đã xóa lô hàng trong khi người dùng đang chỉnh sửa, và không có chiến lược hợp nhất nào có thể giúp.
Kho lưu trữ thắng. Loại bỏ chỉnh sửa của người dùng và hiển thị dữ liệu hiện tại:
await entry.ReloadAsync(ct);
return Result.Conflict(
"This shipment was changed by another user. Review the current values and try again.");
Đây là tùy chọn an toàn nhất và là mặc định của tôi cho bất cứ điều gì con người chỉnh sửa. Không có gì bị ghi đè lặng lẽ, và người đó có thể quyết định với dữ liệu thực trước mắt.
Client thắng. Đặt giá trị của bạn qua:
entry.OriginalValues.SetValues(databaseValues);
await dbContext.SaveChangesAsync(ct);
Sao chép giá trị cơ sở dữ liệu vào OriginalValues làm mới token, vì vậy lần UPDATE thứ hai khớp với hàng, và thay đổi của bạn ghi đè lên thay đổi của người dùng khác. Chỉ sử dụng điều này khi người ghi của bạn có thẩm quyền, chẳng hạn như tác vụ nhập sở hữu bản ghi.
Hợp nhất. Giữ các trường người dùng thực sự thay đổi và lấy phần còn lại từ cơ sở dữ liệu:
var currentValues = entry.CurrentValues;
foreach (var property in currentValues.Properties)
{
var proposed = currentValues[property];
var original = entry.OriginalValues[property];
var fromDatabase = databaseValues[property];
if (Equals(proposed, original))
{
currentValues[property] = fromDatabase;
}
}
entry.OriginalValues.SetValues(databaseValues);
await dbContext.SaveChangesAsync(ct);
Vòng lặp so sánh giá trị đề xuất của mỗi thuộc tính với giá trị tải ban đầu. Nếu chúng bằng nhau, người dùng không chạm vào trường đó, vì vậy giá trị mới hơn của cơ sở dữ liệu thắng. Nếu chúng khác nhau, chỉnh sửa của người dùng được giữ lại.
Hai nhân viên chỉnh sửa các trường khác nhau của cùng một lô hàng đều có thay đổi của họ qua, đó là lý do tại sao hợp nhất đáng giá mã bổ sung trên biểu mẫu với nhiều trường độc lập.
Cả ba chiến lược đều giả định có ai đó đang chờ câu trả lời. Công việc nền cần cái gì khác.
Thử lại Xung đột với Pipeline Khả năng phục hồi
Khi không có người dùng nào đang xem, xung đột đồng thời chỉ là lỗi tạm thời. Phản ứng đúng là đọc hàng mới và lặp lại công việc.
Đó là thử lại, và trong .NET bạn có thể sử dụng [thư viện Polly](https://antondevtips.com/blog/how-to-implement-retries-and-resilience-patterns-with-polly-and-microsoft-resilience) và tạo pipeline xử lý chính xác ngoại lệ này:
var pipeline = new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions
{
ShouldHandle = new PredicateBuilder()
.Handle<DbUpdateConcurrencyException>(),
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(20),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true
})
.Build();
UseJitter quan trọng ở đây. Nếu không, nhiều worker va chạm trên cùng một hàng sẽ lùi lại với độ trễ giống nhau và va chạm lại trong lần thử tiếp theo. Jitter phân tán chúng.
Pipeline phải bọc toàn bộ đọc-sửa-ghi, không chỉ lưu:
await pipeline.ExecuteAsync(async token =>
{
await using var dbContext = await dbContextFactory.CreateDbContextAsync(token);
var shipment = await dbContext.Shipments
.FirstAsync(s => s.Number == number, token);
shipment.Status = ShipmentStatus.Dispatched;
shipment.DispatchedAtUtc = DateTime.UtcNow;
await dbContext.SaveChangesAsync(token);
}, ct);
Thử lại chỉ SaveChangesAsync sẽ gửi cùng token cũ lại, khiến mọi lần thử thất bại vì lý do tương tự. Việc đọc phải xảy ra lại.
Lưu ý: mỗi lần thử cần DbContext riêng, được tạo ở đây thông qua IDbContextFactory. Một context mà SaveChanges đã ném ngoại lệ vẫn giữ thực thể thất bại trong tracker thay đổi với giá trị cũ, lỗi và tái sử dụng nó biến một thử lại sạch thành lỗi vi mô. Xem Cách Quản lý vòng đời DbContext EF Core tại sao factory là công cụ đúng trong công việc nền.
Đồng thời Tiêu cực: Khóa Hàng trước
Đồng thời tiêu cực bắt đầu từ giả định ngược lại: xung đột được dự kiến.
Thay vì phát hiện xung đột sau khi ghi, nó ngăn người ghi thứ hai tạo ra một. Giao dịch đầu tiên khóa hàng, và mọi người khác đợi lượt.
Trường hợp kinh điển là bộ đếm mà nhiều yêu cầu cùng tiếp cận. Trong lĩnh vực logistics của chúng ta, đó là tồn kho kho:
public class StockItem
{
public Guid Id { get; set; }
public string Sku { get; set; }
public int Quantity { get; set; }
}
Trong đợt giảm giá, hàng trăm yêu cầu mỗi giây giảm số lượng của cùng một SKU phổ biến. Đồng thời tích cực xử lý điều này kém vì, khi có nhiều tranh chấp, hầu hết các lần thử thua cuộc đua, và các lần thử lại chồng chất lên cùng một hàng.
Khóa cung cấp cho bạn hàng đợi (ví dụ cho Postgres):
await using var transaction = await dbContext.Database.BeginTransactionAsync(ct);
var stockItem = await dbContext.StockItems
.FromSql($"SELECT * FROM stock_items WHERE sku = {sku} FOR UPDATE")
.SingleAsync(ct);
if (stockItem.Quantity < request.Quantity)
{
await transaction.RollbackAsync(ct);
return Result.Conflict($"Not enough stock for SKU '{sku}'");
}
stockItem.Quantity -= request.Quantity;
await dbContext.SaveChangesAsync(ct);
await transaction.CommitAsync(ct);
FOR UPDATE lấy khóa cấp hàng mà cơ sở dữ liệu giữ cho đến khi giao dịch cam kết hoặc hoàn nguyên.
Yêu cầu thứ hai bị chặn trên SELECT thay vì UPDATE. Đó là sự khác biệt quan trọng so với mọi thứ ở trên: khi nó đọc, giao dịch đầu tiên đã hoàn thành, và nó thấy số lượng mới. Không có khoảng trống, vì vậy không có xung đột để giải quyết.
EF Core không có API tích hợp cho khóa tiêu cực, vì vậy truy vấn được triển khai thủ công sử dụng phương thức FromSql. {sku} nội suy trở thành tham số SQL thực, vì vậy vẫn an toàn với tiêm.
Hai yêu cầu tương tự với khóa tiêu cực:
So sánh điều này với hai sơ đồ trước. Trong cả ba, Yêu cầu A thắng. Điều thay đổi là điều gì xảy ra với Yêu cầu B: nó lặng lẽ phá hủy công việc của A, nó nhận ngoại lệ, hoặc nó đợi rồi đọc giá trị đúng.
Lưu ý: trên SQL Server, tương đương là gợi ý khóa: SELECT * FROM StockItems WITH (UPDLOCK, ROWLOCK) WHERE Sku = @sku.
Mặc định, yêu cầu bị chặn đợi cho đến khi khóa được giữ, vì vậy đặt timeout ngay sau khi bạn mở giao dịch:
await dbContext.Database.ExecuteSqlRawAsync("SET LOCAL lock_timeout = '3s'", ct);
PostgreSQL cung cấp hai biến thể bổ sung值得了解.
FOR UPDATE NOWAIT thất bại ngay lập tức thay vì chờ đợi, ném lỗi 55P03. Sử dụng nó khi từ chối nhanh tốt hơn cho người gọi hơn là thành công chậm.
FOR UPDATE SKIP LOCKED bỏ qua hàng mà người khác đã khóa. Đây là cách bạn xây dựng hàng đợi công việc trên bảng mà nhiều worker cùng poll:
var jobs = await dbContext.ShipmentJobs
.FromSql($"""
SELECT * FROM shipment_jobs
WHERE status = 'Pending'
ORDER BY created_at
LIMIT {batchSize}
FOR UPDATE SKIP LOCKED
""")
.ToListAsync(ct);
Mỗi worker nhận một batch khác nhau, và không ai trong số họ đợi nhau. Tôi đã sử dụng điều này để quét bảng Outbox trong ứng dụng sản phẩm, và với khối lượng dữ liệu lớn, nó hoạt động tốt.
Khóa không miễn phí, và chi phí là:
- Giao dịch vẫn mở trong suốt đọc-sửa-ghi, giữ kết nối từ pool trong suốt thời gian đó
- Nguy cơ deadlock trở nên có thể ngay khi hai đường mã khóa cùng hàng theo thứ tự khác nhau
- Nó chỉ hoạt động trong một giao dịch, vì vậy bạn không thể giữ khóa trong khi người dùng điền vào biểu mẫu
- Một hoạt động chậm trong khóa chặn mọi người ghi khác trên hàng đó
Điểm cuối值得 lặp lại. Không bao giờ gọi API bên ngoài trong giao dịch bị khóa, vì timeout ba giây của nhà cung cấp thanh toán trở thành ba giây chặn cho mọi yêu cầu xếp hàng sau bạn.
Cho một bộ đếm duy nhất, có cách bỏ qua cả hai mẫu.
Một Lựa chọn thứ ba: Một câu lệnh Nguyên tử
Đồng thời tích cực và tiêu cực đều tồn tại vì khoảng trống giữa việc đọc giá trị và ghi lại. Đôi khi bạn có thể chỉ loại bỏ khoảng trống.
Nếu giá trị mới là hàm của giá trị cũ, cơ sở dữ liệu có thể tính toán nó trong một câu lệnh, và ExecuteUpdateAsync biểu达 điều đó trong LINQ:
var affected = await dbContext.StockItems
.Where(s => s.Sku == sku && s.Quantity >= request.Quantity)
.ExecuteUpdateAsync(
s => s.SetProperty(x => x.Quantity, x => x.Quantity - request.Quantity),
ct);
if (affected == 0)
{
return Result.Conflict($"Not enough stock for SKU '{sku}'");
}
Điều đó tạo ra một chuyến đi duy nhất:
UPDATE stock_items
SET quantity = quantity - @p0
WHERE sku = @p1 AND quantity >= @p2;
Đọc và ghi xảy ra trong một câu lệnh duy nhất, và cơ sở dữ liệu tự động khóa hàng trong thời gian đó. Không thực thể nào được tải, không giao dịch nào vẫn mở qua mạng, và không có gì để thử lại.
Mệnh đề bảo vệ quantity >= @p2 là điều khiến nó đúng. Nó ngăn cập nhật chạy khi tồn kho đã giảm dưới yêu cầu của người gọi, và số hàng bị ảnh hưởng cho biết liệu nó có chạy không. Zero hàng có nghĩa là hoặc SKU không tồn tại hoặc không đủ tồn kho, và cả hai dẫn đến cùng câu trả lời cho người gọi.
Lưu ý: ExecuteUpdateAsync bỏ qua Change Tracker EF Core. Token đồng thời của bạn không được kiểm tra, các thực thể đã tải trong bộ nhớ lặng lẽ trở nên lỗi thời, và không có interceptor SaveChanges hay sự kiện miền nào được kích hoạt. Bảo vệ trong Where là sự bảo vệ duy nhất bạn nhận được, vì vậy nó phải đúng. Tôi đã trình bày các hậu quả khác trong Cách đúng để sử dụng các phương thức ExecuteUpdate và ExecuteDelete trong EF Core.
Cách tiếp cận này bị hạn chế và hoạt động cho bộ đimestock, cờ, và chuyển đổi trạng thái, nơi quy tắc phù hợp trong một mệnh đề SQL duy nhất. Ngay khi quyết định cần nhiều thực thể, dữ liệu bên ngoài, hoặc logic nằm trong mô hình miền của bạn, bạn quay lại chọn giữa đồng thời tích cực và tiêu cực.
Đồng thời Tích cực vs Tiêu cực: So sánh Chi tiết
Hãy so sánh hai cách tiếp cận theo các tiêu chí thực sự quyết định sự lựa chọn.
Xử lý xung đột
- Tích cực: phát hiện xung đột sau sự kiện và thất bại lần ghi
- Tiêu cực: ngăn xung đột bằng cách khiến người ghi thứ hai đợi
Mức tranh chấp tốt nhất
- Tích cực: thấp đến trung bình, nơi xung đột là ngoại lệ
- Tiêu cực: cao, nơi nhiều người ghi cùng tiếp cận hàng liên tục
Thông lượng và độ trễ
- Tích cực: nhanh khi xung đột hiếm, và nó suy giảm khi tranh chấp tăng vì mọi cuộc đua thua là công việc lãng phí cộng với thử lại
- Tiêu cực: trần thấp hơn, nhưng có thể dự đoán, vì mỗi người ghi đợi khóa thay vì thực hiện lại thao tác
Chế độ thất bại
- Tích cực: ngoại lệ mã của bạn phải xử lý, trên mỗi đường dẫn cập nhật
- Tiêu cực: đợi, timeout khóa, hoặc deadlock
Nguy cơ deadlock
- Tích cực: không có, vì không có gì bị khóa
- Tiêu cực: có thật ngay khi hai đường mã lấy khóa theo thứ tự khác nhau
Thời lượng giao dịch
- Tích cực: ngắn, vì giao dịch chỉ là lưu
- Tiêu cực: bao gồm toàn bộ đọc-sửa-ghi, giữ kết nối trong suốt
Chỉnh sửa lâu dài và không trạng thái
- Tích cực: hoạt động qua các yêu cầu, vì token đi đến client và quay lại
- Tiêu cực: không thể, vì khóa không thể tồn tại giữa hai yêu cầu HTTP
Độ phức tạp
- Tích cực: token, cộng với mã giải quyết xung đột và thử lại trong mỗi đường dẫn ghi
- Tiêu cực: SQL thô cho khóa, cộng với timeout và thứ tự khóa nhất quán
Hỗ trợ cơ sở dữ liệu
- Tích cực: hạng nhất trong EF Core thông qua token đồng thời
- Tiêu cực: không có API EF Core, vì vậy gợi ý khóa được viết tay (SQL) cho mỗi nhà cung cấp cơ sở dữ liệu
Qua các dịch vụ hoặc cơ sở dữ liệu
- Tích cực: hoạt động ở bất cứ đâu, vì token chỉ là giá trị bạn đọc, truyền đi, và so sánh
- Tiêu cực: hoạt động qua bất kỳ số lượng dịch vụ nào chia sẻ cùng cơ sở dữ liệu, vì khóa sống trong cơ sở dữ liệu và không trong quy trình của bạn. Nó ngừng hoạt động khi dữ liệu được tách thành các cơ sở dữ liệu riêng biệt, hoặc khi khóa cần tồn tại qua một giao dịch
Không có cách tiếp cận nào tốt hơn trên toàn cầu, và hầu hết các hệ thống tôi đã làm việc cuối cùng sử dụng cả hai. Các phần tương tác của ứng dụng là tích cực, và một số bộ đếm nóng là tiêu cực hoặc nguyên tử.
Tóm tắt
Khi bạn chọn cách tiếp cận cho một thao tác cụ thể, hãy xem qua danh sách này.
Sử dụng đồng thời tích cực khi:
- Xung đột là hiếm, điều này đúng với hầu hết thực thể kinh doanh
- Một người chỉnh sửa dữ liệu qua biểu mẫu, qua các yêu cầu riêng biệt
- Cửa sổ chỉnh sửa dài, từ vài giây đến vài phút
- Bạn có thể hiển thị thông báo có ý nghĩa cho người dùng và để họ quyết định
- Đường dẫn ghi qua các dịch vụ hoặc cơ sở dữ liệu, vì vậy không có khóa chung
Sử dụng đồng thời tiêu cực khi:
- Nhiều người ghi cùng tiếp cận hàng cùng lúc và các thử lại tích cực liên tục thất bại
- Thao tác phải thành công trong lần thử đầu tiên, không phải cuối cùng
- Sự chính xác phụ thuộc vào việc đọc nhiều hàng và ghi chúng như một đơn vị
- Bạn đang phân phối công việc từ bảng hàng đợi (outbox), đây là trường hợp
SKIP LOCKED - Toàn bộ thao tác ngắn và chạy trong một giao dịch
Sử dụng một câu lệnh nguyên tử khi:
- Bạn đang thay đổi bộ đếm hoặc cờ, và giá trị mới tuân theo giá trị cũ
- Quy tắc phù hợp trong một mệnh đề SQL, chẳng hạn như
quantity >= @requested - Bạn muốn thông lượng tối đa trên hàng nóng mà không có logic thử lại
Cờ đỏ cho thấy bạn chọn sai:
- Ngoại lệ đồng thời liên tục hiển thị trong nhật ký, vì vậy tích cực là không phù hợp cho hàng đó
- Yêu cầu timeout chờ khóa, vì vậy giao dịch tiêu cực đang thực hiện quá nhiều công việc
- Lỗi deadlock xuất hiện, vì vậy hai đường lấy cùng khóa theo thứ tự khác nhau
- Người dùng báo cáo thay đổi biến mất, vì vậy bạn chưa có kiểm soát đồng thời nào
Hy vọng bạn thấy bản tin này hữu ích. Hẹn gặp lại lần sau.



