强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

PostgreSQL 完全指南 / 10 - 事务与锁

第 10 章 · 事务与锁

事务是关系型数据库的基石。本章深入讲解 ACID 原理、隔离级别、MVCC 实现、锁机制和死锁处理。


10.1 ACID 原则

原则全称含义PG 实现机制
AAtomicity 原子性事务全部成功或全部回滚CLOG 事务状态
CConsistency 一致性数据满足所有约束约束检查(CHECK/UNIQUE/FK)
IIsolation 隔离性并发事务互不干扰MVCC + 锁
DDurability 持久性提交后数据不丢失WAL + fsync

10.2 事务基本操作

-- 显式事务
BEGIN;
    UPDATE accounts SET balance = balance - 100 WHERE id = 1;
    UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

-- 回滚
BEGIN;
    DELETE FROM important_data WHERE id = 42;
ROLLBACK;  -- 数据恢复

-- SAVEPOINT(保存点)
BEGIN;
    INSERT INTO orders (customer_id, total) VALUES (1, 100);
    SAVEPOINT sp1;
        INSERT INTO order_items (order_id, product_id, qty) VALUES (1, 10, 5);
    ROLLBACK TO sp1;  -- 只回滚到保存点,订单仍在
    INSERT INTO order_items (order_id, product_id, qty) VALUES (1, 20, 3);
COMMIT;

-- 自动提交
-- psql 默认每条语句自动提交
-- 关闭自动提交:\set AUTOCOMMIT off

10.3 隔离级别

隔离级别脏读不可重复读幻读序列化异常
READ UNCOMMITTED✅ 可能✅ 可能✅ 可能✅ 可能
READ COMMITTED(PG 默认)❌ 不会✅ 可能✅ 可能✅ 可能
REPEATABLE READ❌ 不会❌ 不会❌ 不会✅ 可能
SERIALIZABLE❌ 不会❌ 不会❌ 不会❌ 不会

⚠️ 注意:PostgreSQL 中,即使设置 READ UNCOMMITTED,行为也等同于 READ COMMITTED(不会读到脏数据)。这是因为 MVCC 的天然优势。

实际演示

-- READ COMMITTED(默认)
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;
    SELECT balance FROM accounts WHERE id = 1;  -- 假设读到 1000
    -- 此时另一个事务把 balance 改为 800 并提交
    SELECT balance FROM accounts WHERE id = 1;  -- 读到 800(不可重复读)
COMMIT;

-- REPEATABLE READ
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
    SELECT balance FROM accounts WHERE id = 1;  -- 读到 1000
    -- 此时另一个事务把 balance 改为 800 并提交
    SELECT balance FROM accounts WHERE id = 1;  -- 仍然读到 1000(快照)
    -- 但如果尝试修改这行,会报错:could not serialize access due to concurrent update
COMMIT;

-- SERIALIZABLE
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    -- 最强隔离,任何序列化异常都会回滚
    -- 适合银行转账等场景
COMMIT;

隔离级别选择指南

场景推荐隔离级别
一般业务READ COMMITTED(默认,够用)
需要一致性快照REPEATABLE READ
金融/转账SERIALIZABLE
批量报表REPEATABLE READ
高并发 OLTPREAD COMMITTED

10.4 MVCC 实现细节

可见性规则

每行有 xmin(插入事务 ID)和 xmax(删除事务 ID)。可见性判断:

行对当前事务可见,当且仅当:
1. xmin 已提交
2. xmax 未设置(为 0)或 xmax 未提交或 xmax 在当前快照之后
-- 查看行的事务信息
SELECT xmin, xmax, ctid, id, name FROM employees;

-- xmin  | xmax | ctid  | id | name
-- 1000  |    0 | (0,1) |  1 | Alice     ← 活跃(xmax=0)
-- 1000  | 1001 | (0,2) |  2 | Bob       ← 被事务 1001 删除
-- 1002  |    0 | (0,3) |  3 | Charlie   ← 活跃

-- 查看事务 ID 信息
SELECT txid_current();          -- 当前事务 ID
SELECT txid_current_snapshot(); -- 当前快照

HOT Update(Heap-Only Tuple)

-- 如果更新的列不涉及索引列,且新行在同一页面,PG 使用 HOT Update
-- HOT Update 不需要更新索引,性能更好

-- 设计建议:频繁更新的列和索引列分开
CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    customer_id INT NOT NULL,       -- 索引列(不频繁更新)
    status SMALLINT DEFAULT 0,      -- 频繁更新的列
    updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_orders_customer ON orders (customer_id);
-- 更新 status 和 updated_at 不影响 customer_id 索引 → HOT Update

10.5 锁机制

行级锁

锁类型SQL 语法冲突用途
FOR UPDATESELECT ... FOR UPDATE其他 FOR UPDATE更新行
FOR NO KEY UPDATESELECT ... FOR NO KEY UPDATEFOR UPDATE更新非键列
FOR SHARESELECT ... FOR SHAREFOR UPDATE读取并阻止修改
FOR KEY SHARESELECT ... FOR KEY SHAREFOR NO KEY UPDATE外键检查
-- 锁定行,防止并发修改
BEGIN;
    SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
    -- 其他会话尝试修改这行会阻塞
    UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;

