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

12.7 — 6. Transactions và Locking

Tóm tắt (abstract)

Trong SQL, transaction giúp bạn đảm bảo dữ liệu “đúng trạng thái” (ACID), còn locking/concurrency quyết định mức độ an toàn và khả năng chịu tải khi nhiều người dùng cùng thao tác. Bài này tập trung vào isolation levels, deadlock avoidance và các chiến lược optimistic/pessimistic phù hợp cho CRM production.

12.7 — 6. Transactions và Locking

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

  • Nắm được ý chính của bài và mối liên hệ với module.
  • Áp dụng được kiến thức vào bối cảnh CRM/.NET backend.
  • Sẵn sàng chuyển sang bài kế tiếp với nền tảng chắc chắn.

Nội dung bài học

12.7.1 — 6.1 ACID

Thuộc tínhÝ nghĩa
AtomicityToàn bộ transaction thành công hoặc rollback hoàn toàn
ConsistencyDatabase luôn ở trạng thái hợp lệ trước và sau transaction
IsolationTransaction đang chạy không thấy thay đổi của transaction khác (ở mức độ nhất định)
DurabilitySau khi commit, dữ liệu được lưu vĩnh viễn dù server crash

12.7.2 — 6.2 Isolation Levels

-- SQL Server
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -- Dirty read cho phép
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- Mặc định SQL Server
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- An toàn nhất, chậm nhất

-- PostgreSQL
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- Mặc định PostgreSQL
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- PostgreSQL không có READ UNCOMMITTED thực sự
Isolation LevelDirty ReadNon-Repeatable ReadPhantom Read
Read UncommittedCó thểCó thểCó thể
Read CommittedKhôngCó thểCó thể
Repeatable ReadKhôngKhôngCó thể
SerializableKhôngKhôngKhông

Giải thích các loại anomaly:

  • Dirty read: Đọc dữ liệu chưa commit của transaction khác — nguy hiểm vì transaction kia có thể rollback
  • Non-repeatable read: Đọc cùng row hai lần trong cùng transaction nhưng giá trị khác nhau (transaction khác đã update + commit giữa hai lần đọc)
  • Phantom read: Đọc cùng range query hai lần nhưng kết quả khác nhau (transaction khác đã insert/delete rows phù hợp điều kiện)

12.7.3 — 6.3 Transaction trong CRM

-- Chuyển lead thành customer: phải atomic
BEGIN TRANSACTION;
BEGIN TRY
-- Tạo customer mới
INSERT INTO Customers (CompanyName, AssignedToUserId, Status, CreatedAt, UpdatedAt)
SELECT CompanyName, AssignedToUserId, 'Active', GETUTCDATE(), GETUTCDATE()
FROM Leads WHERE LeadId = @LeadId;

DECLARE @NewCustomerId INT = SCOPE_IDENTITY();

-- Cập nhật lead
UPDATE Leads
SET Status = 'Won',
ConvertedAt = GETUTCDATE(),
ConvertedToCustomerId = @NewCustomerId,
UpdatedAt = GETUTCDATE()
WHERE LeadId = @LeadId;

-- Tạo notification
INSERT INTO Notifications (UserId, Title, Type, RelatedEntityType, RelatedEntityId, CreatedAt)
SELECT AssignedToUserId,
N'Lead "' + CompanyName + N'" đã được convert thành công!',
'Info', 'Customer', @NewCustomerId, GETUTCDATE()
FROM Leads WHERE LeadId = @LeadId;

COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
THROW;
END CATCH;

12.7.4 — 6.4 Deadlock Detection và Xử lý

Deadlock xảy ra khi hai transaction chờ nhau theo vòng tròn. SQL Server tự detect và chọn một transaction làm "victim" (rollback).

-- Simulate deadlock (chạy 2 session song song để test)
-- Session 1:
BEGIN TRANSACTION;
UPDATE Customers SET Status = 'Inactive' WHERE CustomerId = 1;
WAITFOR DELAY '00:00:05';
UPDATE Leads SET Status = 'Lost' WHERE AssignedToUserId = 1;
COMMIT;

