0

PostgreSQL Bài 30: Bốn cấp độ cô lập giao dịch (Transaction Isolation Levels): Read Committed, Repeatable Read, Serializable

Trong tính chất ACID của cơ sở dữ liệu quan hệ, chữ I (Isolation - Tính cô lập) là thành phần phức tạp nhất. Nó quy định mức độ một giao dịch (Transaction) có thể nhìn thấy hoặc bị ảnh hưởng bởi những thay đổi diễn ra đồng thời từ các giao dịch khác.

Theo chuẩn ANSI SQL-92, có 4 cấp độ cô lập giao dịch. Tuy nhiên, nhờ nền tảng kiến trúc MVCC đã học ở Bài 29, PostgreSQL triển khai các cấp độ này hoàn toàn khác biệt và an toàn hơn đáng kể so với các hệ thống RDBMS truyền thống dùng khóa dòng (Lock-based RDBMS).

Bài học này sẽ mổ xẻ cơ chế điều phối Snapshot qua từng cấp độ, các hiện tượng bất thường (Anomalies) và thuật toán SSI (Serializable Snapshot Isolation).

1. Bốn hiện tượng bất thường (Read & Write Anomalies)

Trước khi đi vào các cấp độ, bạn cần hiểu rõ 4 hiện tượng xung đột dữ liệu kinh điển:

  1. Dirty Read (Đọc bẩn): Transaction A đọc được dữ liệu mà Transaction B vừa sửa đổi nhưng chưa hề COMMIT. Nếu Transaction B sau đó ROLLBACK, dữ liệu A vừa đọc là dữ liệu "ma", chưa từng tồn tại trên thực tế.

  2. Non-repeatable Read (Đọc không lặp lại / Fuzzy Read): Transaction A đọc một dòng dữ liệu. Transaction B sửa đổi (UPDATE) hoặc xóa (DELETE) dòng đó và COMMIT. Transaction A đọc lại dòng đó một lần nữa thì thấy dữ liệu đã bị thay đổi hoặc biến mất.

  3. Phantom Read (Bóng ma): Transaction A đọc một tập hợp các dòng thỏa mãn điều kiện WHERE (ví dụ: WHERE age > 18 ra 10 dòng). Transaction B chèn thêm (INSERT) một dòng mới thỏa mãn điều kiện đó và COMMIT. Transaction A chạy lại câu lệnh cũ thì thấy xuất hiện thêm dòng mới (11 dòng).

  4. Serialization Anomaly (Bất thường tuần tự hóa / Write Skew): Kết quả thực thi song song của một nhóm transaction không tương đương với bất kỳ thứ tự thực thi tuần tự (từng transaction một) nào của chúng, dẫn đến vi phạm ràng buộc nghiệp vụ tổng thể.

2. Ma trận cấp độ cô lập: Chuẩn ANSI SQL vs Hiện thực trong PostgreSQL

Theo chuẩn SQL-92, bảng phân loại được định nghĩa như sau:

Cấp độ cô lập (ANSI SQL-92) Dirty Read Non-repeatable Read Phantom Read
Read Uncommitted Có thể xảy ra Có thể xảy ra Có thể xảy ra
Read Committed Không Có thể xảy ra Có thể xảy ra
Repeatable Read Không Không Có thể xảy ra
Serializable Không Không Không

Sự khác biệt cốt tử trong PostgreSQL:

Quy tắc đặc thù của PostgreSQL:

  1. Trong PostgreSQL, Read Uncommitted được đối xử tương đương hệt như Read Committed. Do bản chất MVCC luôn yêu cầu kiểm tra cờ commit của Transaction (xmin), PostgreSQL không bao giờ cho phép Dirty Read xảy ra, ngay cả khi bạn cấu hình rõ ràng SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED.

  2. Ở cấp độ Repeatable Read, PostgreSQL ngăn chặn hoàn toàn cả Phantom Read! Đây là điểm vượt trội nhờ cơ chế chụp Snapshot một lần duy nhất.

Bảng hiện thực thực tế trong PostgreSQL:

Cấp độ cấu hình trong PostgreSQL Dirty Read Non-repeatable Read Phantom Read Serialization Anomaly
Read Uncommitted Không (chuyển về Read Committed) Có thể Có thể Có thể
Read Committed (Mặc định) Không Có thể Có thể Có thể
Repeatable Read Không Không Không Có thể (Write Skew)
Serializable Không Không Không Không (Ngăn chặn 100%)

3. Cấp độ 1: Read Committed (Mặc định trong PostgreSQL)

Đây là cấp độ mặc định của mọi session trong PostgreSQL.

3.1. Cơ chế Snapshot: Chụp mới trên mỗi câu lệnh (Statement-level Snapshot)

Mỗi khi bạn thực thi một câu lệnh SQL (SELECT, UPDATE, DELETE) bên trong transaction, PostgreSQL sẽ chụp một Snapshot hoàn toàn mới ngay tại thời điểm câu lệnh đó bắt đầu chạy.

TRANSACTION READ COMMITTED:
BEGIN;
  ├── Statement 1: SELECT ... ──► [Chụp Snapshot 1] (Chỉ thấy dữ liệu đã commit trước mốc 1)
  │   (Trong lúc này, Transaction B commit dữ liệu mới)
  └── Statement 2: SELECT ... ──► [Chụp Snapshot 2] (Thấy ngay dữ liệu mà B vừa commit!)
COMMIT;

3.2. Thực nghiệm quan sát Non-repeatable Read

Mở 2 terminal psql song song:

SQL

-- Chuẩn bị dữ liệu
CREATE TABLE accounts (id INT PRIMARY KEY, balance INT);
INSERT INTO accounts VALUES (1, 100);
Thời điểm Session 1 (Read Committed) Session 2 (Đồng thời)
T1T_1 BEGIN;
T2T_2 SELECT balance FROM accounts WHERE id = 1;
(Kết quả: 100)
T3T_3 BEGIN;

UPDATE accounts SET balance = 500 WHERE id = 1;

COMMIT;
T4T_4 SELECT balance FROM accounts WHERE id = 1;

(Kết quả: 500 - Dữ liệu đã đổi!)
T5T_5 COMMIT;

Nhận xét: Trong cùng 1 transaction, hai lần chạy cùng một câu lệnh SELECT trả về hai kết quả khác nhau. Điều này hoàn toàn hợp lệ ở cấp độ Read Committed.

4. Cấp độ 2: Repeatable Read (Nhất quán toàn bộ giao dịch)

4.1. Cơ chế Snapshot: Chụp một lần duy nhất (Transaction-level Snapshot)

Ở cấp độ này, PostgreSQL chỉ chụp đúng một Snapshot duy nhất tại thời điểm câu lệnh truy vấn đầu tiên trong transaction được thực thi. Toàn bộ các câu lệnh tiếp theo sau đó đều nhìn vào bức ảnh tĩnh này cho đến khi kết thúc transaction.

TRANSACTION REPEATABLE READ:
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
  ├── Statement 1: SELECT ... ──► [CHỤP SNAPSHOT DUY NHẤT]
  │   (Dù hàng trăm transaction khác commit phía sau)
  └── Statement 2: SELECT ... ──► [Vẫn dùng Snapshot ban đầu] (Dữ liệu không đổi!)
COMMIT;

4.2. Xung đột ghi (First-Committer-Wins / Serialization Failure)

Nếu hai transaction ở mức Repeatable Read cùng cố gắng cập nhật (UPDATE hoặc DELETE) trên cùng một dòng dữ liệu:

  • Transaction nào COMMIT trước sẽ thành công.

  • Transaction còn lại khi đến lượt chạy sẽ bị hủy ngang và ném lỗi Serialization Failure:

Plaintext

ERROR: could not serialize access due to concurrent update

Cách xử lý phía Backend (Go/Java/Node/PHP): Khi làm việc với Repeatable Read hoặc Serializable, ứng dụng bắt buộc phải thiết lập cơ chế Retry tự động (thử lại transaction từ 2 đến 3 lần) khi bắt được mã lỗi SQLSTATE 40001 (serialization_failure).

5. Hiện tượng Write Skew: Giới hạn của Repeatable Read

Nhiều kỹ sư cho rằng Repeatable Read đã giải quyết được cả Dirty Read, Non-repeatable Read và Phantom Read thì đã đủ an toàn 100%.

Điều này hoàn toàn sai. Có những ràng buộc nghiệp vụ tổng thể vẫn bị phá vỡ dưới Repeatable Read do hiện tượng Write Skew (Lệch ghi).

Bài toán trực đêm của Bác sĩ (The On-Call Doctor Problem):

  • Quy tắc bệnh viện: Luôn luôn phải có ít nhất 1 bác sĩ trực đêm trong ca trực.

  • Hiện tại có 2 bác sĩ đang trực: Alice và Bob. Cả hai cùng muốn xin nghỉ trực cùng một lúc.

SQL

CREATE TABLE doctors (
    id INT PRIMARY KEY,
    name TEXT,
    on_duty BOOLEAN NOT NULL
);
INSERT INTO doctors VALUES (1, 'Alice', TRUE), (2, 'Bob', TRUE);
Thời điểm Session Bác sĩ Alice (Repeatable Read) Session Bác sĩ Bob (Repeatable Read)
T1T_1 BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T2T_2 SELECT COUNT(*) FROM doctors WHERE on_duty = TRUE;

(Ra 2 bác sĩ: Đủ điều kiện nghỉ!)
T3T_3 SELECT COUNT(*) FROM doctors WHERE on_duty = TRUE;

(Ra 2 bác sĩ: Đủ điều kiện nghỉ!)
T4T_4 UPDATE doctors SET on_duty = FALSE WHERE name = 'Alice';
T5T_5 UPDATE doctors SET on_duty = FALSE WHERE name = 'Bob';
T6T_6 COMMIT; (Thành công)
T7T_7 COMMIT; (Thành công! Không hề bị conflict!)

Hậu quả:

Kiểm tra bảng doctors:

SQL

SELECT * FROM doctors;
-- Alice: on_duty = FALSE
-- Bob:   on_duty = FALSE

Cả hai bác sĩ đều đã nghỉ trực thành công, bệnh viện không còn ai trực!

Tại sao Repeatable Read lại để lọt lỗi này?

Bởi vì Alice sửa dòng của Alice (id = 1), Bob sửa dòng của Bob (id = 2). Hai transaction sửa hai dòng hoàn toàn khác nhau nên không kích hoạt xung đột dòng (concurrent update). Nhưng logic quyết định của cả hai lại phụ thuộc vào tổng số dòng thỏa mãn điều kiện (COUNT(*) >= 2).

6. Cấp độ 3: Serializable và Thuật toán SSI (Serializable Snapshot Isolation)

Để ngăn chặn 100% mọi bất thường dữ liệu (bao gồm cả Write Skew) mà không cần lập trình viên phải tự viết các câu lệnh khóa thủ công LOCK TABLE hay SELECT ... FOR UPDATE, PostgreSQL cung cấp cấp độ tối thượng: Serializable.

6.1. Cuộc cách mạng SSI từ PostgreSQL 9.1

Trong các database cổ điển, Serializable được triển khai bằng S2PL (Strict Two-Phase Locking): khóa cả bảng hoặc khóa các khoảng trống (Predicate Locks / Gap Locks), khiến hiệu năng đọc-ghi song song tụt dốc thảm hại.

