PostgreSQL: PostgreSQL事务与并发控制

最后更新:2026-08-26

1. 你将学到


2. 故事

Alice 负责一家电商平台的订单系统。在某次促销活动中,一件限量商品只剩最后 1 件库存,两个用户几乎同时下单:

如果没有事务保护,两人都能下单成功,库存变为 -1,出现超卖。Alice 需要借助 PostgreSQL 的事务隔离、MVCC 和行锁机制,确保只有一个用户成功扣减库存,另一个被阻塞或收到错误提示,从而保证数据一致性。


3. Concept:ACID 与事务基础

(1) ACID 四大特性

特性 英文 含义 PostgreSQL 实现
原子性 Atomicity 事务要么全部成功,要么全部回滚 WAL (Write-Ahead Log)
一致性 Consistency 事务前后数据库满足约束 约束、触发器、类型检查
隔离性 Isolation 并发事务互不干扰 MVCC + 锁机制
持久性 Durability 提交后数据不丢失 WAL + fsync

▶ 示例:ACID 原子性——转账要么全成功要么全回滚

SQL
BEGIN;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 500 WHERE account_id = 2;
COMMIT;

输出:

TEXT 📖 仅展示
-- SQL 语句执行成功

(2) 事务控制语句

语句 作用 对应 SQL
开始事务 显式开启一个事务 BEGINSTART TRANSACTION
提交事务 持久化所有修改 COMMIT
回滚事务 撤销所有修改 ROLLBACK

▶ 示例:回滚事务恢复数据

SQL
BEGIN;
DELETE FROM orders WHERE order_id = 999;
-- Oops, wrong deletion!
ROLLBACK;
-- Data is restored

输出:

TEXT 📖 仅展示
-- SQL 语句执行成功

(3) Autocommit 模式

PostgreSQL 默认开启 Autocommit,每条 SQL 自动作为一个事务提交。

模式 行为 psql 设置
Autocommit ON 每条语句自动提交 默认
Autocommit OFF 需手动 COMMIT \set AUTOCOMMIT off

▶ 示例:关闭 Autocommit 后手动提交

BASH
\set AUTOCOMMIT off
DELETE FROM orders WHERE order_status = 'cancelled';
-- Check before committing
SELECT count(*) FROM orders WHERE order_status = 'cancelled';
COMMIT;

输出:

TEXT 📖 仅展示
# 命令执行成功

(4) SAVEPOINT 部分回滚

在事务内部设置保存点,可以只回滚到某个保存点而非整个事务。

▶ 示例:使用 SAVEPOINT 实现部分回滚

SQL
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;

输出:

TEXT 📖 仅展示
-- 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 不可能 不可能 不可能 最严格,检测序列化冲突

▶ 示例:设置隔离级别

SQL
BEGIN ISOLATION LEVEL READ COMMITTED;
-- or
BEGIN ISOLATION LEVEL REPEATABLE READ;
-- or
BEGIN ISOLATION LEVEL SERIALIZABLE;

输出:

TEXT 📖 仅展示
-- SQL 语句执行成功

(2) READ COMMITTED vs REPEATABLE READ 实战对比

▶ 示例:READ COMMITTED——同一事务内两次查询结果不同

SQL
-- 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;

输出:

TEXT 📖 仅展示
 count 
-------
     5
(1 row)

▶ 示例:REPEATABLE READ——同一事务内两次查询结果一致

SQL
-- 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;

输出:

TEXT 📖 仅展示
 count 
-------
     5
(1 row)

(3) SERIALIZABLE 隔离级别

SERIALIZABLE 是最严格的级别,PostgreSQL 使用 Serializable Snapshot Isolation (SSI) 检测序列化冲突。

▶ 示例:SERIALIZABLE 冲突检测

SQL
-- 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

输出:

TEXT 📖 仅展示
 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 >= 当前快照的事务看不到此删除

▶ 示例:查看行的版本信息

SQL
SELECT xmin, xmax, * FROM products WHERE product_id = 1;

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

(2) MVCC 读写并发流程

100%
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 读不阻塞写

SQL
-- 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;

输出:

TEXT 📖 仅展示
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 数量

SQL
SELECT relname, n_live_tup, n_dead_tup, last_vacuum, last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ 示例:手动触发 VACUUM

SQL
VACUUM orders;              -- Reclaim space, not block reads
VACUUM FULL orders;         -- Full table rewrite, locks table
VACUUM ANALYZE orders;      -- Reclaim + update statistics

输出:

TEXT 📖 仅展示
-- 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 防止超卖

SQL
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

输出:

TEXT 📖 仅展示
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 与所有锁冲突

▶ 示例:显式表锁

SQL
LOCK TABLE orders IN ACCESS EXCLUSIVE MODE;
-- Perform critical operations
COMMIT;

输出:

