Giao dịch (Transactions)

← Sửa đổi dữ liệu - INSERT, UPDATE, DELETE, UPSERT | → Nhập và xuất dữ liệu CSV

Bạn sẽ học được

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 ở đây
UPDATE 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áiTênÝ nghĩa
AAtomicity (Nguyên tử)Tất cả hoặc không gì cả
CConsistency (Nhất quán)CSDL luôn thỏa mãn mọi ràng buộc trước và sau giao dịch
IIsolation (Cô lập)Các giao dịch đồng thời không thấy trạng thái dở dang của nhau
DDurability (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ặc
ROLLBACK;                       -- 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ận
SELECT 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ày
ROLLBACK;
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

→ Ràng buộc - PRIMARY KEY, FOREIGN KEY, CHECK, UNIQUE, NOT NULL, DEFAULT


4. Trạng thái lỗi trong giao dịch

Một lệnh lỗi làm cả giao dịch không dùng được

BEGIN;
INSERT INTO accounts (name, balance) VALUES ('Carol', 5000);
SELECT * FROM bang_khong_ton_tai;   -- ERROR
INSERT INTO accounts (name, balance) VALUES ('Dave', 3000);
-- ERROR: current transaction is aborted,
--        commands ignored until end of transaction block
COMMIT;   -- 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ắcTrạ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 CHECK
 
ROLLBACK TO SAVEPOINT sp1;    -- ← quay lại điểm lưu, giao dịch còn sống
 
INSERT 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ưu
ROLLBACK 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ượngMô 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 readChạy cùng một truy vấn hai lần, số dòng khác nhau
Serialization anomalyKế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ứcDirty readNon-repeatablePhantomSerialization anomaly
READ UNCOMMITTEDKhông¹Có thểCó thểCó thể
READ COMMITTED ← mặc địnhKhôngCó thểCó thểCó thể
REPEATABLE READKhôngKhôngKhông²Có thể
SERIALIZABLEKhôngKhôngKhôngKhô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ịch
BEGIN ISOLATION LEVEL REPEATABLE READ;
   ...
COMMIT;
 
-- Hoặc sau khi BEGIN
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
   ...
COMMIT;
 
-- Mặc định cho toàn phiên
SET 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 B
BEGIN;
SELECT balance FROM accounts
WHERE name = 'Alice';   -- 9000
                                    UPDATE accounts SET balance = 5000
                                    WHERE name = 'Alice';
                                    COMMIT;
SELECT balance FROM accounts
WHERE 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 B
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts
WHERE name = 'Alice';   -- 9000
                                    UPDATE accounts SET balance = 5000
                                    WHERE name = 'Alice';
                                    COMMIT;
SELECT balance FROM accounts
WHERE 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ápHành vi
FOR UPDATEKhóa mạnh nhất, chặn cả đọc-để-ghi
FOR NO KEY UPDATEYếu hơn, cho phép thêm khóa ngoại tham chiếu
FOR SHARENhiều giao dịch cùng đọc được, nhưng không ai ghi được
FOR UPDATE NOWAITBáo lỗi ngay thay vì chờ
FOR UPDATE SKIP LOCKEDBỏ 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_viec
WHERE trang_thai = 'cho_xu_ly'
ORDER BY do_uu_tien DESC, id
LIMIT 1
FOR UPDATE SKIP LOCKED;      -- ← worker khác lấy công việc kế tiếp
 
UPDATE 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_lieu
SET noi_dung = 'nội dung mới',
    version = version + 1
WHERE 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 B
BEGIN;                              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ần
SELECT * FROM accounts
WHERE id IN (1, 2)
ORDER BY id
FOR 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

Migration nên bọc trong giao dịch

BEGIN;
ALTER TABLE ...;
UPDATE ...;
-- kiểm tra
COMMIT;

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_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start;

Đặt giới hạn:

SET idle_in_transaction_session_timeout = '5min';
SET statement_timeout = '30s';

10. Bài tập

  1. 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.
  2. Thử rút nhiều hơn số dư — quan sát ràng buộc CHECK chặn giao dịch.
  3. 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.
  4. Mở hai phiên psql, chứng minh sự khác biệt giữa READ COMMITTED và REPEATABLE READ.
  5. Viết mẫu hàng đợi công việc dùng FOR UPDATE SKIP LOCKED.
  6. 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.
  7. Chứng minh PostgreSQL cho phép ROLLBACK lệnh CREATE TABLE.

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.

← Sửa đổi dữ liệu - INSERT, UPDATE, DELETE, UPSERT | → Nhập và xuất dữ liệu CSV