Quản lý Truy cập Đồng Thời trong Cơ Sở Dữ Liệu: Khóa Bi quan và Khóa Lạc quan

Khi nhiều giao dịch cùng thao tác trên cùng một tập dữ liệu, hiện tượng truy cập đồng thời (concurrency) phát sinh. Việc xử lý không đúng có thể dẫn đến mất tính toàn vẹn dữ liệu — ví dụ như ghi đè giá trị, đọc dữ liệu lỗi (dirty read), hoặc trạng thái không nhất quán. Hai chiến lược cơ bản để kiểm soát điều này là khóa bi quan (pessimistic locking) và khóa lạc quan (optimistic locking), mỗi loại phù hợp với các mô hình tải và yêu cầu hiệu năng khác nhau.

Khóa Bi quan: Ưu tiên An toàn bằng Cách Giới hạn Truy Cập

Khóa bi quan giả định xung đột xảy ra thường xuyên, do đó áp dụng cơ chế khóa sớm — tức là giữ tài nguyên (hàng, trang, bảng) ngay từ lúc đọc đầu tiên cho đến khi giao dịch kết thúc. Điều này đảm bảo tính toàn vẹn tuyệt đối nhưng đánh đổi bằng khả năng mở rộng: các phiên khác phải chờ, dẫn đến độ trễ cao và tắc nghẽn (blocking) trong hệ thống có lưu lượng lớn.

Dưới đây là một thủ tục lưu trữ SQL Server sử dụng UPDLOCK để ngăn chặn việc đọc và cập nhật song song trên cùng bản ghi:

CREATE OR ALTER PROCEDURE [dbo].[LockStudentPessimistically]
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @currentAge INT;

    BEGIN TRANSACTION;
    
    -- Đọc và khóa hàng ngay lập tức để tránh tranh chấp
    SELECT @currentAge = Age 
    FROM Student WITH (UPDLOCK, ROWLOCK) 
    WHERE FirstName = N'fengjie';

    -- Giả lập xử lý kéo dài (ví dụ: logic nghiệp vụ phức tạp)
    WAITFOR DELAY '00:00:05';

    UPDATE Student 
    SET Age = @currentAge - 1 
    WHERE FirstName = N'fengjie';

    COMMIT TRANSACTION;
END;

Khóa Lạc quan: Tối ưu Hiệu năng bằng Kiểm tra Cuối Cùng

Khóa lạc quan không khóa dữ liệu trong suốt quá trình xử lý. Thay vào đó, nó ghi lại một dấu vết (thường là cột rowversion hoặc timestamp) tại thời điểm đọc. Khi thực hiện UPDATE, hệ thống so sánh giá trị dấu vết hiện tại với giá trị đã lưu — nếu không khớp, nghĩa là dữ liệu đã bị thay đổi bởi giao dịch khác, và câu lệnh sẽ không ảnh hưởng đến bất kỳ hàng nào (ROWS AFFECTED = 0).

Thủ tục sau minh họa cách triển khai:

CREATE OR ALTER PROCEDURE [dbo].[UpdateStudentOptimistically]
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @expectedVersion ROWVERSION;
    DECLARE @currentAge INT;

    SELECT @expectedVersion = RowVersion, @currentAge = Age
    FROM Student 
    WHERE FirstName = N'fengjie';

    -- Chỉ cập nhật nếu phiên bản chưa thay đổi
    UPDATE Student 
    SET Age = @currentAge - 1 
    WHERE FirstName = N'fengjie' 
      AND RowVersion = @expectedVersion;
END;

Phía ứng dụng cần xử lý trường hợp cập nhật thất bại. Ví dụ trong C# với Entity Framework:

public int TryUpdateWithRetry(string firstName, int maxRetries = 5)
{
    for (int i = 0; i <= maxRetries; i++)
    {
        try
        {
            using var context = new BingFaTestEntities();
            var student = context.Student
                .FirstOrDefault(s => s.FirstName == firstName);

            if (student == null) return 0;

            student.Age -= 1;
            return context.SaveChanges(); // Có thể ném DbUpdateConcurrencyException
        }
        catch (DbUpdateConcurrencyException)
        {
            if (i == maxRetries) throw;
            Thread.Sleep(300); // Chờ ngắn trước khi thử lại
        }
    }
    return 0;
}

Tích hợp với Entity Framework

Khi dùng mô hình Database-First, bạn có thể cấu hình thuộc tính RowVersion trong file EDMX hoặc qua Fluent API để EF tự động thêm điều kiện kiểm tra phiên bản vào mệnh đề WHERE của lệnh UPDATE. Giá trị thuộc tính ConcurrencyMode nên được đặt thành Fixed.

Nếu cập nhật thất bại do xung đột, EF sẽ ném ngoại lệ DbUpdateConcurrencyException. Bạn có thể bắt ngoại lệ này và quyết định hành vi tiếp theo — như tải lại dữ liệu, thông báo người dùng, hoặc tự động hợp nhất thay đổi.

public class ConcurrencyAwareContext : BingFaTestEntities
{
    public override int SaveChanges()
    {
        try
        {
            return base.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex)
        {
            // Ghi log, phân tích entityEntries, hoặc gọi Resolve() tùy ngữ cảnh
            throw new InvalidOperationException("Dữ liệu đã bị thay đổi bởi người dùng khác.", ex);
        }
    }
}

Thẻ: SQL-Server entity-framework concurrency-control rowversion updlock

Đăng vào ngày 22 tháng 6 lúc 23:05