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

纲要

  • 事务快照(Transaction Snapshot)
    • 快照的组成:xmin、xmax、xip_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列表,仅包含 xmin 与 xmax 之间的活跃事务。

例如,快照 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 READ 和 SERIALIZABLE :事务中第一个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 CHARACTERISTICS 和 BEGIN 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-binary:pip 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=False 或 BEGIN 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 READ和SERIALIZABLE,其中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,但需评估性能开销。

相关推荐
AOI小白新手上路6 分钟前
从 Keil 调试视角看 51 指针,到 Cortex-M 内存映射访问寄存器 · 学习笔记
笔记·学习
余槐i7 分钟前
EXPLAIN 显示走了索引查询仍慢:回表与选择性在 500 万行表上拉开 10 倍耗时
数据库·postgresql·性能优化·vacuum·explain
深度智能Ai41 分钟前
IndexTTS‑2高清音质接口对接文档
数据库
香瓜子rd1 小时前
MySQL InnoDB 并发控制核心原理:事务、隔离级别、MVCC 与锁机制
数据库·后端
Elastic 中国社区官方博客1 小时前
Elasticsearch Serverless 如何通过 hollow shards 将索引节点关闭次数降低 30%
大数据·数据库·elasticsearch·搜索引擎·serverless·全文检索
ycvv2 小时前
手机启动失败后的数据提取评估:系统状态、加密条件与文件验收
服务器·数据库·智能手机
不恋水的雨2 小时前
gbase中union all导致末尾多出空格的坑
数据库·sql·mysql
坤坤子吖3 小时前
ELF加载、动态链接、GOT与PLT
linux·笔记·学习
AI 算法大模型备案~当当3 小时前
各地备案数量怎么看:一份属地公告的认读与台账方法
java·数据库·人工智能
hgfsjk3 小时前
野莓采集图片修改指南:从单张处理到批量优化
大数据·经验分享·笔记·其他