PostgreSQL笔记29:事务隔离级别深度解析——从MVCC快照到SSI可串行化

纲要

  • 事务快照(Transaction Snapshot)
    • 快照的组成:xminxmaxxip_list
    • 快照查看函数:pg_current_snapshot()(PG 13+)、txid_current_snapshot()(已废弃)
    • 快照的可见性判定规则
  • SQL标准定义的并发异常
    • 脏读(Dirty Read)
    • 不可重复读(Nonrepeatable Read)
    • 幻读(Phantom Read)
    • 序列化异常(Serialization Anomaly)
    • 更新丢失(Lost Update)
    • 读偏序(Read Skew)与写偏序(Write Skew)
  • PostgreSQL支持的隔离级别
    • READ COMMITTED(默认)
    • REPEATABLE READ
    • SERIALIZABLE
    • READ UNCOMMITTED 的行为等价于 READ COMMITTED
  • 各隔离级别下的异常对照表
  • 更新丢失的深入分析
    • READ COMMITTED 下应用层更新丢失的成因与复现
    • REPEATABLE READ 下更新冲突的处理
  • 可串行化快照隔离(SSI)
    • 谓词锁(Predicate Lock / SIREAD Lock)
    • 序列化失败的处理

事务快照:MVCC的基石

PostgreSQL通过多版本并发控制(MVCC)实现事务隔离,其核心机制是事务快照(Transaction Snapshot)。快照记录了在某一时刻所有事务的活动状态,每个事务根据自己所持有的快照来判断哪些数据版本可见。

快照的组成

快照的文本表示格式为 xmin:xmax:xip_list

  • xmin :当前仍处于活跃状态的最早事务ID。所有小于 xmin 的事务,要么已提交(可见),要么已回滚(不可见)。
  • xmax :第一个尚未被分配的事务ID。所有大于等于 xmax 的事务ID,在快照时刻尚未启动,因此不可见。
  • xip_list :快照时刻活跃事务ID列表,仅包含 xminxmax 之间的活跃事务。

例如,快照 100:104:100,102 表示:

  • xmin = 100:事务ID小于100的均非活跃
  • xmax = 104:事务ID大于等于104的尚未启动
  • xip_list = 100,102:事务100和102在快照时刻处于活跃状态

查看当前快照

PostgreSQL提供了查看当前事务快照的函数:

sql 复制代码
-- PostgreSQL 13+ 推荐使用
SELECT pg_current_snapshot();

-- PostgreSQL 12及以下版本使用(已废弃)
SELECT txid_current_snapshot();

pg_current_snapshot() 函数返回 pg_snapshot 类型,自PostgreSQL 13起引入,替代了原有的 txid_current_snapshot()

sql 复制代码
postgres=# SELECT pg_current_snapshot();
 pg_current_snapshot 
---------------------
 795:799:795,797
(1 row)

快照获取时机

不同隔离级别下,快照的获取策略不同:

  • READ COMMITTED :每个SQL命令执行时重新获取一次快照
  • REPEATABLE READSERIALIZABLE :事务中第一个SQL命令执行时获取快照,整个事务期间复用该快照

这一差异直接决定了不同隔离级别下可见性行为的不同。

SQL标准定义的并发异常

SQL标准定义了多种并发事务可能引发的异常现象:

异常类型 描述
脏读(Dirty Read) 一个事务读取了另一个事务尚未提交的数据
不可重复读(Nonrepeatable Read) 同一事务内两次查询返回不同的结果集(因其他事务提交了更改)
幻读(Phantom Read) 同一事务内两次查询返回不同的行集合(因其他事务插入了满足条件的新行)
序列化异常(Serialization Anomaly) 并发执行的结果不等同于任何串行执行顺序的结果

此外,实践中还需关注:

  • 更新丢失(Lost Update):两个事务同时读取并更新同一行,后提交的事务覆盖了先提交事务的更改
  • 读偏序(Read Skew):事务读取了不一致的数据状态(如账户余额汇总时部分账户已更新)
  • 写偏序(Write Skew):两个事务各自基于读取的数据做出写入决策,整体结果不一致

PostgreSQL的隔离级别

PostgreSQL内部仅实现三种隔离级别:

隔离级别 脏读 不可重复读 幻读 序列化异常
READ UNCOMMITTED 不可能(PG实现) 可能 可能 可能
READ COMMITTED 不可能 可能 可能 可能
REPEATABLE READ 不可能 不可能 不可能(PG实现) 可能
SERIALIZABLE 不可能 不可能 不可能 不可能

关键要点

  1. READ UNCOMMITTED 在PostgreSQL中的行为等价于 READ COMMITTED------虽然语法层面支持,但实际效果相同。
  2. PostgreSQL的 REPEATABLE READ 不允许幻读,这比SQL标准的要求更为严格(标准允许幻读)。
  3. READ COMMITTED 是PostgreSQL的默认隔离级别

设置事务隔离级别的语法:

sql 复制代码
-- 方式一:BEGIN时指定
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- 方式二:BEGIN后使用SET TRANSACTION
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- 方式三:设置会话默认值
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ COMMITTED;

版本说明SET SESSION CHARACTERISTICSBEGIN TRANSACTION ISOLATION LEVEL 语法在所有受支持的PostgreSQL版本中均可用。

各隔离级别行为详解

READ COMMITTED(读已提交)