TEXT 📖 仅展示
-- 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 防止重复处理

SQL
-- 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

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ 示例:Advisory Lock 实现单实例任务

SQL
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;

输出:

TEXT 📖 仅展示
INSERT 0 1

7. Concept:死锁检测与处理

(1) 死锁的产生

两个事务互相等待对方持有的锁,形成循环依赖。

100%
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]

▶ 示例:死锁场景

SQL
-- 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

输出:

TEXT 📖 仅展示
-- SQL 语句执行成功

(2) 死锁检测与预防

策略 说明
自动检测 PostgreSQL 默认 1 秒(deadlock_timeout)检测一次
自动回滚 检测到死锁后自动回滚其中一个事务
固定加锁顺序 总是按同一顺序获取锁,避免循环等待
短事务 减少事务持有锁的时间

▶ 示例:按固定顺序加锁预防死锁

SQL
-- 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;

输出:

TEXT 📖 仅展示
 count 
-------
     5
(1 row)

8. Concept:悲观锁与乐观锁

(1) 两种并发策略对比

维度 悲观锁 乐观锁
思路 先锁再改,冲突时阻塞 先改再检,冲突时重试
实现 SELECT FOR UPDATE version 列 + WHERE version = old
冲突频率 适合高冲突场景 适合低冲突场景
性能 锁等待开销 重试开销
死锁风险

▶ 示例:悲观锁实现库存扣减

SQL
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;

输出:

TEXT 📖 仅展示
UPDATE 3

▶ 示例:乐观锁实现库存扣减

SQL
-- 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

输出:

TEXT 📖 仅展示
-- SQL 语句执行成功

▶ 示例:乐观锁应用层重试逻辑

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;

输出:

TEXT 📖 仅展示
 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' 第二阶段确认回滚

▶ 示例:两阶段提交流程

SQL
-- 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';

输出:

TEXT 📖 仅展示
-- SQL 语句执行成功

▶ 示例:查看已准备的事务

SQL
SELECT * FROM pg_prepared_xacts;

输出:

TEXT 📖 仅展示
 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 需要实现一个完整的库存扣减事务,处理并发下单、防超卖、记录订单日志。

SQL
-- 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;

❓ 常见问题

Q PostgreSQL 为什么没有 READ UNCOMMITTED 隔离级别?
A PostgreSQL 将 READ UNCOMMITTED 映射为 READ COMMITTED,因为 MVCC 架构下不可能读到未提交数据(总是读快照),所以脏读在 PG 中不可能发生。
Q REPEATABLE READ 能防止幻读吗?
A PostgreSQL 的 REPEATABLE READ 实际上可以防止幻读,这超出了 SQL 标准对该级别的要求。标准中只有 SERIALIZABLE 才防幻读,但 PG 通过 MVCC 快照机制在 REPEATABLE READ 下也阻止了幻读。
Q SAVEPOINT 能嵌套吗?
A 可以。SAVEPOINT 支持嵌套,内层 ROLLBACK TO SAVEPOINT 只回滚到该保存点,不影响外层。RELEASE SAVEPOINT 会释放指定保存点及其之后创建的所有保存点。
Q SELECT FOR UPDATE 和直接 UPDATE 有什么区别?
A SELECT FOR UPDATE 先锁定行再决定是否修改,适合"先读后写"的复合操作;直接 UPDATE 是一步完成。SELECT FOR UPDATE 的优势是可以在修改前做业务判断。
Q VACUUM FULL 什么时候用?
A 仅在表大量膨胀且日常 VACUUM 无法回收空间时使用。VACUUM FULL 需要 ACCESS EXCLUSIVE 锁,期间表完全不可用。常规维护应依赖 autovacuum。
Q Advisory Lock 和普通锁有什么区别?
A Advisory Lock 是应用定义的锁,不绑定表或行,不会自动在事务结束时释放(除非用 xact 版本)。普通锁由 PostgreSQL 自动管理,绑定特定数据库对象。
Q 2PC 在什么场景下使用?
A 2PC 主要用于跨数据库或跨系统的分布式事务,需要协调者(coordinator)统一提交。单库事务用普通的 BEGIN/COMMIT 即可,2PC 有额外开销且需要配置 max_prepared_transactions。

📖 小节


📝 作业

  1. ⭐ 编写事务,从 accounts 表中账户 1 转账 200 USD 到账户 2,如果账户 1 余额不足则回滚。

  2. ⭐⭐ 使用 SAVEPOINT 编写事务:先插入一条订单记录,然后尝试更新库存,如果库存不足则回滚到保存点但保留订单(状态标记为 'failed'),最后提交事务。

  3. ⭐⭐⭐ 实现一个乐观锁版本的库存扣减函数:使用 version 列,当并发冲突时自动重试最多 3 次,3 次都失败则返回 FALSE。同时记录每次重试的日志到 retry_log 表。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