SERIALIZABLE — đây là mức cô lập giao dịch cao nhất trong PostgreSQL. Mức này đảm bảo rằng kết quả của các giao dịch song song sẽ giống hệt như khi chúng được thực hiện TUẦN TỰ, từng cái một. Không có bất kỳ bất thường nào của thực thi song song (ví dụ như Dirty Read, Non-Repeatable Read, Phantom Read) có thể xảy ra.
Nói đơn giản, SERIALIZABLE đảm bảo trật tự tuyệt đối và tính nhất quán giữa các giao dịch song song. Nó giống như PostgreSQL nói: "Tất cả giao dịch — xếp hàng nhé các bạn!"
Tại sao cần mức SERIALIZABLE? Đôi khi bạn muốn chắc chắn 100% rằng dữ liệu của mình luôn nhất quán, bất chấp các thay đổi song song. Hãy tưởng tượng cảnh trong siêu thị, nơi các thu ngân phục vụ khách cùng lúc. Nếu không ai kiểm soát thứ tự, có thể số hàng hóa ra khỏi cửa hàng sẽ nhiều hơn số đã mua. Với SERIALIZABLE thì chuyện đó không thể xảy ra.
Ví dụ thiết lập mức SERIALIZABLE
Để đặt mức cô lập SERIALIZABLE, bạn dùng lệnh:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Ví dụ, tạo một giao dịch sử dụng mức này:
BEGIN; -- Bắt đầu giao dịch
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- Đặt mức cô lập
SELECT * FROM products WHERE category = 'Electronics'; -- Lấy danh sách sản phẩm
UPDATE products SET stock = stock - 1 WHERE product_id = 123; -- Cập nhật tồn kho
COMMIT; -- Xác nhận thay đổi
Case: đặt vé rạp chiếu phim
Cùng xem một ví dụ thực tế nơi mức SERIALIZABLE là không thể thiếu. Giả sử bạn đang phát triển hệ thống đặt vé rạp chiếu phim online. Người dùng chọn chỗ ngồi, và bạn muốn đảm bảo rằng không có hai khách cùng mua một chỗ cùng lúc.
Đầu tiên tạo bảng chỗ ngồi:
CREATE TABLE seats (
seat_id SERIAL PRIMARY KEY,
is_booked BOOLEAN DEFAULT FALSE
);
Bây giờ thêm vài chỗ ngồi:
INSERT INTO seats (is_booked) VALUES (FALSE), (FALSE), (FALSE);
Ví dụ về giao dịch với SERIALIZABLE.
Đây là cách đặt chỗ an toàn:
BEGIN; -- Bắt đầu giao dịch
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- Mức cô lập SERIALIZABLE
-- Kiểm tra chỗ còn trống không
SELECT is_booked FROM seats WHERE seat_id = 1;
-- Đặt chỗ
UPDATE seats SET is_booked = TRUE WHERE seat_id = 1;
COMMIT; -- Xác nhận đặt chỗ
Nếu có giao dịch song song thứ hai cố đặt cùng chỗ đó, PostgreSQL sẽ không để xảy ra lộn xộn và sẽ báo lỗi conflict serialize.
Ngăn chặn Phantom Read
Giờ cùng tìm hiểu về "phantom read" mà chúng ta muốn tránh. Phantom Read xảy ra khi một giao dịch nhìn thấy dữ liệu được thêm bởi giao dịch khác trong quá trình nó đang chạy. Ví dụ, giao dịch của bạn mong đợi một số dòng nhất định, nhưng bất ngờ giao dịch khác thêm hoặc xóa dòng, làm thay đổi kết quả.
Xem ví dụ:
Dữ liệu trước khi bắt đầu giao dịch
| id | balance | user |
|---|---|---|
| 1 | 1000 | Alice |
| 2 | 500 | Bob |
Giao dịch 1
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- Đếm số user có balance lớn hơn 400
SELECT COUNT(*) FROM accounts WHERE balance > 400;
-- Mong đợi kết quả: 2 (Alice và Bob)
Giao dịch 2
Ở session khác, một giao dịch song song được thực hiện:
BEGIN;
INSERT INTO accounts (id, balance, user) VALUES (3, 700, 'Charlie');
COMMIT;
Quay lại Giao dịch 1
-- Lặp lại truy vấn
SELECT COUNT(*) FROM accounts WHERE balance > 400;
Bây giờ, nếu không dùng SERIALIZABLE, kết quả sẽ là 3 thay vì 2, vì Charlie được thêm vào trong lúc Giao dịch 1 đang chạy. Đó chính là Phantom Read.
Nhưng với SERIALIZABLE, PostgreSQL đảm bảo Giao dịch 1 sẽ không thấy Charlie, vì "thế giới" của nó đã bị đóng băng tại thời điểm bắt đầu giao dịch.
Đặc điểm và hạn chế của mức SERIALIZABLE
Chúng ta đã hiểu SERIALIZABLE giúp đạt được cô lập lý tưởng như thế nào. Nhưng trên đời này có gì hoàn hảo mà không có nhược điểm đâu? Nói thật nhé.
Giảm hiệu năng
SERIALIZABLE tốn nhiều tài nguyên hơn hẳn so với các mức READ COMMITTED hay REPEATABLE READ. Vì sao? PostgreSQL phải giả lập thực thi tuần tự, theo dõi mọi xung đột có thể giữa các giao dịch.
Lỗi serialize
Nếu PostgreSQL phát hiện không thể thực hiện giao dịch theo "thứ tự lý tưởng", nó sẽ báo lỗi serialize (serialization_failure) và rollback giao dịch.
Ví dụ lỗi:
ERROR: could not serialize access due to concurrent update
Để xử lý tình huống này, ta có thể chạy lại giao dịch sau khi thất bại:
DO $$
DECLARE
done BOOLEAN := FALSE;
BEGIN
WHILE NOT done LOOP
BEGIN
-- Bắt đầu giao dịch
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- Thực hiện thao tác
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- Xác nhận thay đổi
COMMIT;
done := TRUE; -- Thoát vòng lặp nếu thành công
EXCEPTION WHEN serialization_failure THEN
ROLLBACK; -- Rollback khi lỗi
END;
END LOOP;
END;
$$;
Đây là cách làm quen thuộc trong các hệ thống dùng SERIALIZABLE.
Đoạn code này được viết bằng PL-SQL. Mình sẽ quay lại với nó sau nhé. Chỉ muốn cho bạn xem code đẹp và chạy được thôi. Và cũng để bạn thấy tại sao cần PL-SQL :)
Khi nào nên dùng SERIALIZABLE?
Dùng mức cô lập này khi cái giá của sai sót là rất lớn:
- Giao dịch tài chính, như xử lý thanh toán hoặc chia thưởng.
- Hệ thống quản lý kho, để tránh trùng lặp đơn hàng.
- Đặt chỗ online, nơi cần loại trừ xung đột khi đặt tài nguyên.
Nếu bạn xây dựng hệ thống mà dữ liệu phải nhất quán 100%, còn hiệu năng không quá quan trọng, SERIALIZABLE sẽ là bạn thân nhất của bạn.
GO TO FULL VERSION