Chuyển tới nội dung chính

12.11 — Mini case study

Tóm tắt

Một sự cố database điển hình và gây bối rối: 40 deadlock mỗi giờ trong giờ cao điểm, và điều lạ là nó xảy ra giữa cùng một đoạn code chạy song song — không phải giữa hai chức năng khác nhau. Nguyên nhân là thứ tự khoá không xác định: hai giao dịch cùng cập nhật hai bản ghi nhưng theo thứ tự ngược nhau, nên mỗi bên giữ thứ bên kia cần. Bài này chỉ cách đọc deadlock graph để tìm chính xác hai câu lệnh liên quan — thông tin mà log ứng dụng không bao giờ cho bạn — và ba cách sửa theo chi phí tăng dần, trong đó cách rẻ nhất chỉ là thêm một ORDER BY.

Mục tiêu bài học​

Sau bài này bạn có thể:

  • Đọc deadlock graph để tìm nguyên nhân.
  • Nhận ra deadlock do thứ tự khoá không xác định.
  • Sửa bằng cách sắp xếp thứ tự truy cập.
  • Xử lý deadlock còn lại bằng retry đúng cách.

Nội dung bài học​

12.11.1 — Sự cố​

Giờ cao điểm (9h-11h, 14h-16h):
~40 deadlock/giờ
Lỗi: "Transaction was deadlocked on lock resources with another process
and has been chosen as the deadlock victim"

Đoạn code gây lỗi: CHỈ MỘT — PointTransferService.TransferAsync
public async Task TransferPointsAsync(Guid fromId, Guid toId, int points, CancellationToken ct)
{
await using var tx = await _db.Database.BeginTransactionAsync(ct);

var from = await _db.Accounts.FirstAsync(a => a.Id == fromId, ct);
from.Points -= points;
await _db.SaveChangesAsync(ct); // KHOÁ hàng "from"

var to = await _db.Accounts.FirstAsync(a => a.Id == toId, ct);
to.Points += points;
await _db.SaveChangesAsync(ct); // KHOÁ hàng "to"

await tx.CommitAsync(ct);
}

Deadlock xảy ra giữa chính đoạn code này với chính nó:

Giao dịch A: chuyển điểm từ X sang Y
Giao dịch B: chuyển điểm từ Y sang X <- NGƯỢC chiều

t=0 A khoá X
t=1 B khoá Y
t=2 A chờ khoá Y (B đang giữ)
t=3 B chờ khoá X (A đang giữ)
-> DEADLOCK

Cả hai giao dịch đều đúng về mặt logic. Vấn đề nằm ở thứ tự khoá được lấy.

12.11.2 — Đọc deadlock graph​

Log ứng dụng chỉ nói "deadlock xảy ra" — không nói giữa câu lệnh nào. Deadlock graph của SQL Server thì có.

-- Bật Extended Events để thu deadlock graph
CREATE EVENT SESSION [DeadlockCapture] ON SERVER
ADD EVENT sqlserver.xml_deadlock_report
ADD TARGET package0.event_file (SET filename = N'deadlocks.xel');

ALTER EVENT SESSION [DeadlockCapture] ON SERVER STATE = START;
-- Đọc lại
SELECT CAST(event_data AS XML) AS DeadlockGraph
FROM sys.fn_xe_file_target_read_file('deadlocks*.xel', NULL, NULL, NULL);

Ba phần quan trọng trong graph:

<deadlock>
<victim-list>
<victimProcess id="process1a2b3c" /> <!-- giao dịch BỊ HUỶ -->
</victim-list>
<process-list>
<process id="process1a2b3c" ...>
<inputbuf>UPDATE Accounts SET Points = ... WHERE Id = @p0</inputbuf>
<!-- câu lệnh giao dịch này ĐANG chạy -->
</process>
<process id="process4d5e6f" ...>
<inputbuf>UPDATE Accounts SET Points = ... WHERE Id = @p0</inputbuf>
<!-- CÙNG câu lệnh -> deadlock giữa code với chính nó -->
</process>
</process-list>
<resource-list>
<keylock objectname="dbo.Accounts" ...> <!-- tài nguyên tranh chấp -->
<owner-list><owner id="process4d5e6f" /></owner-list>
<waiter-list><waiter id="process1a2b3c" /></waiter-list>
</keylock>
</resource-list>
</deadlock>
PhầnCho biết
victim-listGiao dịch nào bị huỷ
inputbuf của từng processCâu lệnh cụ thể — thông tin quan trọng nhất
resource-listTài nguyên nào bị tranh chấp và theo kiểu khoá gì

Thấy cùng một câu lệnh trong cả hai process là dấu hiệu dứt khoát của deadlock do thứ tự khoá (bài 12.7).

12.11.3 — Cách sửa 1: sắp xếp thứ tự khoá​

Cách rẻ nhất và hiệu quả nhất: luôn khoá theo một thứ tự nhất quán.

public async Task TransferPointsAsync(Guid fromId, Guid toId, int points, CancellationToken ct)
{
await using var tx = await _db.Database.BeginTransactionAsync(ct);

// Nạp CẢ HAI tài khoản theo thứ tự ID tăng dần — NHẤT QUÁN mọi lần
var ids = new[] { fromId, toId }.OrderBy(id => id).ToArray();

var accounts = await _db.Accounts
.Where(a => ids.Contains(a.Id))
.OrderBy(a => a.Id) // thứ tự khoá XÁC ĐỊNH
.ToListAsync(ct);

var from = accounts.First(a => a.Id == fromId);
var to = accounts.First(a => a.Id == toId);

from.Points -= points;
to.Points += points;

await _db.SaveChangesAsync(ct); // MỘT lần lưu
await tx.CommitAsync(ct);
}
Giao dịch A (X -> Y): khoá theo thứ tự ID -> khoá X rồi Y
Giao dịch B (Y -> X): khoá theo thứ tự ID -> khoá X rồi Y <- CÙNG thứ tự

-> B chờ A xong -> KHÔNG deadlock, chỉ chờ đợi

Thứ tự nhất quán biến deadlock thành chờ đợi — chậm hơn một chút nhưng không thất bại.

Thay đổi thứ hai cũng quan trọng: gộp thành một SaveChangesAsync. Nó rút ngắn khoảng thời gian giữ khoá, giảm cả xác suất tranh chấp lẫn thời gian chờ của giao dịch khác.

Nguyên tắc tổng quát: mọi giao dịch chạm nhiều bản ghi nên truy cập chúng theo cùng một thứ tự — thường là theo khoá chính.

12.11.4 — Cách sửa 2: rút ngắn giao dịch​

Deadlock cần hai giao dịch chồng lấn về thời gian. Giao dịch càng ngắn, xác suất càng thấp.

// SAI — gọi API bên ngoài TRONG transaction
await using var tx = await _db.Database.BeginTransactionAsync(ct);

var from = await _db.Accounts.FirstAsync(...);
await _apiClient.VerifyAsync(from.Id, ct); // 200ms — GIỮ KHOÁ suốt thời gian này
from.Points -= points;

await _db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
// ĐÚNG — mọi thứ chậm/mạng nằm NGOÀI transaction
var verification = await _apiClient.VerifyAsync(fromId, ct); // trước transaction
if (!verification.Ok) return Result.Failure("Xác thực thất bại");

await using var tx = await _db.Database.BeginTransactionAsync(ct);
// ... chỉ thao tác database, nhanh
await tx.CommitAsync(ct);

Quy tắc: trong transaction chỉ có thao tác database. Gọi HTTP, gửi email, đọc file — tất cả ra ngoài.

Một giao dịch giữ khoá 200ms thay vì 5ms làm xác suất deadlock tăng 40 lần.

12.11.5 — Cách sửa 3: retry​

Kể cả sau hai cách trên, deadlock vẫn có thể xảy ra ở mức thấp. Đây là loại lỗi đáng retry vì lần chạy sau thường không trùng thời điểm với giao dịch kia:

builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(cs, sql => sql.EnableRetryOnFailure(
maxRetryCount: 3,
maxRetryDelay: TimeSpan.FromSeconds(5),
errorNumbersToAdd: [1205]))); // 1205 = deadlock victim

Cảnh báo quan trọng: EnableRetryOnFailure không tự retry được khi bạn dùng transaction tường minh — EF Core không biết cách chạy lại một khối BeginTransaction do bạn quản lý. Phải dùng execution strategy:

var strategy = _db.Database.CreateExecutionStrategy();

await strategy.ExecuteAsync(async () =>
{
await using var tx = await _db.Database.BeginTransactionAsync(ct);
// ... thao tac
await tx.CommitAsync(ct);
});

Không có CreateExecutionStrategy, EF Core sẽ ném exception khi bạn dùng transaction tường minh cùng retry — đó là cách nó báo cho bạn biết cấu hình chưa đúng (bài 13.9).

Retry là lớp cuối, không phải lớp đầu. Nếu bạn cần retry cho 40 deadlock mỗi giờ, vấn đề gốc chưa được sửa.

12.11.6 — Kết quả và giám sát​

Thay đổiDeadlock mỗi giờ
Ban đầu40
Sắp xếp thứ tự khoá2
Đưa lời gọi API ra ngoài transaction0,3
Thêm retry0 (người dùng không thấy)

Giám sát để biết nó không quay lại:

-- Dem deadlock tu khi SQL Server khoi dong
SELECT cntr_value AS SoDeadlock
FROM sys.dm_os_performance_counters
WHERE counter_name = 'Number of Deadlocks/sec'
AND instance_name = '_Total';

Đặt cảnh báo trên xu hướng, không trên giá trị tuyệt đối: vài deadlock mỗi ngày là bình thường trong hệ có tải cao; tăng đột ngột thì không.

12.11.7 — Rà lại code của bạn​

Danh sách rà soát deadlock

  • •Giao dịch chạm nhiều bản ghi truy cập chúng theo thứ tự nhất quán.
  • •Không gọi API bên ngoài bên trong transaction.
  • •Không gửi email hay đọc file trong transaction.
  • •Gộp nhiều SaveChanges thành một khi có thể.
  • •Có bắt Extended Events để thu deadlock graph.
  • •Retry dùng CreateExecutionStrategy khi có transaction tường minh.
  • •Mã lỗi 1205 nằm trong danh sách lỗi đáng retry.
  • •Có giám sát số deadlock theo xu hướng.

Bài tập áp dụng​

Bài 1 — Tái hiện deadlock​

Chạy hai giao dịch cập nhật hai bản ghi theo thứ tự ngược nhau và quan sát lỗi 1205.

Tiêu chí hoàn thành: bạn giải thích được vì sao deadlock không phải là timeout, và vì sao database chọn hy sinh một bên thay vì chờ.

Gợi ý và lời giải — Bài 1

Gợi ý. Mở hai cửa sổ truy vấn và chạy xen kẽ từng câu lệnh theo thứ tự đánh số.

Lời giải. Mở hai session. Chạy theo đúng thứ tự thời gian này:

-- Session A, bước 1
BEGIN TRANSACTION;
UPDATE Leads SET Value = Value + 1 WHERE Id = 1;
-- chưa COMMIT, đang giữ khoá trên hàng 1
-- Session B, bước 2
BEGIN TRANSACTION;
UPDATE Leads SET Value = Value + 1 WHERE Id = 2;
-- chưa COMMIT, đang giữ khoá trên hàng 2
-- Session A, bước 3
UPDATE Leads SET Value = Value + 1 WHERE Id = 2;
-- TREO: chờ khoá hàng 2 mà B đang giữ
-- Session B, bước 4
UPDATE Leads SET Value = Value + 1 WHERE Id = 1;
-- TREO: chờ khoá hàng 1 mà A đang giữ

Khoảng năm giây sau, một trong hai session nhận:

Msg 1205, Level 13, State 45, Line 3
Transaction (Process ID 58) was deadlocked on lock resources with another
process and has been chosen as the deadlock victim. Rerun the transaction.

Session còn lại chạy tiếp bình thường.

Vì sao đây không phải timeout — và đây là điểm quan trọng nhất của bài.

Lock timeoutDeadlock
Nguyên nhânBên giữ khoá chậmHai bên chờ vòng tròn
Nếu chờ thêmCuối cùng sẽ đượcKhông bao giờ được
Ai phát hiệnHết hạn LOCK_TIMEOUTDeadlock monitor của database
Mã lỗi12221205
Chờ lâu hơn có giúp khôngCóKhông

Timeout là "tôi chờ đủ lâu rồi, thôi bỏ". Deadlock là một trạng thái có thể chứng minh là không lối thoát: A chờ B, B chờ A, và không ai nhả trước khi được thứ mình chờ.

Vì sao database hy sinh một bên thay vì chờ. Vì chờ là vô ích. SQL Server có một luồng nền gọi là deadmonitor, mặc định chạy mỗi năm giây. Nó dựng đồ thị chờ (wait-for graph): mỗi đỉnh là một giao dịch, mỗi cạnh là "đang chờ khoá của". Nếu đồ thị có chu trình, đó là deadlock.

A --chờ--> B
^ |
| |
+---chờ----+ chu trình => deadlock

Đã có chu trình thì cách duy nhất phá vỡ là loại một đỉnh. Database chọn "nạn nhân" theo DEADLOCK_PRIORITY, và khi ngang nhau thì chọn giao dịch rẻ nhất để rollback — tức là bên đã ghi ít transaction log hơn.

Bạn có thể tự chọn ai chịu thua:

SET DEADLOCK_PRIORITY LOW;   -- job nền nhường cho request của người dùng

Hệ quả cho code ứng dụng: lỗi 1205 là lỗi có thể thử lại được, khác hẳn phần lớn lỗi SQL. Giao dịch bị rollback hoàn toàn và sạch sẽ — không có trạng thái dở dang. Chạy lại thường thành công, vì bên kia lúc này đã commit xong. Xem cách retry đúng ở bài 3.

Đừng nhầm hai điều này: deadlock không có nghĩa là bạn cần transaction ngắn hơn hay index tốt hơn — dù cả hai đều giảm xác suất xảy ra. Nguyên nhân gốc luôn là thứ tự lấy khoá không nhất quán, và bài 3 sửa đúng chỗ đó.


Bài 2 — Đọc deadlock graph​

Bật Extended Events, tái hiện deadlock, và tìm hai câu lệnh trong inputbuf.

Tiêu chí hoàn thành: bạn chỉ ra được trong XML chỗ nào chứng minh thứ tự khoá ngược nhau.

Gợi ý và lời giải — Bài 2

Gợi ý. Phần resource-list cho biết mỗi bên giữ gì và chờ gì.

Lời giải — session system_health đã bắt sẵn deadlock, không cần tạo mới:

SELECT  CAST(event_data AS XML) AS deadlock_xml
FROM sys.fn_xe_file_target_read_file('system_health*.xel', NULL, NULL, NULL)
WHERE object_name = 'xml_deadlock_report'
ORDER BY file_offset DESC;

Nếu muốn một session riêng để dễ tìm:

CREATE EVENT SESSION DeadlockWatch ON SERVER
ADD EVENT sqlserver.xml_deadlock_report
ADD TARGET package0.event_file (SET filename = N'DeadlockWatch.xel')
WITH (STARTUP_STATE = ON);

ALTER EVENT SESSION DeadlockWatch ON SERVER STATE = START;

XML rút gọn của deadlock ở bài 1:

<deadlock>
<victim-list><victimProcess id="process1a2b3c" /></victim-list>
<process-list>
<process id="process1a2b3c" spid="58" transactionname="user_transaction">
<inputbuf>
UPDATE Leads SET Value = Value + 1 WHERE Id = 2;
</inputbuf>
</process>
<process id="process4d5e6f" spid="61" transactionname="user_transaction">
<inputbuf>
UPDATE Leads SET Value = Value + 1 WHERE Id = 1;
</inputbuf>
</process>
</process-list>
<resource-list>
<keylock hobtid="72057594045333504" objectname="Crm.dbo.Leads" mode="X">
<owner-list><owner id="process4d5e6f" mode="X" /></owner-list>
<waiter-list><waiter id="process1a2b3c" mode="X" requestType="wait" /></waiter-list>
</keylock>
<keylock hobtid="72057594045333504" objectname="Crm.dbo.Leads" mode="X">
<owner-list><owner id="process1a2b3c" mode="X" /></owner-list>
<waiter-list><waiter id="process4d5e6f" mode="X" requestType="wait" /></waiter-list>
</keylock>
</resource-list>
</deadlock>

Chỗ chứng minh thứ tự ngược nhau nằm ở resource-list. Đọc hai khối keylock cạnh nhau:

keylock thứ nhất:  owner = process4d5e6f   waiter = process1a2b3c
keylock thứ hai: owner = process1a2b3c waiter = process4d5e6f
^^^^^^^^^^^^^ ^^^^^^^^^^^^^
hai process ĐỔI CHỖ cho nhau giữa owner và waiter

Mỗi process vừa là chủ của một khoá, vừa là kẻ chờ của khoá kia. Đó chính là chu trình trong đồ thị chờ — và nó chỉ xảy ra khi hai bên lấy khoá theo thứ tự ngược nhau.

Ghép với inputbuf thì thấy rõ: process 58 đang chờ ở Id = 2 (nghĩa là nó đã lấy Id = 1 trước), process 61 đang chờ ở Id = 1 (nghĩa là nó đã lấy Id = 2 trước).

process 58:  lấy 1  ->  chờ 2
process 61: lấy 2 -> chờ 1

Vì sao phải đọc graph chứ không đọc log ứng dụng. Log ứng dụng chỉ ghi được câu lệnh của chính nó — câu lệnh bị lỗi 1205. Nửa còn lại của deadlock chạy ở một request khác, có khi ở một pod khác, và thành công nên chẳng ai ghi gì cả. Không có graph, bạn chỉ thấy một nửa câu chuyện và sẽ đi sửa nhầm chỗ.

Ba chi tiết khác đáng đọc trong XML:

  • objectname — bảng nào. Nếu hai keylock trỏ hai bảng khác nhau thì đây là deadlock giữa các bảng, và cách sửa là thống nhất thứ tự truy cập bảng, không phải thứ tự hàng.
  • mode — X là khoá ghi, U là update lock, S là khoá đọc. Deadlock S với X thường sửa được bằng READ COMMITTED SNAPSHOT, còn X với X như ở đây thì không.
  • lockMode trong <waiter> cùng requestType="convert" — dấu hiệu của deadlock chuyển đổi khoá: cả hai bên đang giữ khoá S và cùng muốn nâng lên X. Đây là biến thể rất hay gặp của mẫu "đọc rồi ghi trong cùng một transaction", và cách sửa là dùng UPDLOCK ngay từ lần đọc:
SELECT Value FROM Leads WITH (UPDLOCK) WHERE Id = @id;

Bài 3 — Sửa và đo​

Thêm sắp xếp theo id, chạy lại bài 1 với 100 cặp giao dịch và đếm số deadlock.

Tiêu chí hoàn thành: bạn nêu được vì sao sắp xếp loại bỏ deadlock chứ không chỉ làm nó hiếm đi, và vì sao vẫn cần retry.

Gợi ý và lời giải — Bài 3

Gợi ý. Nghĩ theo đồ thị: nếu mọi giao dịch đều đi theo cùng một chiều, chu trình có thể tồn tại không?

Lời giải — bản có lỗi:

public async Task TransferAsync(int fromId, int toId, decimal amount, CancellationToken ct)
{
await using var tx = await _db.Database.BeginTransactionAsync(ct);

var from = await _db.Accounts.FirstAsync(a => a.Id == fromId, ct);
var to = await _db.Accounts.FirstAsync(a => a.Id == toId, ct);

from.Balance -= amount;
to.Balance += amount;

await _db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
}

Thứ tự khoá ở đây do tham số quyết định. Transfer(1, 2) lấy 1 rồi 2; Transfer(2, 1) lấy 2 rồi 1. Chạy song song là deadlock.

Bản đã sửa:

public async Task TransferAsync(int fromId, int toId, decimal amount, CancellationToken ct)
{
await using var tx = await _db.Database.BeginTransactionAsync(ct);

// Luôn khoá theo id tăng dần, bất kể chiều chuyển tiền là gì
var ids = new[] { fromId, toId }.OrderBy(id => id).ToArray();

var accounts = new Dictionary<int, Account>();
foreach (var id in ids)
accounts[id] = await _db.Accounts.FirstAsync(a => a.Id == id, ct);

accounts[fromId].Balance -= amount;
accounts[toId].Balance += amount;

await _db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
}

Dòng OrderBy là toàn bộ nội dung của bản sửa. Vòng lặp foreach bên dưới chỉ để đảm bảo thứ tự lấy khoá đúng bằng thứ tự đã sắp — gộp hai lần đọc thành một truy vấn WHERE Id IN (...) cũng được, nhưng khi đó thứ tự khoá lại do execution plan quyết định chứ không phải do bạn.

Chạy 100 cặp giao dịch ngược chiều, mỗi cặp song song:

Bản có lỗi:   94 deadlock / 100 cặp
Bản đã sửa: 0 deadlock / 100 cặp

Vì sao sắp xếp loại bỏ hẳn deadlock, không chỉ làm nó hiếm đi. Deadlock cần một chu trình trong đồ thị chờ. Nếu mọi giao dịch đều lấy khoá theo id tăng dần, thì mỗi cạnh "A chờ B" chỉ xuất hiện khi B đang giữ một id lớn hơn hoặc bằng id mà A đang có. Đi theo các cạnh chờ, id chỉ có thể tăng. Mà một chu trình bắt buộc phải quay về điểm xuất phát — tức là id phải giảm lại. Mâu thuẫn.

Không sắp xếp:   1 -> 2 -> 1     chu trình tồn tại được
Sắp xếp: 1 -> 2 -> 3 id luôn tăng, không đóng vòng được

Đây là một bảo đảm cấu trúc, không phải một cải thiện xác suất. Đó là khác biệt giữa cách sửa này và "làm transaction ngắn lại" hay "thêm index" — hai cách sau chỉ thu hẹp cửa sổ va chạm.

Vì sao vẫn cần retry. Vì bảo đảm trên chỉ đúng cho đường code mà bạn đã sắp xếp. Trong một hệ thống thật vẫn còn:

  • Các đường code khác chạm cùng bảng mà chưa được sắp xếp.
  • Lock escalation: khi một giao dịch khoá quá nhiều hàng, SQL Server nâng lên khoá cả bảng — lúc đó thứ tự hàng không còn ý nghĩa.
  • Deadlock trên index phụ hoặc trên khoá ngoại tới bảng khác, xảy ra ở tầng dưới code của bạn.
  • Job nền, báo cáo, migration chạy đồng thời.

Retry đúng cách — chỉ bắt 1205, có độ trễ tăng dần, và giới hạn số lần:

private const int MaxAttempts = 3;

public async Task<T> WithDeadlockRetryAsync<T>(Func<Task<T>> action, CancellationToken ct)
{
for (var attempt = 1; ; attempt++)
{
try
{
return await action();
}
catch (SqlException ex) when (ex.Number == 1205 && attempt < MaxAttempts)
{
// Lùi theo cấp số nhân, cộng nhiễu ngẫu nhiên để hai bên
// không cùng thử lại vào đúng một thời điểm
var delay = TimeSpan.FromMilliseconds(50 * Math.Pow(2, attempt - 1)
+ Random.Shared.Next(0, 50));
_logger.LogWarning("Deadlock lần {Attempt}, thử lại sau {Delay}ms",
attempt, delay.TotalMilliseconds);
await Task.Delay(delay, ct);
}
}
}