READ COMMITTED 是默认隔离级别。每个查询开始时获取一次快照,因此:

  • 不会出现脏读:查询只能看到查询开始前已提交的数据
  • 会出现不可重复读:两次查询之间,其他事务可能提交更改,导致两次结果不同
  • 会出现幻读:其他事务插入的新行可能在后续查询中可见
  • 会出现序列化异常

UPDATE、DELETE、SELECT FOR UPDATE 等命令在搜索目标行时,只会找到命令开始时已提交的行。如果目标行被其他并发事务修改,当前事务会等待该事务提交或回滚。

REPEATABLE READ(可重复读)

事务在第一个命令执行时获取快照并在整个事务期间复用

  • 不会出现不可重复读:整个事务使用同一快照,两次查询结果一致
  • 不会出现幻读(PostgreSQL实现比标准更严格)
  • 可能出现序列化异常
  • 更新冲突时抛出错误:ERROR: could not serialize access due to concurrent update

SERIALIZABLE(可串行化)

最严格的隔离级别:

  • 杜绝所有SQL标准定义的异常
  • 基于可串行化快照隔离(Serializable Snapshot Isolation, SSI) 实现
  • 检测到序列化冲突时,终止其中一个事务并返回 serialization_failure 错误
  • 性能开销最大,系统吞吐量显著下降
  • 热备(Hot Standby)备库不支持 SERIALIZABLE 隔离级别

更新丢失(Lost Update)深度分析

更新丢失是应用层面需要特别关注的问题。PostgreSQL在 READ COMMITTED 隔离级别下无法自动预防应用层的更新丢失

READ COMMITTED下的更新丢失复现

sql 复制代码
-- 准备测试表
CREATE TABLE accounts (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    amount INT
);

INSERT INTO accounts VALUES (1, 'alice', 1000);

事务T1(窗口1)

sql 复制代码
BEGIN;  -- 默认 READ COMMITTED

-- 查询alice余额
SELECT amount FROM accounts WHERE id = 1;  -- 返回 1000

-- 应用层计算:1000 + 100 = 1100
UPDATE accounts SET amount = 1100 WHERE id = 1;

COMMIT;

事务T2(窗口2)------并发执行

sql 复制代码
BEGIN;  -- 默认 READ COMMITTED

-- 查询alice余额(在T1提交前执行,返回1000)
SELECT amount FROM accounts WHERE id = 1;  -- 返回 1000

-- 应用层计算:1000 + 100 = 1100
-- 该UPDATE会被T1的行锁阻塞,直到T1提交
UPDATE accounts SET amount = 1100 WHERE id = 1;

COMMIT;

最终结果alice 的余额变为 1100 ,而非预期的 1200。两次各加100的操作,丢失了100。

问题成因

PostgreSQL无法推断应用层 1000 这个值与 UPDATE 语句中 1100 的关联关系。在 READ COMMITTED 下,T2的UPDATE被阻塞等待T1提交后,会重新获取快照 并读取T1提交后的最新值(1100),然后在此基础上应用 SET amount = 1100,导致T1的更新被覆盖。

正确做法:在UPDATE中直接计算

sql 复制代码
-- 正确:原子性地在原值基础上增加
BEGIN;
UPDATE accounts SET amount = amount + 100 WHERE id = 1;
COMMIT;

此时即使两个事务并发执行,行锁机制确保更新串行化,最终结果为1200。

REPEATABLE READ下的行为

sql 复制代码
-- 窗口1
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT amount FROM accounts WHERE id = 1;  -- 1000
-- 暂不提交

-- 窗口2
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT amount FROM accounts WHERE id = 1;  -- 1000
UPDATE accounts SET amount = 1100 WHERE id = 1;  -- 被阻塞

-- 窗口1提交后,窗口2收到错误
ERROR: could not serialize access due to concurrent update

REPEATABLE READ 下,PostgreSQL检测到更新冲突并主动报错,而非静默覆盖。

可串行化快照隔离(SSI)

PostgreSQL的 SERIALIZABLE 隔离级别基于可串行化快照隔离(Serializable Snapshot Isolation, SSI) 实现。

谓词锁(Predicate Lock / SIREAD Lock)

SSI的核心机制是谓词锁(SIREAD锁)

  • 与传统的读写锁不同,SIREAD锁不阻塞写操作
  • 其作用是跟踪事务间的读写依赖关系(rw-conflict edges)
  • 当检测到依赖图中存在环(即无法串行化),PostgreSQL终止其中一个事务

序列化失败处理

应用程序在使用 SERIALIZABLE 隔离级别时,必须处理 serialization_failure 错误并重试事务:

sql 复制代码
-- 应用层重试逻辑(伪代码)
WHILE TRUE:
    BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    TRY:
        -- 执行业务操作
        COMMIT;
        BREAK;
    EXCEPT serialization_failure:
        ROLLBACK;
        -- 等待后重试
        SLEEP(random_delay);
        CONTINUE;

版本说明

  • PostgreSQL 9.1 之前,SERIALIZABLE 级别实际上等价于快照隔离(Snapshot Isolation),而非真正的可串行化
  • PostgreSQL 9.1 起引入SSI,提供了完整的可串行化保证
  • pg_current_snapshot() 自 PostgreSQL 13 起可用

API 速览

1. pg_current_snapshot()

  • 所属库:系统函数
  • 方法签名pg_current_snapshot() → pg_snapshot
  • 描述:返回当前事务的快照
  • 引入版本:PostgreSQL 13
  • 返回值pg_snapshot 类型,文本格式为 xmin:xmax:xip_list