PostgreSQL là hệ thống đầu tiên trên thế giới thương mại hóa thuật toán SSI (Serializable Snapshot Isolation):

  • Vẫn cho phép đọc và ghi diễn ra đồng thời hoàn toàn dựa trên MVCC (không khóa vật lý!).

  • Thay vì khóa bảng, PostgreSQL sử dụng các con trỏ nhẹ trong bộ nhớ RAM gọi là SIREAD Locks (Predicate Locks) để theo dõi các phụ thuộc đọc-ghi (Read-Write Dependencies).

  • Nếu engine phát hiện một chu trình phụ thuộc nguy hiểm (rw-antidependencies cycle) có nguy cơ dẫn đến Write Skew, engine sẽ chủ động hủy (Abort) một trong các transaction tham gia và ném lỗi:

    Plaintext

    ERROR: could not serialize access due to read/write dependencies among transactions
    DETAIL: Reason code = Canceled on identification as a pivot, ...
    
    

6.2. Chạy lại bài toán Bác sĩ ở mức Serializable:

Nếu hai bác sĩ thực hiện quy trình trên ở cấp độ SERIALIZABLE:

  • Alice commit thành công ở T6T_6.

  • Ngay tại T7T_7, khi Bob gõ COMMIT;, PostgreSQL sẽ phát hiện xung đột và hủy ngay lập tức giao dịch của Bob. Bác sĩ Bob buộc phải ở lại trực, ràng buộc nghiệp vụ được bảo vệ tuyệt đối.

7. Bảng so sánh tổng kết và Lựa chọn thực chiến

Cấp độ Ưu điểm Nhược điểm / Rủi ro Kịch bản sử dụng thực tế
Read Committed (Mặc định) Hiệu năng cao nhất, không lo bị abort do serialization failure. Dữ liệu có thể bị thay đổi giữa 2 câu SELECT trong cùng transaction. Hơn 95% tác vụ web thông thường, CRUD, đọc ghi đơn lẻ.
Repeatable Read Đảm bảo dữ liệu không đổi suốt transaction, đọc dữ liệu nhất quán 100%. Phải viết mã nguồn xử lý retry khi bị concurrent update conflict. Xuất báo cáo tài chính dài, tác vụ đối soát sao kê cần chụp ảnh toàn hệ thống tại 1 mốc.
Serializable An toàn toán học tuyệt đối, ngăn chặn mọi lỗi logic đồng thời phức tạp. Tốn thêm RAM theo dõi SIREAD lock, tỷ lệ abort cao nếu traffic ghi xung đột lớn. Nghiệp vụ tài chính ngân hàng, trừ tiền ví điện tử, phân bổ vé máy bay/ghế ngồi nhạy cảm.

8. Tóm tắt & Bài tiếp theo

  • PostgreSQL không có Dirty Read; Read Uncommitted tự động chuyển thành Read Committed.

  • Read Committed chụp Snapshot mới cho từng câu lệnh SQL riêng lẻ.

  • Repeatable Read chỉ chụp Snapshot 1 lần ở đầu transaction; bảo vệ khỏi cả Non-repeatable Read và Phantom Read.

  • Repeatable Read vẫn có thể bị phá vỡ bởi hiện tượng Write Skew.

  • Serializable với công nghệ SSI (Serializable Snapshot Isolation) bảo vệ toàn vẹn tuyệt đối mọi logic kinh doanh mà không cần khóa chặn I/O, nhưng ứng dụng backend bắt buộc phải có cơ chế Transaction Retry.

Bài 31 xem tiếp: Hệ thống khóa chuyên sâu: Table Locks, Row Locks, Advisory Locks — chúng ta sẽ tìm hiểu chi tiết các loại khóa vật lý trong PostgreSQL: 8 cấp độ khóa bảng (AccessShare đến AccessExclusive), 4 cấp độ khóa dòng (FOR UPDATE, FOR SHARE), và cách dùng Advisory Locks để tạo khóa phân tán (Distributed Lock) điều phối ứng dụng trực tiếp bằng database.


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í