PostgreSQL: PostgreSQL事务与并发控制
最后更新:2026-08-26
1. 你将学到
- 理解 ACID 四大特性及 PostgreSQL 的实现方式
- 使用
BEGIN/COMMIT/ROLLBACK控制事务 - 使用
SAVEPOINT实现事务内部分回滚 - 理解 PostgreSQL 三种隔离级别的区别与选择
- 掌握 MVCC 多版本并发控制原理
- 使用行锁、表锁和 Advisory Locks
- 理解死锁检测与处理
- 对比悲观锁与乐观锁策略
- 使用两阶段提交(2PC)处理分布式事务
2. 故事
Alice 负责一家电商平台的订单系统。在某次促销活动中,一件限量商品只剩最后 1 件库存,两个用户几乎同时下单:
- 用户 A 读取库存为 1,准备扣减
- 用户 B 也读取库存为 1,也准备扣减
如果没有事务保护,两人都能下单成功,库存变为 -1,出现超卖。Alice 需要借助 PostgreSQL 的事务隔离、MVCC 和行锁机制,确保只有一个用户成功扣减库存,另一个被阻塞或收到错误提示,从而保证数据一致性。
3. Concept:ACID 与事务基础
(1) ACID 四大特性
| 特性 | 英文 | 含义 | PostgreSQL 实现 |
|---|---|---|---|
| 原子性 | Atomicity | 事务要么全部成功,要么全部回滚 | WAL (Write-Ahead Log) |
| 一致性 | Consistency | 事务前后数据库满足约束 | 约束、触发器、类型检查 |
| 隔离性 | Isolation | 并发事务互不干扰 | MVCC + 锁机制 |
| 持久性 | Durability | 提交后数据不丢失 | WAL + fsync |
▶ 示例:ACID 原子性——转账要么全成功要么全回滚
BEGIN;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 500 WHERE account_id = 2;
COMMIT;
输出:
-- SQL 语句执行成功
(2) 事务控制语句
| 语句 | 作用 | 对应 SQL |
|---|---|---|
| 开始事务 | 显式开启一个事务 | BEGIN 或 START TRANSACTION |
| 提交事务 | 持久化所有修改 | COMMIT |
| 回滚事务 | 撤销所有修改 | ROLLBACK |
▶ 示例:回滚事务恢复数据
BEGIN;
DELETE FROM orders WHERE order_id = 999;
-- Oops, wrong deletion!
ROLLBACK;
-- Data is restored
输出:
-- SQL 语句执行成功
(3) Autocommit 模式
PostgreSQL 默认开启 Autocommit,每条 SQL 自动作为一个事务提交。
| 模式 | 行为 | psql 设置 |
|---|---|---|
| Autocommit ON | 每条语句自动提交 | 默认 |
| Autocommit OFF | 需手动 COMMIT | \set AUTOCOMMIT off |
▶ 示例:关闭 Autocommit 后手动提交
\set AUTOCOMMIT off
DELETE FROM orders WHERE order_status = 'cancelled';
-- Check before committing
SELECT count(*) FROM orders WHERE order_status = 'cancelled';
COMMIT;
输出:
# 命令执行成功
(4) SAVEPOINT 部分回滚
在事务内部设置保存点,可以只回滚到某个保存点而非整个事务。
▶ 示例:使用 SAVEPOINT 实现部分回滚
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
SAVEPOINT sp1;
UPDATE accounts SET balance = balance - 200 WHERE account_id = 1;
-- Second update was wrong, rollback to savepoint
ROLLBACK TO SAVEPOINT sp1;
-- First update still active, can commit
COMMIT;
输出:
-- SQL 语句执行成功
| 语句 | 作用 |
|---|---|
SAVEPOINT sp_name |
创建保存点 |
ROLLBACK TO SAVEPOINT sp_name |
回滚到保存点 |
RELEASE SAVEPOINT sp_name |
释放保存点(之后的回滚不能再引用) |
4. Concept:事务隔离级别
(1) PostgreSQL 的三种隔离级别
PostgreSQL 只实现三种隔离级别(READ UNCOMMITTED 被映射为 READ COMMITTED):
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | PG 实现 |
|---|---|---|---|---|
| READ COMMITTED | 不可能 | 可能 | 可能 | 默认级别,每次查询看到最新已提交快照 |
| REPEATABLE READ | 不可能 | 不可能 | 可能(PG 实际阻止幻读) | 事务内看到开始时的快照 |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 | 最严格,检测序列化冲突 |
▶ 示例:设置隔离级别
BEGIN ISOLATION LEVEL READ COMMITTED;
-- or
BEGIN ISOLATION LEVEL REPEATABLE READ;
-- or
BEGIN ISOLATION LEVEL SERIALIZABLE;
输出:
-- SQL 语句执行成功
(2) READ COMMITTED vs REPEATABLE READ 实战对比
▶ 示例:READ COMMITTED——同一事务内两次查询结果不同
-- Session 1
BEGIN ISOLATION LEVEL READ COMMITTED;
SELECT balance FROM accounts WHERE account_id = 1; -- Returns 1000
-- Session 2 (another connection)
UPDATE accounts SET balance = 800 WHERE account_id = 1;
COMMIT;
-- Back to Session 1
SELECT balance FROM accounts WHERE account_id = 1; -- Returns 800 (sees committed change)
COMMIT;
输出:
count
-------
5
(1 row)
▶ 示例:REPEATABLE READ——同一事务内两次查询结果一致
-- Session 1
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE account_id = 1; -- Returns 1000
-- Session 2 (another connection)
UPDATE accounts SET balance = 800 WHERE account_id = 1;
COMMIT;
-- Back to Session 1
SELECT balance FROM accounts WHERE account_id = 1; -- Still returns 1000
COMMIT;
输出:
count
-------
5
(1 row)
(3) SERIALIZABLE 隔离级别
SERIALIZABLE 是最严格的级别,PostgreSQL 使用 Serializable Snapshot Isolation (SSI) 检测序列化冲突。
▶ 示例:SERIALIZABLE 冲突检测
-- Session 1
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT balance FROM accounts WHERE account_id = 1;
-- Session 2
BEGIN ISOLATION LEVEL SERIALIZABLE;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
COMMIT;
-- Back to Session 1
UPDATE accounts SET balance = balance + 50 WHERE account_id = 1;
-- ERROR: could not serialize access due to concurrent update
输出:
count
-------
5
(1 row)
| 隔离级别选择建议 | 场景 | 推荐级别 |
|---|---|---|
| 大多数 Web 应用 | READ COMMITTED | 默认,性能与一致性平衡 |
| 报表/审计需要一致快照 | REPEATABLE READ | 同一事务多次读保持一致 |
| 银行/财务核心交易 | SERIALIZABLE | 最严格,牺牲性能换安全 |
5. Concept:MVCC 多版本并发控制
(1) MVCC 核心原理
MVCC (Multi-Version Concurrency Control) 是 PostgreSQL 并发控制的核心。每个事务看到的是数据在某个时间点的快照,读操作不阻塞写操作,写操作不阻塞读操作。
每个行版本(tuple)包含四个隐藏字段:
| 字段 | 含义 |
|---|---|
xmin |
插入该行的事务 ID |
xmax |
删除/更新该行的事务 ID(0 表示仍有效) |
xmin 可见性 |
事务 ID < 当前快照的事务能看到此行 |
xmax 可见性 |
事务 ID >= 当前快照的事务看不到此删除 |
▶ 示例:查看行的版本信息
SELECT xmin, xmax, * FROM products WHERE product_id = 1;
输出:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(2) MVCC 读写并发流程
sequenceDiagram
participant R as Reader(Tx1)
participant W as Writer(Tx2)
participant T as Table
R->>T: SELECT (snapshot at Tx1 start)
T-->>R: Returns version V1 (xmin=100, xmax=0)
W->>T: UPDATE (creates new version)
T-->>T: V1 xmax=200, V2 xmin=200 xmax=0
R->>T: SELECT again (same snapshot)
T-->>R: Still returns V1 (Tx2 not committed)
W->>T: COMMIT
Note over T: V2 now visible to new transactions
R->>T: SELECT again (same snapshot)
T-->>R: Still returns V1 (REPEATABLE READ)
▶ 示例:MVCC 读不阻塞写
-- Session 1: Long-running read
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT * FROM orders WHERE order_date = '2025-01-01';
-- This query does NOT block writers
-- Session 2: Concurrent write (no block from Session 1)
UPDATE orders SET order_status = 'shipped' WHERE order_id = 100;
COMMIT;
输出:
UPDATE 3
(3) MVCC 的代价:Dead Tuples 与 VACUUM
更新操作在 MVCC 中不修改原行,而是创建新版本。旧行变成 dead tuples,需要 VACUUM 清理。
| 操作 | MVCC 行为 | Dead Tuples |
|---|---|---|
| INSERT | 创建新行 | 无 |
| DELETE | 标记旧行 xmax | 产生 1 个 dead tuple |
| UPDATE | 标记旧行 xmax + 创建新行 | 产生 1 个 dead tuple |
▶ 示例:查看 dead tuples 数量
SELECT relname, n_live_tup, n_dead_tup, last_vacuum, last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;
输出:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ 示例:手动触发 VACUUM
VACUUM orders; -- Reclaim space, not block reads
VACUUM FULL orders; -- Full table rewrite, locks table
VACUUM ANALYZE orders; -- Reclaim + update statistics
输出:
-- SQL 语句执行成功
| VACUUM 类型 | 锁级别 | 回收空间 | 速度 |
|---|---|---|---|
VACUUM |
SHARE | 标记可复用,不归还磁盘 | 快 |
VACUUM FULL |
ACCESS EXCLUSIVE | 完全归还磁盘 | 慢,锁表 |
VACUUM ANALYZE |
SHARE | 同 VACUUM + 更新统计 | 快 |
6. Concept:锁机制
(1) 行锁(Row-level Locks)
PostgreSQL 在修改行时自动获取行锁,其他事务必须等待。
| 行锁类型 | 获取方式 | 冲突 |
|---|---|---|
| FOR UPDATE | SELECT ... FOR UPDATE |
排他锁,阻塞其他 FOR UPDATE/FOR NO KEY UPDATE |
| FOR NO KEY UPDATE | SELECT ... FOR NO KEY UPDATE |
不阻塞 FOR KEY SHARE |
| FOR SHARE | SELECT ... FOR SHARE |
阻塞 FOR UPDATE/FOR NO KEY UPDATE |
| FOR KEY SHARE | SELECT ... FOR KEY SHARE |
最弱,仅阻塞 FOR UPDATE |
▶ 示例:SELECT FOR UPDATE 防止超卖
BEGIN;
SELECT stock FROM products WHERE product_id = 1 FOR UPDATE;
-- stock = 1, row locked
UPDATE products SET stock = stock - 1 WHERE product_id = 1;
COMMIT;
-- If another transaction tries FOR UPDATE on same row, it waits
输出:
UPDATE 3
(2) 表锁(Table-level Locks)
| 锁模式 | 获取方式 | 冲突 |
|---|---|---|
| ACCESS SHARE | SELECT |
与 ACCESS EXCLUSIVE 冲突 |
| ROW SHARE | SELECT FOR |
与 EXCLUSIVE/ACCESS EXCLUSIVE 冲突 |
| ROW EXCLUSIVE | UPDATE/DELETE |
与 SHARE/EXCLUSIVE 等冲突 |
| SHARE | LOCK TABLE ... SHARE |
与 ROW EXCLUSIVE 等冲突 |
| ACCESS EXCLUSIVE | ALTER TABLE |
与所有锁冲突 |
▶ 示例:显式表锁
LOCK TABLE orders IN ACCESS EXCLUSIVE MODE;
-- Perform critical operations
COMMIT;
输出:
-- SQL 语句执行成功
(3) Advisory Locks(PostgreSQL 特有)
Advisory Locks 是应用级别的锁,不绑定于任何表行,适合分布式协调。
| 函数 | 特点 |
|---|---|
pg_try_advisory_lock(id) |
非阻塞,获取失败立即返回 false |
pg_advisory_lock(id) |
阻塞等待直到获取 |
pg_advisory_unlock(id) |
释放锁 |
pg_advisory_xact_lock(id) |
事务结束自动释放 |
▶ 示例:使用 Advisory Lock 防止重复处理
-- Non-blocking attempt
SELECT pg_try_advisory_lock(12345);
-- Returns true: got the lock, proceed
-- Returns false: another session holds it, skip
-- Transaction-level advisory lock (auto-release on COMMIT/ROLLBACK)
BEGIN;
SELECT pg_advisory_xact_lock(12345);
-- Do work...
COMMIT; -- Lock auto-released
输出:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ 示例:Advisory Lock 实现单实例任务
CREATE FUNCTION run_daily_report() RETURNS void AS $$
BEGIN
IF pg_try_advisory_lock(99999) THEN
-- Only one session can run this at a time
INSERT INTO report_log (report_date, status)
VALUES (CURRENT_DATE, 'running');
PERFORM pg_sleep(5); -- Simulate work
UPDATE report_log SET status = 'done' WHERE report_date = CURRENT_DATE;
PERFORM pg_advisory_unlock(99999);
ELSE
RAISE NOTICE 'Report already running in another session';
END IF;
END;
$$ LANGUAGE plpgsql;
输出:
INSERT 0 1
7. Concept:死锁检测与处理
(1) 死锁的产生
两个事务互相等待对方持有的锁,形成循环依赖。
flowchart LR
A[Tx1: Lock Row A] -->|Wait for Row B| B[Tx2: Lock Row B]
B -->|Wait for Row A| A
A -->|Deadlock!| C[PG auto-detects in 1s]
▶ 示例:死锁场景
-- Session 1
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1; -- Lock row 1
-- Session 2
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 2; -- Lock row 2
-- Session 1 (now waits for row 2)
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
-- Session 2 (deadlock!)
UPDATE accounts SET balance = balance + 100 WHERE account_id = 1;
-- ERROR: deadlock detected
输出:
-- SQL 语句执行成功
(2) 死锁检测与预防
| 策略 | 说明 |
|---|---|
| 自动检测 | PostgreSQL 默认 1 秒(deadlock_timeout)检测一次 |
| 自动回滚 | 检测到死锁后自动回滚其中一个事务 |
| 固定加锁顺序 | 总是按同一顺序获取锁,避免循环等待 |
| 短事务 | 减少事务持有锁的时间 |
▶ 示例:按固定顺序加锁预防死锁
-- Always lock rows in account_id order
BEGIN;
SELECT * FROM accounts WHERE account_id IN (1, 2) ORDER BY account_id FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
COMMIT;
输出:
count
-------
5
(1 row)
8. Concept:悲观锁与乐观锁
(1) 两种并发策略对比
| 维度 | 悲观锁 | 乐观锁 |
|---|---|---|
| 思路 | 先锁再改,冲突时阻塞 | 先改再检,冲突时重试 |
| 实现 | SELECT FOR UPDATE |
version 列 + WHERE version = old |
| 冲突频率 | 适合高冲突场景 | 适合低冲突场景 |
| 性能 | 锁等待开销 | 重试开销 |
| 死锁风险 | 有 | 无 |
▶ 示例:悲观锁实现库存扣减
BEGIN;
SELECT stock FROM products WHERE product_id = 1 FOR UPDATE;
-- stock = 1, row locked
IF stock > 0 THEN
UPDATE products SET stock = stock - 1 WHERE product_id = 1;
END IF;
COMMIT;
输出:
UPDATE 3
▶ 示例:乐观锁实现库存扣减
-- Add version column
ALTER TABLE products ADD COLUMN version INT DEFAULT 1;
-- Optimistic update
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE product_id = 1 AND version = 5;
-- If affected rows = 0, someone else modified it, need to retry
输出:
-- SQL 语句执行成功
▶ 示例:乐观锁应用层重试逻辑
CREATE FUNCTION deduct_stock(p_id INT, p_qty INT) RETURNS BOOLEAN AS $$
DECLARE
v_version INT;
v_stock INT;
v_updated INT;
BEGIN
LOOP
SELECT stock, version INTO v_stock, v_version
FROM products WHERE product_id = p_id;
IF v_stock < p_qty THEN RETURN FALSE; END IF;
UPDATE products
SET stock = stock - p_qty, version = version + 1
WHERE product_id = p_id AND version = v_version;
GET DIAGNOSTICS v_updated = ROW_COUNT;
IF v_updated > 0 THEN RETURN TRUE; END IF;
-- Conflict, retry
END LOOP;
END;
$$ LANGUAGE plpgsql;
输出:
count
-------
5
(1 row)
9. Concept:两阶段提交(2PC)
(1) 2PC 用于分布式事务
当事务涉及多个数据库或外部资源时,需要两阶段提交确保原子性。
| 阶段 | 操作 | 说明 |
|---|---|---|
| PREPARE | PREPARE TRANSACTION 'tx_id' |
事务进入准备状态,写入 WAL |
| COMMIT | COMMIT PREPARED 'tx_id' |
第二阶段确认提交 |
| ROLLBACK | ROLLBACK PREPARED 'tx_id' |
第二阶段确认回滚 |
▶ 示例:两阶段提交流程
-- Phase 1: Prepare
BEGIN;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
PREPARE TRANSACTION 'transfer_out';
-- (Coordinator confirms all participants are prepared)
-- Phase 2: Commit
COMMIT PREPARED 'transfer_out';
输出:
-- SQL 语句执行成功
▶ 示例:查看已准备的事务
SELECT * FROM pg_prepared_xacts;
输出:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
| 参数 | 默认值 | 说明 |
|---|---|---|
max_prepared_transactions |
0 | 必须大于 0 才能使用 2PC |
deadlock_timeout |
1s | 死锁检测间隔 |
idle_in_transaction_session_timeout |
0 | 事务空闲超时(毫秒) |
10. 实战:电商库存扣减完整事务方案
Alice 需要实现一个完整的库存扣减事务,处理并发下单、防超卖、记录订单日志。
-- Step 1: Create tables
CREATE TABLE products (
product_id INT PRIMARY KEY,
product_name TEXT NOT NULL,
stock INT NOT NULL DEFAULT 0,
price NUMERIC(10,2) NOT NULL,
version INT NOT NULL DEFAULT 1
);
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
customer_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL,
total_price NUMERIC(10,2) NOT NULL,
order_status TEXT DEFAULT 'pending',
created_at TIMESTAMP DEFAULT now()
);
INSERT INTO products VALUES
(1, 'Limited Edition Watch', 1, 299.99, 1),
(2, 'Wireless Headphones', 50, 89.99, 1);
-- Step 2: Pessimistic lock approach for hot items
CREATE FUNCTION place_order(
p_customer_id INT, p_product_id INT, p_qty INT
) RETURNS INT AS $$
DECLARE
v_stock INT;
v_order_id INT;
BEGIN
BEGIN
-- Lock the product row
SELECT stock INTO v_stock
FROM products
WHERE product_id = p_product_id
FOR UPDATE;
IF v_stock < p_qty THEN
RAISE EXCEPTION 'Insufficient stock: % < %', v_stock, p_qty;
END IF;
-- Deduct stock
UPDATE products
SET stock = stock - p_qty
WHERE product_id = p_product_id;
-- Create order
INSERT INTO orders (customer_id, product_id, quantity, total_price)
VALUES (p_customer_id, p_product_id, p_qty,
p_qty * (SELECT price FROM products WHERE product_id = p_product_id))
RETURNING order_id INTO v_order_id;
RETURN v_order_id;
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE '%', SQLERRM;
RETURN -1;
END;
END;
$$ LANGUAGE plpgsql;
-- Step 3: Test concurrent orders
SELECT place_order(101, 1, 1); -- Success, order created
SELECT place_order(102, 1, 1); -- Fails, stock insufficient
-- Step 4: Check results
SELECT product_id, stock FROM products WHERE product_id = 1;
SELECT order_id, customer_id, order_status FROM orders;
❓ 常见问题
📖 小节
- ACID 是事务的四大保证,PostgreSQL 通过 WAL、MVCC、约束和锁实现
BEGIN/COMMIT/ROLLBACK控制事务边界,SAVEPOINT实现部分回滚- PostgreSQL 实现三种隔离级别:READ COMMITTED(默认)、REPEATABLE READ、SERIALIZABLE
- MVCC 让读写互不阻塞,每个事务看到数据快照,更新创建新版本
- Dead tuples 由 VACUUM/autovacuum 清理,大量更新场景需关注清理策略
- 行锁
SELECT FOR UPDATE防止并发修改冲突,Advisory Lock 用于应用级协调 - 死锁由 PostgreSQL 自动检测(1 秒)并回滚其中一个事务
- 悲观锁适合高冲突场景,乐观锁适合低冲突场景
- 两阶段提交(2PC)用于分布式事务的原子性保证
📝 作业
-
⭐ 编写事务,从
accounts表中账户 1 转账 200 USD 到账户 2,如果账户 1 余额不足则回滚。 -
⭐⭐ 使用
SAVEPOINT编写事务:先插入一条订单记录,然后尝试更新库存,如果库存不足则回滚到保存点但保留订单(状态标记为 'failed'),最后提交事务。 -
⭐⭐⭐ 实现一个乐观锁版本的库存扣减函数:使用 version 列,当并发冲突时自动重试最多 3 次,3 次都失败则返回 FALSE。同时记录每次重试的日志到
retry_log表。