sql 复制代码
SELECT pg_current_snapshot();
-- 输出示例: 795:799:795,797

2. txid_current_snapshot()(已废弃)

  • 所属库:系统函数
  • 方法签名txid_current_snapshot() → txid_snapshot
  • 描述 :返回当前事务的快照(已废弃,推荐使用 pg_current_snapshot()
  • 适用版本:PostgreSQL 12及以下

3. SET TRANSACTION

  • 所属库:SQL命令
  • 语法SET TRANSACTION transaction_mode [, ...]
  • 参数ISOLATION LEVEL { SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED }
  • 描述:设置当前事务的特性
sql 复制代码
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 执行操作
COMMIT;

4. BEGIN 事务块

  • 所属库:SQL命令
  • 语法BEGIN [ WORK | TRANSACTION ] [ transaction_mode [, ...] ]
  • 参数 :同 SET TRANSACTION
  • 描述:开始一个事务块
sql 复制代码
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 执行操作
COMMIT;

Demo 简单示例

以下是一个使用Node.js + pg 驱动演示各隔离级别行为的完整示例。

运行说明

  1. 确保PostgreSQL运行中,创建测试数据库
  2. 安装依赖:npm install pg
  3. 运行:node isolation_demo.js

代码

js 复制代码
const { Client } = require('pg');

const config = {
    host: 'localhost',
    port: 5432,
    database: 'testdb',
    user: 'postgres',
    password: 'your_password'
};

// 对应的PostgreSQL原生指令:
// psql -d testdb -U postgres

async function setupTable(client) {
    await client.query(`
        DROP TABLE IF EXISTS accounts;
        CREATE TABLE accounts (
            id INT PRIMARY KEY,
            name VARCHAR(50),
            amount INT
        );
        INSERT INTO accounts VALUES (1, 'alice', 1000);
    `);
    console.log('[Setup] 表已创建,初始余额: 1000');
}

// Demo 1: READ COMMITTED 下的更新丢失
async function demoLostUpdateRC() {
    const client1 = new Client(config);
    const client2 = new Client(config);
    await client1.connect();
    await client2.connect();

    try {
        await setupTable(client1);

        // T1: 读取余额
        await client1.query('BEGIN');
        const res1 = await client1.query('SELECT amount FROM accounts WHERE id = 1');
        const balance1 = res1.rows[0].amount;
        console.log(`[T1] 读取余额: ${balance1}`);

        // T2: 并发读取(在T1提交前)
        await client2.query('BEGIN');
        const res2 = await client2.query('SELECT amount FROM accounts WHERE id = 1');
        const balance2 = res2.rows[0].amount;
        console.log(`[T2] 读取余额: ${balance2}`);

        // T1: 更新为 balance1 + 100
        await client1.query('UPDATE accounts SET amount = $1 WHERE id = 1', [balance1 + 100]);
        await client1.query('COMMIT');
        console.log(`[T1] 提交更新: ${balance1} -> ${balance1 + 100}`);

        // T2: 更新为 balance2 + 100(此时T1已提交)
        // 在READ COMMITTED下,T2会读取到T1提交后的新值,然后应用自己的更新
        await client2.query('UPDATE accounts SET amount = $1 WHERE id = 1', [balance2 + 100]);
        await client2.query('COMMIT');
        console.log(`[T2] 提交更新: ${balance2} -> ${balance2 + 100}`);

        const final = await client1.query('SELECT amount FROM accounts WHERE id = 1');
        console.log(`[结果] 最终余额: ${final.rows[0].amount} (预期: 1200, 实际: ${final.rows[0].amount})`);
        // 输出: 1100 ------ 更新丢失!
    } finally {
        await client1.end();
        await client2.end();
    }
}

// Demo 2: 正确的原子更新
async function demoCorrectUpdate() {
    const client = new Client(config);
    await client.connect();

    try {
        await setupTable(client);

        // 使用原子操作: amount = amount + 100
        await client.query('BEGIN');
        await client.query('UPDATE accounts SET amount = amount + 100 WHERE id = 1');
        await client.query('COMMIT');

        const res = await client.query('SELECT amount FROM accounts WHERE id = 1');
        console.log(`[正确方式] 最终余额: ${res.rows[0].amount} (预期: 1100)`);
    } finally {
        await client.end();
    }
}

// Demo 3: REPEATABLE READ 下检测更新冲突
async function demoRRConflict() {
    const client1 = new Client(config);
    const client2 = new Client(config);
    await client1.connect();
    await client2.connect();

    try {
        await setupTable(client1);

        // T1: REPEATABLE READ
        await client1.query('BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ');
        const res1 = await client1.query('SELECT amount FROM accounts WHERE id = 1');
        console.log(`[RR-T1] 读取余额: ${res1.rows[0].amount}`);

        // T2: REPEATABLE READ
        await client2.query('BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ');
        const res2 = await client2.query('SELECT amount FROM accounts WHERE id = 1');
        console.log(`[RR-T2] 读取余额: ${res2.rows[0].amount}`);

        // T1 更新并提交
        await client1.query('UPDATE accounts SET amount = $1 WHERE id = 1', [res1.rows[0].amount + 100]);
        await client1.query('COMMIT');
        console.log('[RR-T1] 提交成功');

        // T2 尝试更新 ------ 应该报错
        try {
            await client2.query('UPDATE accounts SET amount = $1 WHERE id = 1', [res2.rows[0].amount + 100]);
            await client2.query('COMMIT');
            console.log('[RR-T2] 提交成功(不应发生)');
        } catch (err) {
            await client2.query('ROLLBACK');
            console.log(`[RR-T2] 预期错误: ${err.message}`);
            // 输出: could not serialize access due to concurrent update
        }
    } finally {
        await client1.end();
        await client2.end();
    }
}

// Demo 4: 查看事务快照
async function demoSnapshot() {
    const client = new Client(config);
    await client.connect();

    try {
        // PostgreSQL 13+
        const res = await client.query('SELECT pg_current_snapshot()');
        console.log(`[快照] pg_current_snapshot: ${res.rows[0].pg_current_snapshot}`);

        // 开启事务查看快照变化
        await client.query('BEGIN');
        const snap1 = await client.query('SELECT pg_current_snapshot()');
        console.log(`[快照-事务中] ${snap1.rows[0].pg_current_snapshot}`);
        await client.query('COMMIT');
    } finally {
        await client.end();
    }
}

async function main() {
    console.log('=== Demo 1: READ COMMITTED 更新丢失 ===');
    await demoLostUpdateRC();

    console.log('\n=== Demo 2: 正确原子更新 ===');
    await demoCorrectUpdate();

    console.log('\n=== Demo 3: REPEATABLE READ 冲突检测 ===');
    await demoRRConflict();

    console.log('\n=== Demo 4: 事务快照 ===');
    await demoSnapshot();
}

main().catch(console.error);

对应的PostgreSQL原生SQL命令

sql 复制代码
-- 创建测试表
CREATE TABLE accounts (id INT PRIMARY KEY, name VARCHAR(50), amount INT);
INSERT INTO accounts VALUES (1, 'alice', 1000);

-- READ COMMITTED 更新丢失复现
-- 窗口1
BEGIN;
SELECT amount FROM accounts WHERE id = 1;  -- 1000
UPDATE accounts SET amount = 1100 WHERE id = 1;
COMMIT;

-- 窗口2(并发)
BEGIN;
SELECT amount FROM accounts WHERE id = 1;  -- 1000
UPDATE accounts SET amount = 1100 WHERE id = 1;
COMMIT;
-- 结果: 1100

-- 正确原子更新
BEGIN;
UPDATE accounts SET amount = amount + 100 WHERE id = 1;
COMMIT;

-- REPEATABLE READ 冲突检测
-- 窗口1
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT amount FROM accounts WHERE id = 1;
UPDATE accounts SET amount = 1100 WHERE id = 1;
COMMIT;

-- 窗口2
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT amount FROM accounts WHERE id = 1;
UPDATE accounts SET amount = 1100 WHERE id = 1;
-- 等待后报错: ERROR: could not serialize access due to concurrent update

-- 查看快照
SELECT pg_current_snapshot();

技术点总结

  • READ COMMITTED 更新丢失:演示了应用层读取后计算再更新导致的丢失更新问题
  • 原子更新 :使用 amount = amount + 100 避免更新丢失
  • REPEATABLE READ 冲突检测:展示快照隔离下并发更新的冲突报错机制
  • 快照查看 :使用 pg_current_snapshot() 观察事务快照

多语言示例

Go语言示例

使用 pgx 驱动(推荐)或 lib/pq,这里采用 pgx/v5

go 复制代码
package main

import (
    "context"
    "fmt"
    "log"

    "github.com/jackc/pgx/v5"
)

func main() {
    ctx := context.Background()

    // 连接配置
    connStr := "postgres://postgres:your_password@localhost:5432/testdb"
    conn, err := pgx.Connect(ctx, connStr)
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close(ctx)

    // 准备测试表
    setup := `
        DROP TABLE IF EXISTS accounts;
        CREATE TABLE accounts (id INT PRIMARY KEY, name VARCHAR(50), amount INT);
        INSERT INTO accounts VALUES (1, 'alice', 1000);
    `
    if _, err := conn.Exec(ctx, setup); err != nil {
        log.Fatal(err)
    }
    fmt.Println("[Setup] 表已创建,初始余额: 1000")

    // Demo 1: READ COMMITTED 更新丢失
    fmt.Println("\n=== Demo 1: READ COMMITTED 更新丢失 ===")
    demoLostUpdateRC(ctx, conn)

    // Demo 2: 正确原子更新
    fmt.Println("\n=== Demo 2: 正确原子更新 ===")
    demoCorrectUpdate(ctx, conn)

    // Demo 3: REPEATABLE READ 冲突检测
    fmt.Println("\n=== Demo 3: REPEATABLE READ 冲突检测 ===")
    demoRRConflict(ctx, conn)

    // Demo 4: 快照查看
    fmt.Println("\n=== Demo 4: 快照查看 ===")
    demoSapshot(ctx, conn)
}

func demoLostUpdateRC(ctx context.Context, conn *pgx.Conn) {
    // 重置表数据
    conn.Exec(ctx, "TRUNCATE accounts; INSERT INTO accounts VALUES (1, 'alice', 1000)")

    // 使用两个独立连接模拟并发
    conn1, _ := pgx.Connect(ctx, conn.Config().ConnString())
    defer conn1.Close(ctx)
    conn2, _ := pgx.Connect(ctx, conn.Config().ConnString())
    defer conn2.Close(ctx)

    // T1 读取
    tx1, _ := conn1.Begin(ctx)
    var balance1 int
    tx1.QueryRow(ctx, "SELECT amount FROM accounts WHERE id = 1").Scan(&balance1)
    fmt.Printf("[T1] 读取余额: %d\n", balance1)

    // T2 读取(在T1提交前)
    tx2, _ := conn2.Begin(ctx)
    var balance2 int
    tx2.QueryRow(ctx, "SELECT amount FROM accounts WHERE id = 1").Scan(&balance2)
    fmt.Printf("[T2] 读取余额: %d\n", balance2)

    // T1 更新并提交
    _, _ = tx1.Exec(ctx, "UPDATE accounts SET amount = $1 WHERE id = 1", balance1+100)
    tx1.Commit(ctx)
    fmt.Printf("[T1] 提交更新: %d -> %d\n", balance1, balance1+100)

    // T2 更新(此时T1已提交,在READ COMMITTED下会读到新值)
    _, _ = tx2.Exec(ctx, "UPDATE accounts SET amount = $1 WHERE id = 1", balance2+100)
    tx2.Commit(ctx)
    fmt.Printf("[T2] 提交更新: %d -> %d\n", balance2, balance2+100)

    var final int
    conn.QueryRow(ctx, "SELECT amount FROM accounts WHERE id = 1").Scan(&final)
    fmt.Printf("[结果] 最终余额: %d (预期: 1200, 实际: %d)\n", final, final)
    // 输出: 1100
}

func demoCorrectUpdate(ctx context.Context, conn *pgx.Conn) {
    conn.Exec(ctx, "TRUNCATE accounts; INSERT INTO accounts VALUES (1, 'alice', 1000)")

    tx, _ := conn.Begin(ctx)
    _, _ = tx.Exec(ctx, "UPDATE accounts SET amount = amount + 100 WHERE id = 1")
    tx.Commit(ctx)

    var final int
    conn.QueryRow(ctx, "SELECT amount FROM accounts WHERE id = 1").Scan(&final)
    fmt.Printf("[正确方式] 最终余额: %d (预期: 1100)\n", final)
}

func demoRRConflict(ctx context.Context, conn *pgx.Conn) {
    conn.Exec(ctx, "TRUNCATE accounts; INSERT INTO accounts VALUES (1, 'alice', 1000)")

    conn1, _ := pgx.Connect(ctx, conn.Config().ConnString())
    defer conn1.Close(ctx)
    conn2, _ := pgx.Connect(ctx, conn.Config().ConnString())
    defer conn2.Close(ctx)

    tx1, _ := conn1.Begin(ctx)
    tx1.Exec(ctx, "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ")
    var b1 int
    tx1.QueryRow(ctx, "SELECT amount FROM accounts WHERE id = 1").Scan(&b1)
    fmt.Printf("[RR-T1] 读取余额: %d\n", b1)

    tx2, _ := conn2.Begin(ctx)
    tx2.Exec(ctx, "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ")
    var b2 int
    tx2.QueryRow(ctx, "SELECT amount FROM accounts WHERE id = 1").Scan(&b2)
    fmt.Printf("[RR-T2] 读取余额: %d\n", b2)

    // T1 更新并提交
    tx1.Exec(ctx, "UPDATE accounts SET amount = $1 WHERE id = 1", b1+100)
    tx1.Commit(ctx)
    fmt.Println("[RR-T1] 提交成功")

    // T2 尝试更新,应报错
    _, err := tx2.Exec(ctx, "UPDATE accounts SET amount = $1 WHERE id = 1", b2+100)
    if err != nil {
        tx2.Rollback(ctx)
        fmt.Printf("[RR-T2] 预期错误: %v\n", err)
    } else {
        tx2.Commit(ctx)
        fmt.Println("[RR-T2] 提交成功(不应发生)")
    }
}

func demoSapshot(ctx context.Context, conn *pgx.Conn) {
    var snap string
    conn.QueryRow(ctx, "SELECT pg_current_snapshot()").Scan(&snap)
    fmt.Printf("[快照] pg_current_snapshot: %s\n", snap)

    tx, _ := conn.Begin(ctx)
    var snap2 string
    tx.QueryRow(ctx, "SELECT pg_current_snapshot()").Scan(&snap2)
    fmt.Printf("[快照-事务中] %s\n", snap2)
    tx.Commit(ctx)
}

运行说明

  1. 安装Go 1.18+,依赖 github.com/jackc/pgx/v5
  2. 执行 go mod init demo && go get github.com/jackc/pgx/v5
  3. 修改连接串,运行 go run main.go

Python语言示例

使用 psycopg2(或 asyncpg,这里采用同步 psycopg2)。

python 复制代码
import psycopg2
import psycopg2.extras

def main():
    conn_str = "dbname=testdb user=postgres password=your_password host=localhost port=5432"
    conn = psycopg2.connect(conn_str)
    conn.autocommit = True
    cur = conn.cursor()

    # 准备测试表
    cur.execute("DROP TABLE IF EXISTS accounts")
    cur.execute("CREATE TABLE accounts (id INT PRIMARY KEY, name VARCHAR(50), amount INT)")
    cur.execute("INSERT INTO accounts VALUES (1, 'alice', 1000)")
    print("[Setup] 表已创建,初始余额: 1000")

    print("\n=== Demo 1: READ COMMITTED 更新丢失 ===")
    demo_lost_update_rc(conn_str)

    print("\n=== Demo 2: 正确原子更新 ===")
    demo_correct_update(conn_str)

    print("\n=== Demo 3: REPEATABLE READ 冲突检测 ===")
    demo_rr_conflict(conn_str)

    print("\n=== Demo 4: 快照查看 ===")
    demo_snapshot(conn_str)

    cur.close()
    conn.close()

def demo_lost_update_rc(conn_str):
    # 重置表
    conn1 = psycopg2.connect(conn_str)
    conn1.autocommit = True
    c1 = conn1.cursor()
    c1.execute("TRUNCATE accounts; INSERT INTO accounts VALUES (1, 'alice', 1000)")
    c1.close()

    # 两个连接模拟并发
    conn1 = psycopg2.connect(conn_str)
    conn2 = psycopg2.connect(conn_str)
    c1 = conn1.cursor()
    c2 = conn2.cursor()

    # T1
    conn1.autocommit = False
    c1.execute("BEGIN")
    c1.execute("SELECT amount FROM accounts WHERE id = 1")
    balance1 = c1.fetchone()[0]
    print(f"[T1] 读取余额: {balance1}")

    # T2
    conn2.autocommit = False
    c2.execute("BEGIN")
    c2.execute("SELECT amount FROM accounts WHERE id = 1")
    balance2 = c2.fetchone()[0]
    print(f"[T2] 读取余额: {balance2}")

    # T1 更新提交
    c1.execute("UPDATE accounts SET amount = %s WHERE id = 1", (balance1 + 100,))
    conn1.commit()
    print(f"[T1] 提交更新: {balance1} -> {balance1 + 100}")

    # T2 更新(READ COMMITTED下读到新值)
    c2.execute("UPDATE accounts SET amount = %s WHERE id = 1", (balance2 + 100,))
    conn2.commit()
    print(f"[T2] 提交更新: {balance2} -> {balance2 + 100}")

    c3 = conn1.cursor()
    c3.execute("SELECT amount FROM accounts WHERE id = 1")
    final = c3.fetchone()[0]
    print(f"[结果] 最终余额: {final} (预期: 1200, 实际: {final})")
    c1.close(); c2.close(); c3.close()
    conn1.close(); conn2.close()

def demo_correct_update(conn_str):
    conn = psycopg2.connect(conn_str)
    conn.autocommit = False
    cur = conn.cursor()
    cur.execute("TRUNCATE accounts; INSERT INTO accounts VALUES (1, 'alice', 1000)")
    cur.execute("BEGIN")
    cur.execute("UPDATE accounts SET amount = amount + 100 WHERE id = 1")
    conn.commit()
    cur.execute("SELECT amount FROM accounts WHERE id = 1")
    final = cur.fetchone()[0]
    print(f"[正确方式] 最终余额: {final} (预期: 1100)")
    cur.close(); conn.close()

def demo_rr_conflict(conn_str):
    conn1 = psycopg2.connect(conn_str)
    conn2 = psycopg2.connect(conn_str)
    c1 = conn1.cursor()
    c2 = conn2.cursor()

    conn1.autocommit = False
    conn2.autocommit = False

    # 重置
    c1.execute("TRUNCATE accounts; INSERT INTO accounts VALUES (1, 'alice', 1000)")
    conn1.commit()

    c1.execute("BEGIN")
    c1.execute("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ")
    c1.execute("SELECT amount FROM accounts WHERE id = 1")
    b1 = c1.fetchone()[0]
    print(f"[RR-T1] 读取余额: {b1}")

    c2.execute("BEGIN")
    c2.execute("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ")
    c2.execute("SELECT amount FROM accounts WHERE id = 1")
    b2 = c2.fetchone()[0]
    print(f"[RR-T2] 读取余额: {b2}")

    c1.execute("UPDATE accounts SET amount = %s WHERE id = 1", (b1 + 100,))
    conn1.commit()
    print("[RR-T1] 提交成功")

    try:
        c2.execute("UPDATE accounts SET amount = %s WHERE id = 1", (b2 + 100,))
        conn2.commit()
        print("[RR-T2] 提交成功(不应发生)")
    except psycopg2.errors.SerializationFailure as e:
        conn2.rollback()
        print(f"[RR-T2] 预期错误: {e}")

    c1.close(); c2.close()
    conn1.close(); conn2.close()

def demo_snapshot(conn_str):
    conn = psycopg2.connect(conn_str)
    cur = conn.cursor()
    cur.execute("SELECT pg_current_snapshot()")
    snap = cur.fetchone()[0]
    print(f"[快照] pg_current_snapshot: {snap}")

    cur.execute("BEGIN")
    cur.execute("SELECT pg_current_snapshot()")
    snap2 = cur.fetchone()[0]
    print(f"[快照-事务中] {snap2}")
    cur.execute("COMMIT")
    cur.close(); conn.close()

if __name__ == "__main__":
    main()

运行说明

  1. 安装Python 3.8+,依赖 psycopg2-binarypip install psycopg2-binary
  2. 修改连接参数,运行 python demo.py

Java语言示例

使用JDBC(PostgreSQL驱动)和HikariCP(可选,这里简单用DriverManager)。

java 复制代码
import java.sql.*;

public class IsolationDemo {
    static final String URL = "jdbc:postgresql://localhost:5432/testdb";
    static final String USER = "postgres";
    static final String PASSWORD = "your_password";

    public static void main(String[] args) throws SQLException {
        // 初始化表
        try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
             Statement stmt = conn.createStatement()) {
            stmt.execute("DROP TABLE IF EXISTS accounts");
            stmt.execute("CREATE TABLE accounts (id INT PRIMARY KEY, name VARCHAR(50), amount INT)");
            stmt.execute("INSERT INTO accounts VALUES (1, 'alice', 1000)");
            System.out.println("[Setup] 表已创建,初始余额: 1000");
        }

        System.out.println("\n=== Demo 1: READ COMMITTED 更新丢失 ===");
        demoLostUpdateRC();

        System.out.println("\n=== Demo 2: 正确原子更新 ===");
        demoCorrectUpdate();

        System.out.println("\n=== Demo 3: REPEATABLE READ 冲突检测 ===");
        demoRRConflict();

        System.out.println("\n=== Demo 4: 快照查看 ===");
        demoSapshot();
    }

    static void demoLostUpdateRC() throws SQLException {
        try (Connection conn1 = DriverManager.getConnection(URL, USER, PASSWORD);
             Connection conn2 = DriverManager.getConnection(URL, USER, PASSWORD)) {
            // 重置
            try (Statement stmt = conn1.createStatement()) {
                stmt.execute("TRUNCATE accounts; INSERT INTO accounts VALUES (1, 'alice', 1000)");
            }

            conn1.setAutoCommit(false);
            conn2.setAutoCommit(false);

            // T1 读取
            int balance1;
            try (Statement stmt = conn1.createStatement();
                 ResultSet rs = stmt.executeQuery("SELECT amount FROM accounts WHERE id = 1")) {
                rs.next();
                balance1 = rs.getInt(1);
                System.out.println("[T1] 读取余额: " + balance1);
            }

            // T2 读取
            int balance2;
            try (Statement stmt = conn2.createStatement();
                 ResultSet rs = stmt.executeQuery("SELECT amount FROM accounts WHERE id = 1")) {
                rs.next();
                balance2 = rs.getInt(1);
                System.out.println("[T2] 读取余额: " + balance2);
            }

            // T1 更新提交
            try (Statement stmt = conn1.createStatement()) {
                stmt.executeUpdate("UPDATE accounts SET amount = " + (balance1 + 100) + " WHERE id = 1");
            }
            conn1.commit();
            System.out.println("[T1] 提交更新: " + balance1 + " -> " + (balance1 + 100));

            // T2 更新(READ COMMITTED)
            try (Statement stmt = conn2.createStatement()) {
                stmt.executeUpdate("UPDATE accounts SET amount = " + (balance2 + 100) + " WHERE id = 1");
            }
            conn2.commit();
            System.out.println("[T2] 提交更新: " + balance2 + " -> " + (balance2 + 100));

            // 查询结果
            try (Statement stmt = conn1.createStatement();
                 ResultSet rs = stmt.executeQuery("SELECT amount FROM accounts WHERE id = 1")) {
                rs.next();
                int finalBal = rs.getInt(1);
                System.out.printf("[结果] 最终余额: %d (预期: 1200, 实际: %d)\n", finalBal, finalBal);
            }
        }
    }

    static void demoCorrectUpdate() throws SQLException {
        try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
             Statement stmt = conn.createStatement()) {
            stmt.execute("TRUNCATE accounts; INSERT INTO accounts VALUES (1, 'alice', 1000)");
            conn.setAutoCommit(false);
            stmt.executeUpdate("UPDATE accounts SET amount = amount + 100 WHERE id = 1");
            conn.commit();
            try (ResultSet rs = stmt.executeQuery("SELECT amount FROM accounts WHERE id = 1")) {
                rs.next();
                int finalBal = rs.getInt(1);
                System.out.println("[正确方式] 最终余额: " + finalBal + " (预期: 1100)");
            }
        }
    }

    static void demoRRConflict() throws SQLException {
        try (Connection conn1 = DriverManager.getConnection(URL, USER, PASSWORD);
             Connection conn2 = DriverManager.getConnection(URL, USER, PASSWORD)) {
            // 重置
            try (Statement stmt = conn1.createStatement()) {
                stmt.execute("TRUNCATE accounts; INSERT INTO accounts VALUES (1, 'alice', 1000)");
            }

            conn1.setAutoCommit(false);
            conn2.setAutoCommit(false);

            // T1 RR
            try (Statement stmt = conn1.createStatement()) {
                stmt.execute("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ");
                ResultSet rs = stmt.executeQuery("SELECT amount FROM accounts WHERE id = 1");
                rs.next();
                int b1 = rs.getInt(1);
                System.out.println("[RR-T1] 读取余额: " + b1);
                stmt.executeUpdate("UPDATE accounts SET amount = " + (b1 + 100) + " WHERE id = 1");
            }
            conn1.commit();
            System.out.println("[RR-T1] 提交成功");

            // T2 RR 尝试更新
            try (Statement stmt = conn2.createStatement()) {
                stmt.execute("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ");
                ResultSet rs = stmt.executeQuery("SELECT amount FROM accounts WHERE id = 1");
                rs.next();
                int b2 = rs.getInt(1);
                System.out.println("[RR-T2] 读取余额: " + b2);
                stmt.executeUpdate("UPDATE accounts SET amount = " + (b2 + 100) + " WHERE id = 1");
            }
            conn2.commit();
            System.out.println("[RR-T2] 提交成功(不应发生)");
        } catch (SQLException e) {
            if (e.getSQLState().equals("40001")) { // serialization failure
                System.out.println("[RR-T2] 预期错误: " + e.getMessage());
            } else {
                throw e;
            }
        }
    }

    static void demoSapshot() throws SQLException {
        try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
             Statement stmt = conn.createStatement()) {
            try (ResultSet rs = stmt.executeQuery("SELECT pg_current_snapshot()")) {
                rs.next();
                System.out.println("[快照] pg_current_snapshot: " + rs.getString(1));
            }
            conn.setAutoCommit(false);
            try (ResultSet rs = stmt.executeQuery("SELECT pg_current_snapshot()")) {
                rs.next();
                System.out.println("[快照-事务中] " + rs.getString(1));
            }
            conn.commit();
        }
    }
}

运行说明

  1. JDK 11+,添加PostgreSQL JDBC驱动依赖(如 org.postgresql:postgresql:42.7.2
  2. 编译:javac IsolationDemo.java
  3. 运行(需把驱动jar加入classpath):java IsolationDemo

多语言对比

维度 Node.js (pg) Go (pgx) Python (psycopg2) Java (JDBC)
驱动/库 pg pgx/v5 psycopg2 PostgreSQL JDBC Driver
连接方式 回调/async/await 上下文context + 连接池 同步阻塞 同步阻塞
事务控制 client.query('BEGIN')client.query 自动事务 Begin(ctx) 返回 Tx connection.autocommit=FalseBEGIN connection.setAutoCommit(false)
隔离级别设置 SQL语句 SET TRANSACTION SQL语句 SET TRANSACTION SQL语句 SET TRANSACTION stmt.execute("SET TRANSACTION ...")
错误处理 try/catch 返回error try/except try/catch (SQLException)
类型映射 自动映射为JS类型 扫描到变量 自动转换Python类型 通过 ResultSet.getXxx()
连接池支持 内置 Pool pgxpool psycopg2.pool HikariCP / 内置
性能特点 事件驱动,高并发 协程,高吞吐 同步,适中 同步,连接池优化
适用场景 Web后端,高I/O 微服务,云原生 数据科学,脚本 企业级应用
对应PostgreSQL原生SQL示例 与SQL直接对应 与SQL直接对应 与SQL直接对应 与SQL直接对应

核心行为一致性:所有语言示例均实现了相同的四个Demo:

  1. READ COMMITTED更新丢失:展示"读取-计算-写入"模式下的丢失更新。
  2. 正确原子更新 :使用 amount = amount + 100 避免更新丢失。
  3. REPEATABLE READ冲突检测:展示并发更新时序列化冲突报错。
  4. 查看事务快照 :调用 pg_current_snapshot()

官方文档

参考链接

总结

本文系统梳理了PostgreSQL事务隔离级别的完整知识体系。PostgreSQL基于MVCC架构通过事务快照实现并发控制,内部提供三种隔离级别:READ COMMITTED(默认)、REPEATABLE READSERIALIZABLE,其中READ UNCOMMITTED在行为上等价于READ COMMITTED。各隔离级别在脏读、不可重复读、幻读和序列化异常方面的表现可参照官方表13.1。

关键实践要点:

  1. READ COMMITTED下需警惕应用层更新丢失 ,应使用原子更新(如 SET amount = amount + delta)替代"读取-计算-写入"模式。
  2. REPEATABLE READ通过快照复用保证可重复读,但并发更新冲突时会抛出 could not serialize access due to concurrent update 错误。
  3. SERIALIZABLE基于SSI提供最高级别保证,但需处理 serialization_failure 错误并实现重试逻辑。
  4. 快照查看使用 pg_current_snapshot()(PG 13+)或已废弃的 txid_current_snapshot()(PG 12-)。

在实际应用中,READ COMMITTED 作为默认级别适用于大多数场景,兼顾性能与一致性;备份等需要一致视图的场景使用 REPEATABLE READ;对数据一致性要求极高的金融类业务可考虑 SERIALIZABLE,但需评估性能开销。

相关推荐
可涵不会debug1 小时前
LangChain 示例选择器(Example selectors)完整基础概念解读
服务器·前端·数据库
_Narcissus_1 小时前
B树概念及操作笔记(含完整代码实现)
c语言·数据结构·数据库·c++·笔记·b树·算法
l1258651 小时前
# RAG多轮对话检索设计:Query重写如何让“那它呢“变成完整问题
前端·数据库·人工智能·python·算法·fastapi·milvus
小弥儿1 小时前
GitHub今日热榜 | 2026-08-20:Agent 上下文数据库接棒
数据库·学习·开源·github
sunoo-2291 小时前
【C 语言标准 IO 入门】第二天学习笔记:文件操作核心函数 + 实战案例 + 踩坑合集
linux·笔记·vscode·学习
疯狂打码的少年2 小时前
【数据结构】图的应用:最短路径(Dijkstra / Floyd)
数据结构·笔记·算法
倔强的石头_2 小时前
向量数据库从相似度检索走向融合数据底座
数据库
Fa_Mian_Tuan2 小时前
图论基础 | 邻接多重表原理剖析 + 完整可运行C语言代码(插边/删边/释放)
c语言·数据结构·笔记·算法·图论
Eloudy2 小时前
Ubuntu 22.04 从源码编译安装 ROS 2 Jazzy 笔记
linux·笔记·ubuntu