Cùng nhau xem thử mức cô lập READ COMMITTED làm gì nhé. Tên của nó kiểu như gợi ý rằng tất cả những gì mình đọc trong transaction đều đã được "commit" bởi các transaction khác rồi. Giống như ngoài đời, chỉ tin vào mấy tin đồn đã được ghi vào biên bản chính thức thôi ấy mà.
Nói nghiêm túc thì, mức này đảm bảo rằng transaction không thể thấy những thay đổi mà các transaction khác đã thực hiện nhưng chưa commit. Nó giải quyết vấn đề gọi là "dirty read" (Dirty Read). Tuy nhiên, nhớ là dữ liệu bạn đọc có thể thay đổi nếu một transaction khác kịp commit giữa các truy vấn của bạn. Điều này tạo ra rủi ro "non-repeatable read" (Non-Repeatable Read).
Trong PostgreSQL, mức cô lập READ COMMITTED là mặc định luôn. Kiểu như chế độ default ấy — bạn không cần cài đặt gì đặc biệt để dùng nó cả.
Ví dụ sử dụng mức cô lập READ COMMITTED
Cùng xem thử nó hoạt động thế nào ngoài thực tế nhé. Giả sử mình có bảng accounts, chứa thông tin user và số dư của họ:
CREATE TABLE accounts (
account_id SERIAL PRIMARY KEY,
account_name TEXT NOT NULL,
balance NUMERIC(10, 2) NOT NULL
);
INSERT INTO accounts (account_name, balance)
VALUES ('Alice', 1000.00), ('Bob', 500.00);
Giờ tưởng tượng tình huống sau. Session 1 và Session 2 — hai nhân vật chính của chúng ta, cả hai đều làm việc với bảng accounts. Đây là những gì xảy ra:
Session 1:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_name = 'Alice';
-- Chưa có COMMIT hay ROLLBACK gì cả.
Lúc này, từ balance của Alice tạm thời bị trừ 100 đơn vị, nhưng kết quả vẫn chưa được commit.
Session 2:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT balance FROM accounts WHERE account_name = 'Alice';
Kết quả: Session 2 thấy balance của Alice là 1000.00, vì transaction của Session 1 vẫn chưa xong (COMMIT chưa thực hiện). Mức READ COMMITTED bảo vệ mình khỏi "dirty read".
Session 1 kết thúc transaction:
COMMIT;
Bây giờ balance của Alice đã được cập nhật, và giá trị 900.00 đã được ghi nhận trong database.
Session 2 lặp lại truy vấn:
SELECT balance FROM accounts WHERE account_name = 'Alice';
Kết quả: giờ Session 2 thấy balance mới của Alice — 900.00. Lưu ý là kết quả này khác với truy vấn trước, trả về 1000.00. Đây chính là vấn đề "non-repeatable read".
Khi nào nên dùng READ COMMITTED?
Mức cô lập READ COMMITTED — là sự cân bằng giữa hiệu năng và tính nhất quán. Nhưng có vài trường hợp nó cực kỳ hợp lý:
CRUD đơn giản: khi bạn chỉ đọc hoặc cập nhật dữ liệu mà không có mối liên hệ phức tạp.
Cập nhật record: ví dụ, cập nhật hàng loạt dữ liệu trong bảng, nơi bạn muốn thấy ngay các thay đổi đã commit.
Xử lý transaction: hệ thống thanh toán cho phép user chỉ thấy dữ liệu đã xác nhận thôi.
Nhưng nếu bạn chạy các truy vấn phân tích phức tạp hoặc làm việc với lượng dữ liệu lớn, có thể nên cân nhắc mức cô lập khác, ví dụ REPEATABLE READ.
Ưu điểm và nhược điểm của mức cô lập READ COMMITTED
Mức cô lập READ COMMITTED — kiểu như giải pháp trung hòa ấy. Nó bảo vệ bạn khỏi dữ liệu bẩn: bạn sẽ không thấy thay đổi mà transaction khác mới bắt đầu nhưng chưa xong. Tức là không ai đọc được thông tin "sống dở chết dở" có thể bị rollback bất cứ lúc nào.
Chế độ này chạy nhanh hơn các mức nghiêm ngặt hơn (REPEATABLE READ hoặc SERIALIZABLE), vì không cần lock phức tạp hay kiểm tra thêm. Nó khá nhẹ và vẫn đủ tin cậy — vì vậy nó được dùng mặc định và rất hợp cho hầu hết các tác vụ hàng ngày.
Dù nó ngăn được dirty read, READ COMMITTED không bảo vệ khỏi:
- "Non-repeatable read" (
Non-Repeatable Read): giá trị dữ liệu có thể thay đổi nếu transaction khác sửa giữa các truy vấn của bạn. - "Phantom read" (
Phantom Read): transaction khác có thể thêm dòng mới, ảnh hưởng đến kết quả truy vấn của bạn.
Mẹo khi làm việc với READ COMMITTED
Luôn kết thúc transaction: đừng quên dùng COMMIT hoặc ROLLBACK, nếu không sẽ gặp vấn đề về lock đấy.
Đảm bảo mức cô lập đủ dùng: nếu bạn cần đảm bảo dữ liệu không đổi trong suốt transaction, hãy cân nhắc REPEATABLE READ.
Dùng index: cái này giúp PostgreSQL tìm dữ liệu và cập nhật nhanh hơn nhiều.
Ví dụ: xử lý đơn hàng
Giả sử mình có bảng orders, lưu thông tin về các đơn hàng:
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');
Mình muốn cập nhật trạng thái các đơn hàng đang ở trạng thái "pending":
BEGIN;
SELECT * FROM orders WHERE status = 'pending';
UPDATE orders SET status = 'completed' WHERE status = 'pending';
COMMIT;
Nếu trong quá trình này, transaction khác thêm đơn hàng mới với status "pending" và thực hiện COMMIT, transaction của mình sẽ không tính đến dòng đó, vì nó được thêm sau khi mình bắt đầu đọc.
Đây là ví dụ về "phantom read". Nếu muốn tránh trường hợp này, bạn cần dùng SERIALIZABLE.
Mức cô lập READ COMMITTED — là lựa chọn mặc định cho hầu hết database, bao gồm cả PostgreSQL. Nó bảo vệ khỏi dirty read, nên rất hợp cho đa số thao tác tiêu chuẩn. Tuy nhiên, trong các trường hợp cần tính nhất quán nghiêm ngặt, có thể bạn sẽ cần mức cô lập cao hơn. Việc chọn mức cô lập nên dựa vào yêu cầu cụ thể và hiệu năng bạn cần nhé.
GO TO FULL VERSION