CodeGym /Các khóa học /SQL SELF /Cấp độ cách ly REPEATABLE READ: ngăn chặn Non-Repeatable ...

Cấp độ cách ly REPEATABLE READ: ngăn chặn Non-Repeatable Read

SQL SELF
Mức độ, Bài học
Có sẵn

Hãy tưởng tượng bạn đang chơi một game online, và trong lúc đó có thằng cheat nào đó chọc ngoáy vào code game để buff nhân vật của nó, hoặc bạn đang đọc sách trong thư viện mà ai đó lại thay đổi trang, chèn thêm chương mới hoặc thậm chí tráo cả cuốn sách. Khá là khó chịu đúng không? Đó chính là những "bất ngờ" mà cấp độ cách ly REPEATABLE READ bảo vệ bạn khỏi gặp phải.

REPEATABLE READ đảm bảo rằng dữ liệu bạn nhìn thấy trong một giao dịch sẽ không thay đổi cho đến khi giao dịch đó kết thúc. Dù có giao dịch khác cố gắng cập nhật dữ liệu đó, giao dịch của bạn vẫn được bảo vệ khỏi những thay đổi đó.

Những điểm chính:

  • Ngăn chặn Dirty Read (đọc dữ liệu chưa được xác nhận).
  • Và quan trọng nhất, ngăn chặn Non-Repeatable Read. Nghĩa là nếu bạn đã đọc một tập dữ liệu ở đầu giao dịch, thì khi đọc lại bạn sẽ nhận được đúng dữ liệu đó, kể cả khi người khác đã sửa nó.

Tuy nhiên, REPEATABLE READ không bảo vệ khỏi Phantom Read. Nếu có giao dịch khác thêm dòng mới, chúng có thể xuất hiện trong truy vấn lặp lại của bạn. Để xử lý luôn cả trường hợp này, bạn sẽ cần đến cấp độ SERIALIZABLE, nhưng cái này mình sẽ nói sau nhé.

Cách thiết lập cấp độ cách ly REPEATABLE READ

Trước khi vào ví dụ, cùng xem cách bật cấp độ cách ly này trong PostgreSQL. Có hai cách chính:

  1. Thiết lập cấp độ cách ly cho một giao dịch cụ thể:

    SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
    BEGIN;
    -- Các truy vấn của bạn
    COMMIT;
    
  2. Thiết lập cấp độ cách ly cho toàn bộ session hiện tại:

    SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;
    

Ở cách thứ hai, tất cả các giao dịch trong session hiện tại sẽ dùng REPEATABLE READ.

Ví dụ: ngăn chặn Non-Repeatable Read

Giả sử ta có bảng accounts với cấu trúc như sau:

CREATE TABLE orders (
    order_id SERIAL PRIMARY KEY,
    customer_name TEXT NOT NULL,
    status TEXT NOT NULL DEFAULT 'pending'
);

INSERT INTO orders (customer_name, status)
VALUES ('Alice', 'pending'), ('Bob', 'pending');

Bắt đầu với kịch bản cơ bản, khi một giao dịch thay đổi dữ liệu còn giao dịch khác thì đọc.

Kịch bản không dùng REPEATABLE READ (cấp độ READ COMMITTED)

Giao dịch 1 bắt đầu:

BEGIN;
SELECT balance FROM accounts WHERE account_id = 1;
-- Nhận được: 100

Trong lúc đó Giao dịch 2 thay đổi dữ liệu:

BEGIN;
UPDATE accounts SET balance = 150 WHERE account_id = 1;
COMMIT;

Giao dịch 1 tiếp tục:

SELECT balance FROM accounts WHERE account_id = 1;
-- Nhận được: 150 (dữ liệu đã thay đổi!)
COMMIT;

Như bạn thấy, ở cấp độ READ COMMITTED dữ liệu có thể thay đổi giữa hai lần đọc trong cùng một giao dịch. Đây chính là Non-Repeatable Read.

Kịch bản với REPEATABLE READ

Giờ thử lại ví dụ trên nhưng dùng cấp độ cách ly REPEATABLE READ.

Giao dịch 1:

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM accounts WHERE account_id = 1;
-- Nhận được: 100

Giao dịch 2:

BEGIN;
UPDATE accounts SET balance = 150 WHERE account_id = 1;
COMMIT;

Giao dịch 1 tiếp tục làm việc:

SELECT balance FROM accounts WHERE account_id = 1;
-- Vẫn nhận được: 100 (dữ liệu không đổi!)
COMMIT;

Bất kể giao dịch khác có thay đổi gì, giao dịch 1 vẫn nhìn thấy dữ liệu như lúc bắt đầu. Như vậy, Non-Repeatable Read đã bị ngăn chặn.

REPEATABLE READ hoạt động như thế nào