-- NOWAIT:如果无法获取锁,立即报错
SELECT * FROM accounts WHERE id = 1 FOR UPDATE NOWAIT;

-- SKIP LOCKED:跳过已锁定的行(队列场景)
SELECT * FROM tasks WHERE status = 'pending' ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED;

表级锁

锁模式说明冲突
ACCESS SHARESELECTACCESS EXCLUSIVE
ROW SHARESELECT FOR UPDATE/SHAREEXCLUSIVE, ACCESS EXCLUSIVE
ROW EXCLUSIVEINSERT/UPDATE/DELETESHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE, ACCESS EXCLUSIVE
SHARE UPDATE EXCLUSIVEVACUUM, 某些 ALTERSHARE UPDATE EXCLUSIVE 以上
SHARECREATE INDEXROW EXCLUSIVE 以上
SHARE ROW EXCLUSIVEROW EXCLUSIVE 以上
EXCLUSIVE大部分
ACCESS EXCLUSIVEALTER TABLE, DROP TABLE, TRUNCATE所有锁
-- 查看当前锁
SELECT
    l.locktype, l.mode, l.granted,
    a.pid, a.usename, a.query, a.state
FROM pg_locks l
JOIN pg_stat_activity a ON l.pid = a.pid
WHERE l.relation IS NOT NULL
ORDER BY l.relation, l.mode;

-- 查看锁等待关系
SELECT
    blocked_locks.pid AS blocked_pid,
    blocked_activity.usename AS blocked_user,
    blocking_locks.pid AS blocking_pid,
    blocking_activity.usename AS blocking_user,
    blocked_activity.query AS blocked_query
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks
    ON blocking_locks.locktype = blocked_locks.locktype
    AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
    AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
    AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
    AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
    AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
    AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
    AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
    AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
    AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
    AND blocking_locks.pid != blocked_locks.pid
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted;

10.6 死锁

死锁场景

事务 A: UPDATE t SET x=1 WHERE id=1; -- 锁住行 1
事务 B: UPDATE t SET x=2 WHERE id=2; -- 锁住行 2
事务 A: UPDATE t SET x=1 WHERE id=2; -- 等待行 2 → 阻塞
事务 B: UPDATE t SET x=2 WHERE id=1; -- 等待行 1 → 死锁!

PostgreSQL 自动检测死锁(默认 1 秒),回滚其中一个事务:

ERROR: deadlock detected
DETAIL: Process 1234 waits for ShareLock on transaction 5678; blocked by process 5678.
Process 5678 waits for ShareLock on transaction 1234; blocked by process 1234.
HINT: See server log for query details.

预防死锁

-- 策略 1:按固定顺序访问表/行
-- 总是按 id 升序处理
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

-- 策略 2:使用 NOWAIT 或超时
SET lock_timeout = '5s';
SELECT * FROM accounts WHERE id = 1 FOR UPDATE NOWAIT;

-- 策略 3:减少事务持有时间
-- 不要在事务中做外部 API 调用

10.7 Advisory Lock(咨询锁)

-- 会话级咨询锁
SELECT pg_advisory_lock(12345);      -- 获取锁(阻塞式)
SELECT pg_try_advisory_lock(12345);  -- 尝试获取(非阻塞)
SELECT pg_advisory_unlock(12345);    -- 释放锁

-- 事务级咨询锁(事务结束自动释放)
SELECT pg_advisory_xact_lock(12345);
SELECT pg_try_advisory_xact_lock(12345);

-- 适用场景:
-- 1. 分布式锁
-- 2. 防止并发任务重复执行
-- 3. 速率限制

10.8 VACUUM 与事务 ID 回卷

-- 事务 ID 是 32 位无符号整数,约 20 亿个
-- 超过后会回卷(wraparound),导致旧数据丢失

-- 查看事务 ID 消耗
SELECT
    datname,
    age(datfrozenxid) AS xid_age,
    2147483647 - age(datfrozenxid) AS remaining
FROM pg_database;

-- VACUUM FREEZE 防止回卷
VACUUM FREEZE mytable;

-- autovacuum 会自动执行防回卷 VACUUM
-- 确保 autovacuum 正常运行!

⚠️ 注意事项:如果某个表的 age(relfrozenxid) 接近 20 亿,数据库会强制关闭以防止数据损坏。必须确保 autovacuum 正常运行。


业务场景

场景推荐方案
转账SERIALIZABLE + SELECT FOR UPDATE
库存扣减SELECT FOR UPDATE NOWAIT + 乐观锁
任务队列SELECT FOR UPDATE SKIP LOCKED
防重复执行pg_advisory_lock
长事务限制 idle_in_transaction 会话超时

扩展阅读