0

Apache Derby Bài 31: Các mức độ cô lập giao dịch (Transaction Isolation Levels) và hiện tượng Lock

1. Tính chất Cô lập (Isolation) và các hiện tượng "đọc sai"

Trong Bài 30, chúng ta đã nhắc đến tính chất chữ I (Isolation - Cô lập) trong ACID. Khi có hàng trăm người dùng cùng đọc và ghi dữ liệu đồng thời, nếu hệ quản trị CSDL không có cơ chế cách ly tốt, các giao dịch sẽ giẫm chân lên nhau và sinh ra 3 hiện tượng dữ liệu sai lệch kinh điển sau:

  1. Dirty Read (Đọc rác): Giao dịch A cập nhật số dư tài khoản nhưng chưa COMMIT. Giao dịch B đọc được số dư mới này. Sau đó, giao dịch A bị lỗi và ROLLBACK về số cũ. Lúc này, giao dịch B đang cầm một dữ liệu "rác" không hề tồn tại thực tế.

  2. Non-repeatable Read (Đọc không lặp lại): Giao dịch B đọc một dòng dữ liệu. Giao dịch A nhảy vào UPDATE dòng đó và COMMIT. Khi giao dịch B đọc lại dòng đó lần thứ hai (vẫn trong cùng một transaction), giá trị đã bị thay đổi một cách khó hiểu.

  3. Phantom Read (Đọc bóng ma): Giao dịch B dùng lệnh SELECT đếm số lượng nhân viên phòng IT (ví dụ ra 10 người). Giao dịch A nhảy vào INSERT thêm 1 nhân viên mới vào phòng IT và COMMIT. Khi giao dịch B đếm lại, số lượng bỗng nhiên biến thành 11. Dữ liệu mới xuất hiện như một "bóng ma".

Để ngăn chặn các hiện tượng này, cơ sở dữ liệu cung cấp các Mức độ cô lập (Isolation Levels) khác nhau. Bạn phải đánh đổi: mức độ cô lập càng cao (dữ liệu càng an toàn) thì hiệu năng và khả năng xử lý đồng thời (Concurrency) càng giảm.

2. 4 Mức độ cô lập trong Apache Derby

Apache Derby hỗ trợ chuẩn 4 mức độ cô lập của SQL (tương ứng với hằng số trong giao diện java.sql.Connection):

1. READ UNCOMMITTED (Chưa xác nhận)

  • Ngăn chặn: Không ngăn chặn hiện tượng nào (cho phép Dirty Read).

  • Đặc điểm: Tốc độ nhanh nhất. Derby chỉ áp dụng mức này cho các thao tác chỉ đọc (Read-only). Nó không lấy bất kỳ khóa (lock) nào khi đọc dữ liệu, giúp giảm tối đa độ trễ.

  • Mã JDBC: Connection.TRANSACTION_READ_UNCOMMITTED

2. READ COMMITTED (Đã xác nhận) - Mặc định của Derby

  • Ngăn chặn: Ngăn được Dirty Read. Vẫn bị Non-repeatable Read và Phantom Read.

  • Đặc điểm: Một giao dịch chỉ nhìn thấy những dữ liệu đã được COMMIT bởi các giao dịch khác. Đây là sự cân bằng hoàn hảo giữa hiệu năng và độ chính xác, được Derby (và hầu hết các DB như PostgreSQL, SQL Server) chọn làm mặc định.

  • Mã JDBC: Connection.TRANSACTION_READ_COMMITTED

3. REPEATABLE READ (Đọc lặp lại)

  • Ngăn chặn: Ngăn Dirty Read và Non-repeatable Read. Vẫn có thể bị Phantom Read.

  • Đặc điểm: Derby sẽ giữ chặt các khóa trên những dòng (Row) mà nó đã đọc cho đến khi kết thúc giao dịch. Không ai được phép UPDATE hay DELETE những dòng đó cho đến khi bạn COMMIT.

  • Mã JDBC: Connection.TRANSACTION_REPEATABLE_READ

4. SERIALIZABLE (Tuần tự hóa)

  • Ngăn chặn: Ngăn chặn tuyệt đối cả 3 hiện tượng.

  • Đặc điểm: Mức độ an toàn cao nhất nhưng hiệu năng tệ nhất. Derby sẽ khóa toàn bộ bảng (Table) hoặc dải dữ liệu đang được truy vấn. Các giao dịch diễn ra đồng thời sẽ bị ép phải xếp hàng chạy tuần tự từng cái một.

  • Mã JDBC: Connection.TRANSACTION_SERIALIZABLE

3. Cơ chế Khóa (Locking) của Derby Engine