PostgreSQL dùng cơ chế MVCC (Multi-Version Concurrency Control) để hiện thực cấp độ cách ly REPEATABLE READ. Nguyên lý chính của MVCC là mỗi giao dịch nhận được một "snapshot" ổn định của database, snapshot này không thay đổi cho đến khi giao dịch kết thúc. Điều này đạt được nhờ việc tạo và quản lý nhiều phiên bản của các dòng dữ liệu.

Khi giao dịch bắt đầu, nó nhìn thấy dữ liệu ở trạng thái tại thời điểm bắt đầu. Nếu có giao dịch khác sửa đổi, PostgreSQL sẽ tạo một phiên bản mới của dòng, còn phiên bản cũ vẫn giữ cho các giao dịch đang dùng nó.

Chính vì vậy mà giao dịch chạy chậm hơn và tốn nhiều RAM hơn. Và cũng vì thế mà ít ai dùng cấp độ cách ly mạnh nhất: nó an toàn nhất nhưng cũng làm database chậm nhất.

Hạn chế của REPEATABLE READ: Phantom Read

Như đã nói, REPEATABLE READ không bảo vệ khỏi Phantom Read. Để hiểu rõ hơn, cùng xem ví dụ với các truy vấn làm việc trên dải dữ liệu.

Giả sử ta có bảng orders:

CREATE TABLE orders (
    order_id SERIAL PRIMARY KEY,
    amount NUMERIC NOT NULL
);

INSERT INTO orders (amount)
VALUES (50), (100), (150);

Giao dịch 1:

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT COUNT(*) FROM orders WHERE amount > 50;
-- Nhận được: 2

Giao dịch 2:

BEGIN;
INSERT INTO orders (amount) VALUES (200);
COMMIT;

Giao dịch 1 tiếp tục làm việc:

SELECT COUNT(*) FROM orders WHERE amount > 50;
-- Nhận được: 3 (dòng mới xuất hiện trong kết quả truy vấn!)
COMMIT;

Trong trường hợp này, dòng mới (amount = 200) được thêm bởi giao dịch khác, và nó "xuất hiện như ma" trong kết quả truy vấn của giao dịch 1, dù đang ở cấp độ cách ly REPEATABLE READ.

Nếu bạn cần tránh Phantom Read, bạn sẽ phải dùng cấp độ SERIALIZABLE, nhưng điều này luôn phải đánh đổi với hiệu năng.

Ưu điểm và nhược điểm của REPEATABLE READ

Cấp độ cách ly REPEATABLE READ — rất hợp lý khi bạn cần chắc chắn dữ liệu không đổi trong suốt giao dịch. Một khi bạn đã đọc, giá trị đó sẽ giữ nguyên cho đến COMMIT, kể cả khi ai đó ở giao dịch khác cố sửa nó.

Cách này ngăn chặn cả dirty read lẫn non-repeatable read. Bạn luôn làm việc với đúng dữ liệu như lúc đầu — không có chuyện bất ngờ update "giữa chừng". Điều này đặc biệt hữu ích khi bạn xử lý báo cáo hoặc ra quyết định mà cần sự nhất quán.

Nhưng, REPEATABLE READ lại không xử lý được "phantom" (phantom read) — khi dòng mới xuất hiện trong kết quả truy vấn bạn đã chạy trong cùng giao dịch. Ngoài ra, khi tải cao, cấp độ này có thể gây xung đột giữa các giao dịch, nhất là khi nhiều giao dịch cùng truy cập dữ liệu giống nhau. Điều này có thể dẫn đến lock và rollback, dù truy vấn của bạn hoàn toàn đúng.

Tóm lại, REPEATABLE READ — là sự cân bằng tốt giữa độ tin cậy và hiệu năng, nhưng trong môi trường cạnh tranh cao thì cần chú ý và cấu hình thêm.

Mẹo hữu ích và lỗi thường gặp

  • Nhớ rằng chọn cấp độ cách ly ảnh hưởng đến hiệu năng. Chỉ dùng REPEATABLE READ khi thực sự cần đảm bảo dữ liệu không đổi.
  • Dễ nhầm giữa REPEATABLE READ và SERIALIZABLE. Nếu bạn thấy dòng mới xuất hiện khi truy vấn lại, đó là hành vi bình thường của REPEATABLE READ.
  • Khi làm việc với giao dịch dài, cẩn thận với xung đột lock. Giao dịch kéo dài có thể block các thao tác khác.

PostgreSQL cung cấp nhiều công cụ để quản lý cách ly giao dịch. Cấp độ REPEATABLE READ rất phù hợp khi bạn cần chắc chắn dữ liệu đã đọc sẽ không đổi trong cùng một giao dịch.

2
Nhiệm vụ
SQL SELF, mức độ, bài học
Đã khóa
Ngăn chặn Non-Repeatable Read ở mức `REPEATABLE READ`
Ngăn chặn Non-Repeatable Read ở mức `REPEATABLE READ`
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION