Nhóm nhiều câu lệnh thành một đơn vị “được ăn cả, ngã về không”; dùng SAVEPOINT để hủy một phần; và chọn mức cô lập phù hợp.
1. Vì sao cần giao dịch?
Bài toán kinh điển — chuyển tiền:
UPDATE tai_khoan SET so_du = so_du - 1000 WHERE id = 1;-- 💥 mất điện ở đâyUPDATE tai_khoan SET so_du = so_du + 1000 WHERE id = 2;
Nếu hệ thống sập giữa chừng, 1000 đồng biến mất. Giao dịch đảm bảo cả hai lệnh cùng thành công hoặc cả hai cùng không xảy ra.
2. ACID
Chữ cái
Tên
Ý nghĩa
A
Atomicity (Nguyên tử)
Tất cả hoặc không gì cả
C
Consistency (Nhất quán)
CSDL luôn thỏa mãn mọi ràng buộc trước và sau giao dịch
I
Isolation (Cô lập)
Các giao dịch đồng thời không thấy trạng thái dở dang của nhau
D
Durability (Bền vững)
Đã COMMIT là không mất, kể cả khi mất điện
PostgreSQL tuân thủ đầy đủ ACID cho cả DDL lẫn DML — bạn có thể ROLLBACK một CREATE TABLE, điều mà MySQL (với InnoDB) và Oracle không làm được.
3. Cú pháp cơ bản
BEGIN; -- hoặc: START TRANSACTION; cau_lenh_1; cau_lenh_2;COMMIT; -- xác nhận-- hoặcROLLBACK; -- hủy toàn bộ
3.1. Ví dụ hoàn chỉnh
DROP TABLE IF EXISTS accounts;CREATE TABLE accounts ( id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR(100) NOT NULL, balance DEC(15,2) NOT NULL CHECK (balance >= 0));INSERT INTO accounts (name, balance)VALUES ('Alice', 10000), ('Bob', 10000);
Chuyển 1.000 từ Alice sang Bob:
BEGIN;UPDATE accounts SET balance = balance - 1000 WHERE name = 'Alice';UPDATE accounts SET balance = balance + 1000 WHERE name = 'Bob';-- Kiểm tra trước khi xác nhậnSELECT name, balance, sum(balance) OVER () AS tong FROM accounts;COMMIT;
name | balance | tong-------+---------+---------- Alice | 9000.00 | 20000.00 Bob | 11000.00| 20000.00
Tổng không đổi — đúng như mong đợi.
3.2. ROLLBACK — hủy bỏ
BEGIN;UPDATE accounts SET balance = 0;SELECT * FROM accounts; -- thấy 0, nhưng chỉ trong phiên nàyROLLBACK;SELECT * FROM accounts; -- dữ liệu trở lại như cũ
3.3. Ràng buộc chặn giao dịch
BEGIN;UPDATE accounts SET balance = balance - 50000 WHERE name = 'Alice';-- ERROR: new row for relation "accounts" violates check constraint-- "accounts_balance_check"COMMIT;-- ROLLBACK ← Postgres tự động hủy vì giao dịch đã lỗi
BEGIN;INSERT INTO accounts (name, balance) VALUES ('Carol', 5000);SELECT * FROM bang_khong_ton_tai; -- ERRORINSERT INTO accounts (name, balance) VALUES ('Dave', 3000);-- ERROR: current transaction is aborted,-- commands ignored until end of transaction blockCOMMIT; -- thực chất là ROLLBACK
Sau lỗi, mọi lệnh tiếp theo đều bị từ chối cho tới khi bạn ROLLBACK (hoặc ROLLBACK TO SAVEPOINT).
Trong psql, dấu nhắc cho biết trạng thái:
Dấu nhắc
Trạng thái
db=#
Không trong giao dịch
db=*#
Đang trong giao dịch, mọi thứ ổn
db=!#
Giao dịch đã lỗi — chỉ chấp nhận ROLLBACK
5. SAVEPOINT — điểm lưu
Cho phép hủy một phần giao dịch mà không mất toàn bộ.
BEGIN;INSERT INTO accounts (name, balance) VALUES ('Carol', 5000);SAVEPOINT sp1;INSERT INTO accounts (name, balance) VALUES ('Dave', -100);-- ERROR: vi phạm CHECKROLLBACK TO SAVEPOINT sp1; -- ← quay lại điểm lưu, giao dịch còn sốngINSERT INTO accounts (name, balance) VALUES ('Dave', 3000);COMMIT;
Kết quả: cả Carol và Dave đều được thêm; chỉ dòng lỗi bị hủy.
Các lệnh liên quan:
SAVEPOINT ten_diem; -- tạo điểm lưuROLLBACK TO SAVEPOINT ten_diem; -- quay lại (điểm lưu vẫn còn)RELEASE SAVEPOINT ten_diem; -- xóa điểm lưu, giữ nguyên thay đổi
Đừng lạm dụng SAVEPOINT
Mỗi savepoint tạo một subtransaction với XID riêng. Hàng nghìn savepoint trong một giao dịch gây áp lực lớn lên bộ đệm subtransaction và có thể làm chậm toàn bộ máy chủ.
Đây là lý do vì sao lạm dụng try/except quanh từng câu lệnh trong ORM (mỗi khối tạo một savepoint ngầm) là mẫu code nguy hiểm ở quy mô lớn.
5.1. ON_ERROR_ROLLBACK trong psql
\set ON_ERROR_ROLLBACK interactive
psql tự tạo savepoint ngầm trước mỗi lệnh, nên khi bạn gõ sai một câu, giao dịch không bị hỏng. Rất tiện khi làm việc thủ công — nhưng đừng bật trong script tự động.
6. Mức cô lập (Isolation Levels)
6.1. Các hiện tượng bất thường
Hiện tượng
Mô tả
Dirty read
Đọc được dữ liệu chưa COMMIT của giao dịch khác
Non-repeatable read
Đọc cùng một dòng hai lần, giá trị khác nhau
Phantom read
Chạy cùng một truy vấn hai lần, số dòng khác nhau
Serialization anomaly
Kết quả không tương đương với bất kỳ thứ tự tuần tự nào
6.2. Bốn mức trong PostgreSQL
Mức
Dirty read
Non-repeatable
Phantom
Serialization anomaly
READ UNCOMMITTED
Không¹
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ể
SERIALIZABLE
Không
Không
Không
Không
¹ PostgreSQL không thực sự cài đặt READ UNCOMMITTED — nó hành xử như READ COMMITTED.
² Chuẩn SQL cho phép phantom read ở mức này, nhưng PostgreSQL ngăn chặn được nhờ MVCC.
6.3. Đặt mức cô lập
-- Cho một giao dịchBEGIN ISOLATION LEVEL REPEATABLE READ; ...COMMIT;-- Hoặc sau khi BEGINBEGIN;SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; ...COMMIT;-- Mặc định cho toàn phiênSET default_transaction_isolation = 'repeatable read';
6.4. READ COMMITTED — mặc định
Mỗi câu lệnh thấy ảnh chụp dữ liệu tại thời điểm câu lệnh đó bắt đầu.
-- Phiên A -- Phiên BBEGIN;SELECT balance FROM accountsWHERE name = 'Alice'; -- 9000 UPDATE accounts SET balance = 5000 WHERE name = 'Alice'; COMMIT;SELECT balance FROM accountsWHERE name = 'Alice'; -- 5000 ← đã thay đổi!COMMIT;
Phù hợp với hầu hết ứng dụng web: mỗi câu lệnh thấy dữ liệu mới nhất, ít xung đột.
6.5. REPEATABLE READ
Toàn bộ giao dịch thấy cùng một ảnh chụp, lấy tại thời điểm câu lệnh đầu tiên.
-- Phiên A -- Phiên BBEGIN ISOLATION LEVEL REPEATABLE READ;SELECT balance FROM accountsWHERE name = 'Alice'; -- 9000 UPDATE accounts SET balance = 5000 WHERE name = 'Alice'; COMMIT;SELECT balance FROM accountsWHERE name = 'Alice'; -- 9000 ← vẫn giá trị cũCOMMIT;
Phù hợp cho báo cáo cần dữ liệu nhất quán trên nhiều truy vấn.
Nếu cả hai giao dịch cùng sửa một dòng, phiên A sẽ nhận lỗi khi ghi:
ERROR: could not serialize access due to concurrent update
Ứng dụng phải thử lại giao dịch.
6.6. SERIALIZABLE
Đảm bảo kết quả tương đương với việc chạy các giao dịch tuần tự. PostgreSQL cài đặt bằng SSI (Serializable Snapshot Isolation) — không dùng khóa toàn cục nên vẫn nhanh.
BEGIN ISOLATION LEVEL SERIALIZABLE; ...COMMIT;-- Có thể nhận:-- ERROR: could not serialize access due to read/write dependencies-- among transactions-- HINT: The transaction might succeed if retried.
Dùng SERIALIZABLE thì bắt buộc phải có logic thử lại
for lan_thu in range(3): try: with conn.transaction(): thuc_hien_cong_viec() break except SerializationFailure: time.sleep(0.1 * 2 ** lan_thu) # exponential backoff
Không có retry, ứng dụng sẽ báo lỗi ngẫu nhiên cho người dùng khi tải cao.
7. Khóa (Locking)
7.1. SELECT ... FOR UPDATE — khóa bi quan
Khóa các dòng đọc được, ngăn giao dịch khác sửa cho tới khi bạn COMMIT:
BEGIN;SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;-- Giao dịch khác gọi SELECT ... FOR UPDATE trên cùng dòng sẽ phải chờUPDATE accounts SET balance = balance - 1000 WHERE id = 1;COMMIT;
Các biến thể:
Cú pháp
Hành vi
FOR UPDATE
Khóa mạnh nhất, chặn cả đọc-để-ghi
FOR NO KEY UPDATE
Yếu hơn, cho phép thêm khóa ngoại tham chiếu
FOR SHARE
Nhiều giao dịch cùng đọc được, nhưng không ai ghi được
FOR UPDATE NOWAIT
Báo lỗi ngay thay vì chờ
FOR UPDATE SKIP LOCKED
Bỏ qua dòng đang bị khóa
7.2. SKIP LOCKED — hàng đợi công việc
Mẫu này biến bảng thành một hàng đợi tin cậy cho nhiều worker:
BEGIN;SELECT * FROM cong_viecWHERE trang_thai = 'cho_xu_ly'ORDER BY do_uu_tien DESC, idLIMIT 1FOR UPDATE SKIP LOCKED; -- ← worker khác lấy công việc kế tiếpUPDATE cong_viec SET trang_thai = 'dang_xu_ly' WHERE id = ...;COMMIT;
Không có SKIP LOCKED, tất cả worker sẽ xếp hàng chờ cùng một dòng.
7.3. Khóa lạc quan (optimistic locking)
Không dùng khóa CSDL, thay vào đó kiểm tra phiên bản:
UPDATE tai_lieuSET noi_dung = 'nội dung mới', version = version + 1WHERE id = 42 AND version = 7; -- ← chỉ ghi nếu chưa ai sửa-- UPDATE 0 → có người khác đã sửa, cần xử lý xung đột
Phù hợp cho ứng dụng web nơi người dùng mở form rồi lưu sau nhiều phút.
7.4. Deadlock
Xảy ra khi hai giao dịch chờ khóa của nhau:
-- Phiên A -- Phiên BBEGIN; BEGIN;UPDATE accounts SET ... WHERE id=1; UPDATE accounts SET ... WHERE id=2;UPDATE accounts SET ... WHERE id=2; UPDATE accounts SET ... WHERE id=1;-- ⏳ chờ B -- ⏳ chờ A → DEADLOCK
PostgreSQL tự phát hiện sau deadlock_timeout (mặc định 1 giây) và hủy một trong hai giao dịch:
ERROR: deadlock detected
DETAIL: Process 1234 waits for ShareLock on transaction 5678...
Cách phòng tránh: luôn truy cập các dòng theo cùng một thứ tự trong mọi giao dịch:
-- Luôn khóa theo id tăng dầnSELECT * FROM accountsWHERE id IN (1, 2)ORDER BY idFOR UPDATE;
8. Giao dịch và DDL
PostgreSQL cho phép ROLLBACK cả lệnh DDL — cực kỳ hữu ích khi migration:
BEGIN;CREATE TABLE bang_moi (id INT PRIMARY KEY, ten TEXT);ALTER TABLE film ADD COLUMN ghi_chu TEXT;CREATE INDEX idx_test ON film (title);-- Phát hiện có vấn đềROLLBACK;-- Không có gì thay đổi — bảng, cột, index đều biến mất
Nếu bất kỳ bước nào lỗi, toàn bộ được hủy, CSDL không rơi vào trạng thái nửa vời.
Ngoại lệ — các lệnh không chạy được trong giao dịch:
CREATE INDEX CONCURRENTLY
DROP INDEX CONCURRENTLY
VACUUM
CREATE DATABASE, DROP DATABASE
ALTER SYSTEM
CREATE INDEX CONCURRENTLY quan trọng trên production vì nó không khóa bảng khi tạo index — đổi lại là không dùng được trong giao dịch và chậm hơn.
9. Autocommit và giao dịch dài
9.1. Autocommit
Mặc định, mỗi câu lệnh đơn lẻ là một giao dịch:
UPDATE film SET rental_rate = 1;-- ngầm hiểu là: BEGIN; UPDATE ...; COMMIT;
Tắt trong psql:
\set AUTOCOMMIT off
9.2. Giao dịch dài — kẻ thù thầm lặng
Đừng để giao dịch mở quá lâu
Một giao dịch mở hàng giờ (kể cả chỉ đọc) gây ra:
VACUUM không dọn được dead tuple → bảng phình to (table bloat).
Đĩa đầy dần vì WAL không được tái sử dụng.
Nguy cơ transaction ID wraparound — sự cố nghiêm trọng nhất của PostgreSQL.
Kiểm tra giao dịch cũ:
SELECT pid, now() - xact_start AS thoi_gian, state, left(query, 60)FROM pg_stat_activityWHERE xact_start IS NOT NULLORDER BY xact_start;
Đặt giới hạn:
SET idle_in_transaction_session_timeout = '5min';SET statement_timeout = '30s';
10. Bài tập
Tạo bảng accounts như mục 3, thực hiện chuyển tiền trong giao dịch, rồi ROLLBACK và kiểm chứng số dư không đổi.
Thử rút nhiều hơn số dư — quan sát ràng buộc CHECK chặn giao dịch.
Dùng SAVEPOINT để chèn 3 dòng, trong đó dòng thứ 2 bị lỗi nhưng dòng 1 và 3 vẫn được lưu.
Mở hai phiên psql, chứng minh sự khác biệt giữa READ COMMITTED và REPEATABLE READ.
Viết mẫu hàng đợi công việc dùng FOR UPDATE SKIP LOCKED.
Tạo tình huống deadlock có chủ ý giữa hai phiên, rồi sửa lại bằng cách sắp xếp thứ tự khóa.
Chứng minh PostgreSQL cho phép ROLLBACK lệnh CREATE TABLE.
Đáp án
1.
BEGIN;UPDATE accounts SET balance = balance - 1000 WHERE name = 'Alice';UPDATE accounts SET balance = balance + 1000 WHERE name = 'Bob';SELECT * FROM accounts; -- thấy thay đổiROLLBACK;SELECT * FROM accounts; -- trở lại như cũ
2.
BEGIN;UPDATE accounts SET balance = balance - 999999 WHERE name = 'Alice';-- ERROR: violates check constraint "accounts_balance_check"ROLLBACK;
3.
BEGIN;INSERT INTO accounts (name, balance) VALUES ('X', 100);SAVEPOINT sp;INSERT INTO accounts (name, balance) VALUES ('Y', -100); -- lỗiROLLBACK TO SAVEPOINT sp;INSERT INTO accounts (name, balance) VALUES ('Z', 300);COMMIT;SELECT name FROM accounts; -- có X và Z, không có Y
4. Chạy đúng kịch bản ở mục 6.4 và 6.5, mỗi phiên một cửa sổ terminal riêng.
5.
CREATE TABLE cong_viec ( id SERIAL PRIMARY KEY, noi_dung TEXT, trang_thai TEXT NOT NULL DEFAULT 'cho_xu_ly', do_uu_tien INT DEFAULT 0);INSERT INTO cong_viec (noi_dung, do_uu_tien)VALUES ('việc A', 1), ('việc B', 3), ('việc C', 2);-- Mỗi worker chạy:BEGIN;WITH lay AS ( SELECT id FROM cong_viec WHERE trang_thai = 'cho_xu_ly' ORDER BY do_uu_tien DESC, id LIMIT 1 FOR UPDATE SKIP LOCKED)UPDATE cong_viec c SET trang_thai = 'dang_xu_ly'FROM lay WHERE c.id = lay.idRETURNING c.*;COMMIT;
6. Tạo deadlock theo kịch bản mục 7.4. Sửa bằng cách cả hai phiên đều khóa theo id tăng dần:
BEGIN;SELECT * FROM accounts WHERE id IN (1,2) ORDER BY id FOR UPDATE;UPDATE accounts SET balance = balance - 100 WHERE id = 1;UPDATE accounts SET balance = balance + 100 WHERE id = 2;COMMIT;
7.
BEGIN;CREATE TABLE thu_nghiem (id INT);INSERT INTO thu_nghiem VALUES (1);SELECT * FROM thu_nghiem; -- có dữ liệuROLLBACK;SELECT * FROM thu_nghiem; -- ERROR: relation "thu_nghiem" does not exist
11. Tóm tắt
BEGIN / COMMIT / ROLLBACK nhóm nhiều lệnh thành một đơn vị nguyên tử.
Một lệnh lỗi làm cả giao dịch bị hủy — mọi lệnh sau đó bị từ chối cho tới khi ROLLBACK.
SAVEPOINT cho phép hủy một phần; đừng tạo hàng nghìn savepoint.
Mức mặc định READ COMMITTED phù hợp hầu hết ứng dụng; SERIALIZABLE cần logic thử lại.
FOR UPDATE SKIP LOCKED là mẫu chuẩn cho hàng đợi công việc nhiều worker.
Tránh deadlock bằng cách luôn khóa theo cùng thứ tự.
PostgreSQL cho phép ROLLBACK cả DDL — hãy bọc migration trong giao dịch.
Giao dịch mở lâu gây bloat và cản VACUUM — đặt idle_in_transaction_session_timeout.