Ba điều kiện để retry là đúng đắn, không phải chữa cháy:

  1. Chỉ bắt 1205. catch (SqlException) trần sẽ thử lại cả lỗi vi phạm ràng buộc và lỗi cú pháp — thử lại vô nghĩa và che mất lỗi thật.
  2. Toàn bộ transaction phải nằm trong action. Nếu BeginTransaction ở ngoài, lần thử lại sẽ chạy trên một transaction đã bị rollback.
  3. action phải không có tác dụng phụ ngoài database. Nếu nó gửi email hay bắn message lên queue, lần thử lại sẽ gửi lần thứ hai. Đẩy những việc đó ra sau khi commit — xem outbox pattern ở bài 14.8.

Và đừng quên đo lại sau khi sửa. Nếu số deadlock giảm mà không về không, graph của những ca còn lại sẽ chỉ ra đường code tiếp theo cần sắp xếp:

SELECT COUNT(*) AS deadlocks_last_hour
FROM sys.fn_xe_file_target_read_file('system_health*.xel', NULL, NULL, NULL)
WHERE object_name = 'xml_deadlock_report'
AND timestamp_utc > DATEADD(HOUR, -1, SYSUTCDATETIME());

Tự kiểm tra​

Câu hỏi thường gặp

Vì sao deadlock xảy ra giữa cùng một đoạn code?

Vì thứ tự khoá không xác định. Hai giao dịch cùng cập nhật hai bản ghi nhưng theo thứ tự ngược nhau, nên mỗi bên giữ thứ bên kia đang cần và cả hai cùng chờ.

Deadlock graph cho thông tin gì mà log ứng dụng không có?

Câu lệnh cụ thể mà mỗi giao dịch đang chạy, tài nguyên nào bị tranh chấp và theo kiểu khoá gì, và giao dịch nào bị chọn làm nạn nhân. Log ứng dụng chỉ nói deadlock đã xảy ra.

Vì sao sắp xếp thứ tự khoá lại loại bỏ deadlock?

Vì khi mọi giao dịch lấy khoá theo cùng một thứ tự, không thể có tình huống mỗi bên giữ thứ bên kia cần. Deadlock biến thành chờ đợi, chậm hơn một chút nhưng không thất bại.

Vì sao không được gọi API bên ngoài trong transaction?

Vì nó giữ khoá suốt thời gian chờ mạng. Một giao dịch giữ khoá 200 mili giây thay vì 5 mili giây làm xác suất deadlock tăng hàng chục lần, do deadlock cần hai giao dịch chồng lấn về thời gian.

Cái bẫy khi bật EnableRetryOnFailure là gì?

Nó không tự retry được khi bạn dùng transaction tường minh, vì EF Core không biết cách chạy lại một khối BeginTransaction do bạn quản lý. Phải bọc khối đó trong CreateExecutionStrategy.

Vì sao retry là lớp cuối chứ không phải lớp đầu?

Vì nếu cần retry cho bốn mươi deadlock mỗi giờ thì vấn đề gốc chưa được sửa. Retry chỉ nên xử lý phần deadlock còn sót lại sau khi đã sắp xếp thứ tự khoá và rút ngắn giao dịch.

Kết luận​

Ba điều đáng nhớ nhất:

  1. Thứ tự khoá nhất quán biến deadlock thành chờ đợi — cách sửa rẻ nhất và hiệu quả nhất.
  2. Trong transaction chỉ có thao tác database. Mọi thứ chậm ra ngoài.
  3. Deadlock graph cho biết câu lệnh cụ thể — thông tin log ứng dụng không bao giờ có.

Tham khảo​

Điều hướng​