-- Session 2 (chạy cùng lúc):
BEGIN TRANSACTION;
UPDATE Leads SET Status = 'Lost' WHERE AssignedToUserId = 1;
WAITFOR DELAY '00:00:05';
UPDATE Customers SET Status = 'Inactive' WHERE CustomerId = 1;
COMMIT;

Cách tránh deadlock:

  1. Luôn lock bảng theo thứ tự nhất quán (ví dụ: Customers trước, Leads sau)
  2. Giữ transaction ngắn nhất có thể
  3. Tránh user interaction bên trong transaction
  4. Dùng READ COMMITTED SNAPSHOT ISOLATION (RCSI) trên SQL Server — đọc không block ghi
-- Bật RCSI cho database (SQL Server)
ALTER DATABASE CrmDb SET READ_COMMITTED_SNAPSHOT ON;

12.7.5 — 6.5 Optimistic so với Pessimistic Concurrency

-- Pessimistic: lock row khi đọc (ngăn concurrent update)
BEGIN TRANSACTION;
SELECT * FROM Leads WITH (UPDLOCK, ROWLOCK) WHERE LeadId = @LeadId;
-- ... logic ...
UPDATE Leads SET Status = @NewStatus WHERE LeadId = @LeadId;
COMMIT;

-- Optimistic: dùng RowVersion / timestamp
ALTER TABLE Leads ADD RowVersion ROWVERSION NOT NULL;

-- Update chỉ thành công nếu RowVersion chưa thay đổi
UPDATE Leads
SET Status = @NewStatus, UpdatedAt = GETUTCDATE()
WHERE LeadId = @LeadId
AND RowVersion = @OriginalRowVersion;

IF @@ROWCOUNT = 0
RAISERROR('Concurrency conflict: Lead was modified by another user.', 16, 1);

Common pitfalls

  • Chọn isolation level “quá nặng” (ví dụ SERIALIZABLE) cho mọi truy vấn → tăng lock/contention, giảm throughput của CRM.
  • Lock theo thứ tự khác nhau giữa các transaction (lệch lock order) → tăng xác suất deadlock.
  • Transaction quá dài (ôm UI/network calls hoặc bước không cần thiết) → giữ lock lâu, khiến hàng đợi tăng và timeouts.
  • Dùng pessimistic locking cho use case không cần → mất lợi ích concurrency; ưu tiên optimistic + retry khi phù hợp.
  • Không xem xét plan/index khi update nhiều rows → tăng thời gian giữ lock và “khuếch đại” contention.

Ví dụ trong CRM/.NET backend

  • Khi chuyển LeadCustomer, bạn cần atomic 3 bước: tạo Customer, cập nhật Leads.Status = 'Won' và ghi Notification để UI hiển thị “đã convert”.
  • Nếu nhiều agent cùng convert một lead (race condition), dùng optimistic concurrency (RowVersion) hoặc kiểm tra invariant trước khi commit để tránh tạo Customer trùng.
  • Nếu gặp deadlock/conflict, hãy retry có giới hạn và trả lỗi rõ ràng (để client biết nên thử lại lúc nào).

Bài tập áp dụng

  1. Tóm tắt bài học bằng ngôn ngữ của bạn.
  2. Liên hệ nội dung với một tình huống thực tế trong dự án.
  3. Đề xuất một cải tiến cụ thể sau khi học bài này.

Tự kiểm tra

  • Bạn có thể giải thích lại nội dung chính trong 2 phút không?
  • Bạn có ví dụ áp dụng thực tế chưa?
  • Bạn biết bước tiếp theo cần học/triển khai là gì không?

Kết luận

Hoàn thành bài này giúp bạn có góc nhìn đầy đủ hơn trước khi đi tiếp trong module.

Điều hướng