纲要
- 事务快照(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 READSERIALIZABLEREAD 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 |
不可能 | 不可能 | 不可能 | 不可能 |
关键要点:
READ UNCOMMITTED在PostgreSQL中的行为等价于READ COMMITTED------虽然语法层面支持,但实际效果相同。- PostgreSQL的
REPEATABLE READ不允许幻读,这比SQL标准的要求更为严格(标准允许幻读)。 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 驱动演示各隔离级别行为的完整示例。
运行说明
- 确保PostgreSQL运行中,创建测试数据库
- 安装依赖:
npm install pg - 运行:
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)
}
运行说明:
- 安装Go 1.18+,依赖
github.com/jackc/pgx/v5 - 执行
go mod init demo && go get github.com/jackc/pgx/v5 - 修改连接串,运行
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()
运行说明:
- 安装Python 3.8+,依赖
psycopg2-binary:pip install psycopg2-binary - 修改连接参数,运行
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();
}
}
}
运行说明:
- JDK 11+,添加PostgreSQL JDBC驱动依赖(如
org.postgresql:postgresql:42.7.2) - 编译:
javac IsolationDemo.java - 运行(需把驱动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:
- READ COMMITTED更新丢失:展示"读取-计算-写入"模式下的丢失更新。
- 正确原子更新 :使用
amount = amount + 100避免更新丢失。 - REPEATABLE READ冲突检测:展示并发更新时序列化冲突报错。
- 查看事务快照 :调用
pg_current_snapshot()。
官方文档
- PostgreSQL 官方文档 - 第13章 并发控制
- PostgreSQL 官方文档 - 13.2. 事务隔离
- PostgreSQL 官方文档 - SET TRANSACTION
- PostgreSQL 官方文档 - 事务ID和快照信息函数
- PostgreSQL 官方文档 - BEGIN
参考链接
- PostgreSQL Wiki - Serializable Snapshot Isolation
- The Internals of PostgreSQL - Chapter 5. Concurrency Control
- PostgreSQL 中文文档 - 事务隔离
总结
本文系统梳理了PostgreSQL事务隔离级别的完整知识体系。PostgreSQL基于MVCC架构通过事务快照实现并发控制,内部提供三种隔离级别:READ COMMITTED(默认)、REPEATABLE READ和SERIALIZABLE,其中READ UNCOMMITTED在行为上等价于READ COMMITTED。各隔离级别在脏读、不可重复读、幻读和序列化异常方面的表现可参照官方表13.1。
关键实践要点:
READ COMMITTED下需警惕应用层更新丢失 ,应使用原子更新(如SET amount = amount + delta)替代"读取-计算-写入"模式。REPEATABLE READ通过快照复用保证可重复读,但并发更新冲突时会抛出could not serialize access due to concurrent update错误。SERIALIZABLE基于SSI提供最高级别保证,但需处理serialization_failure错误并实现重试逻辑。- 快照查看使用
pg_current_snapshot()(PG 13+)或已废弃的txid_current_snapshot()(PG 12-)。
在实际应用中,READ COMMITTED 作为默认级别适用于大多数场景,兼顾性能与一致性;备份等需要一致视图的场景使用 REPEATABLE READ;对数据一致性要求极高的金融类业务可考虑 SERIALIZABLE,但需评估性能开销。