Để duy trì các mức độ cô lập trên, Derby sử dụng cơ chế Lock (Khóa) phía sau hậu trường. Khi bạn thao tác dữ liệu, Derby sẽ tự động gán các ổ khóa vào tài nguyên đó.

  • Shared Lock (Khóa chia sẻ - S): Được sử dụng khi dùng lệnh SELECT. Nhiều người có thể cùng cầm khóa S trên một dòng dữ liệu (cùng đọc được), nhưng không ai được lấy khóa X để sửa.

  • Exclusive Lock (Khóa độc quyền - X): Được sử dụng khi dùng INSERT, UPDATE, DELETE. Khi một người đang giữ khóa X, không ai khác được phép đọc hay sửa dòng dữ liệu đó.

Lock Escalation (Nâng cấp khóa tự động)

Mặc định, Derby sử dụng Row-level Lock (Khóa cấp dòng). Khóa cấp dòng giúp hàng nghìn người có thể cùng thao tác trên một bảng, miễn là họ sửa các dòng khác nhau.

Tuy nhiên, mỗi ổ khóa sẽ tiêu tốn một ít bộ nhớ RAM. Nếu bạn chạy câu lệnh UPDATE trên 100,000 dòng, việc tạo ra 100,000 ổ khóa cấp dòng sẽ làm sập RAM máy chủ. Lúc này, Derby Optimizer sẽ tự động kích hoạt Lock Escalation: nó hủy toàn bộ khóa cấp dòng và thay bằng một Table-level Lock (Khóa cấp bảng) duy nhất. Toàn bộ bảng sẽ bị khóa cứng cho đến khi cập nhật xong.

4. Hiện tượng Bế tắc (Deadlock) và cách xử lý

Deadlock xảy ra khi hai giao dịch chờ đợi tài nguyên của nhau một cách vô tận.

  • Giao dịch A đang giữ khóa dòng 1, muốn lấy khóa dòng 2.

  • Giao dịch B đang giữ khóa dòng 2, muốn lấy khóa dòng 1.

Cả hai sẽ bị treo (Freeze). Apache Derby có một tiến trình chạy ngầm (Deadlock Daemon) liên tục theo dõi hệ thống. Khi phát hiện Deadlock, Derby sẽ tự động "giết" (Rollback) một trong hai giao dịch để giải cứu giao dịch còn lại.

Giao dịch bị giết sẽ nhận được ngoại lệ với SQLState 40001. Lập trình viên Java cần bắt lỗi này và viết logic tự động thử lại (Retry) giao dịch.

5. Cấu hình Isolation Level trong Java JDBC

Việc thay đổi cấp độ cô lập phải được thực hiện trên đối tượng Connection trước khi bắt đầu giao dịch (trước khi gọi phương thức thực thi SQL).

Java

try (Connection conn = DriverManager.getConnection("jdbc:derby://localhost:1527/db")) {
    
    // Tắt Auto-commit để vào chế độ quản lý Transaction thủ công
    conn.setAutoCommit(false);
    
    // Nâng cấp độ cô lập lên mức cao nhất để chạy báo cáo tài chính
    conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
    
    try (Statement stmt = conn.createStatement()) {
        // Mọi lệnh SELECT trong này sẽ khóa cứng các bảng liên quan
        ResultSet rs = stmt.executeQuery("SELECT SUM(so_du) FROM tai_khoan");
        
        conn.commit();
    } catch (SQLException e) {
        if ("40001".equals(e.getSQLState())) {
            System.err.println("=> Giao dịch bị hủy do Deadlock. Cần Retry!");
        }
        conn.rollback();
    }
} catch (SQLException e) {
    e.printStackTrace();
}

6. Tổng kết

  • Mức độ cô lập càng cao thì tính toàn vẹn dữ liệu càng lớn, nhưng tốc độ hệ thống càng chậm do hiện tượng chờ khóa (Lock Waiting).

  • Mặc định READ_COMMITTED là đủ dùng cho 95% ứng dụng thực tế. Chỉ đẩy lên SERIALIZABLE cho các nghiệp vụ cực kỳ nhạy cảm như chốt sổ kế toán cuối tháng.

  • Luôn chuẩn bị tinh thần bắt lỗi 40001 (Deadlock) trong môi trường đa kết nối.

Trong thế giới CSDL quan hệ, có những tác vụ dữ liệu cần được chạy tự động ngay lập tức khi một bảng có sự biến động (ví dụ: tự động lưu log khi có người xóa đơn hàng). Bài tiếp theo sẽ giới thiệu công cụ thực hiện điều này: Apache Derby Bài 32: Trigger - Tự động hóa tác vụ cơ sở dữ liệu.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí