
👋 大家好,欢迎来到我的技术博客! 📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。 🎯 本文将围绕PostgreSQL这个话题展开,希望能为你带来一些启发或实用的参考。 🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!
文章目录
- PostgreSQL – 事务进阶:隔离级别与设置方法
-
- 什么是事务?ACID 原则回顾 🧱
- SQL 标准定义的四种隔离级别 📜
- PostgreSQL 的 MVCC 机制:隔离性的基石 🧬
- PostgreSQL 支持的隔离级别详解 🔍
-
- 1. READ COMMITTED(默认级别)
-
- 示例场景
- 2. REPEATABLE READ
-
- 示例场景
- 3. SERIALIZABLE
-
- 何时需要 SERIALIZABLE?
- 在 PostgreSQL 中设置隔离级别 🛠️
-
- 方法一:SQL 语句设置
-
- 1. 设置当前会话的默认隔离级别
- 2. 在事务开始时指定
- 方法二:通过 JDBC 设置(Java 应用)
-
- Java 中的隔离级别常量
- 方法三:通过 Spring Framework 设置
- Java 代码实战:模拟并发问题与隔离效果 🧪
-
- 场景:银行转账
-
- 数据库准备
- Java 代码
- 运行结果分析
- 隔离级别与性能权衡 ⚖️
- 常见误区与最佳实践 🚫✅
-
- 误区 1:认为 REPEATABLE READ 不能防止幻读
- 误区 2:在事务中途设置隔离级别
- 最佳实践 1:显式声明隔离级别
- 最佳实践 2:处理序列化失败
- 连接池中的隔离级别设置 🏊♂️
- 监控与调试事务行为 🔍
-
- 1. 查看当前事务隔离级别
- 2. 查看活跃事务
- 3. 使用 `pg_blocking_pids()`
- 4. 日志记录
- 高级话题:SSI 与真正的可串行化 🧠
- 总结:如何选择合适的隔离级别? 🎯
PostgreSQL – 事务进阶:隔离级别与设置方法
在现代应用开发中,数据库事务是保障数据一致性和可靠性的核心机制。PostgreSQL 作为一款功能强大、开源且高度可扩展的关系型数据库,其事务系统设计精巧,尤其在事务隔离级别方面提供了业界领先的实现。理解并正确使用这些隔离级别,对于构建高并发、高可靠性的系统至关重要。
本文将深入探讨 PostgreSQL 的事务隔离级别,从理论基础到实际应用,结合 Java 代码示例,帮助开发者掌握如何在真实项目中合理配置和使用事务隔离,避免常见的并发问题如脏读、不可重复读和幻读。同时,我们还将介绍如何在不同层面(会话、事务、连接池)设置隔离级别,并通过 Mermaid 图表直观展示事务行为差异。
什么是事务?ACID 原则回顾 🧱
在深入隔离级别之前,我们先快速回顾事务的基本概念。
事务(Transaction) 是数据库操作的一个逻辑单元,它包含一个或多个 SQL 语句,这些语句要么全部成功执行,要么全部不执行。事务必须满足 ACID 四个特性:
- A (Atomicity) 原子性:事务是一个不可分割的工作单位,事务中的操作要么都做,要么都不做。
- C (Consistency) 一致性:事务执行前后,数据库必须保持一致性状态(如满足约束、触发器等)。
- I (Isolation) 隔离性:多个事务并发执行时,一个事务的执行不应影响其他事务。
- D (Durability) 持久性:一旦事务提交,其对数据库的修改就是永久性的,即使系统崩溃也不会丢失。
其中,隔离性(Isolation) 是最容易被误解、也最需要开发者主动干预的部分。不同的隔离级别决定了事务之间“可见性”的程度。
SQL 标准定义的四种隔离级别 📜
SQL-92 标准定义了四种事务隔离级别,按隔离强度从低到高依次为:
每种级别解决了特定的并发问题:
| READ UNCOMMITTED | ✅ 允许 | ✅ 允许 | ✅ 允许 |
| READ COMMITTED | ❌ 禁止 | ✅ 允许 | ✅ 允许 |
| REPEATABLE READ | ❌ 禁止 | ❌ 禁止 | ✅ 允许(但 PostgreSQL 特殊处理) |
| SERIALIZABLE | ❌ 禁止 | ❌ 禁止 | ❌ 禁止 |
💡 术语解释:
- 脏读:读取到另一个未提交事务修改的数据。
- 不可重复读:在同一事务中,两次读取同一行数据,结果不同(因为其他事务已提交修改)。
- 幻读:在同一事务中,两次执行相同查询,返回的行数不同(因为其他事务插入/删除了满足条件的行)。
然而,PostgreSQL 并未完全遵循标准。它没有实现 READ UNCOMMITTED,而是将其映射为 READ COMMITTED。更重要的是,PostgreSQL 的 REPEATABLE READ 实际上能防止幻读!这是因为它采用了 多版本并发控制(MVCC) 机制。
PostgreSQL 的 MVCC 机制:隔离性的基石 🧬
PostgreSQL 使用 MVCC(Multi-Version Concurrency Control) 来实现高并发下的事务隔离。其核心思想是:
每个事务看到的是数据库在某个时间点的“快照”(snapshot),而不是实时数据。
这意味着:
- 写操作不会阻塞读操作。
- 读操作不会阻塞写操作。
- 每个事务都有自己的“视图”,基于事务开始时数据库的状态。
在 MVCC 下,每一行数据都包含两个隐藏字段:
- xmin:创建该行的事务 ID
- xmax:删除该行的事务 ID(若未删除则为 0)
当一个事务读取数据时,PostgreSQL 会根据当前事务的快照判断哪些行是“可见”的。
这种机制使得 PostgreSQL 在 REPEATABLE READ 级别下就能提供类似 SERIALIZABLE 的幻读防护(尽管严格来说仍可能有极少数边缘情况,但实践中几乎等同于可串行化)。
PostgreSQL 支持的隔离级别详解 🔍
1. READ COMMITTED(默认级别)
这是 PostgreSQL 的默认隔离级别。特点如下:
- 每次执行 SQL 语句时,都会获取一个新的快照。
- 只能看到在该语句开始前已提交的事务所做的修改。
- 无法保证同一事务内多次读取同一数据的一致性。
示例场景
假设事务 A 执行以下操作:
BEGIN;
SELECT balance FROM accounts WHERE id = 1; — 返回 100
— 此时事务 B 提交了 UPDATE accounts SET balance = 200 WHERE id = 1;
SELECT balance FROM accounts WHERE id = 1; — 返回 200!
COMMIT;
第二次查询看到了事务 B 的修改,这就是不可重复读。
✅ 适用场景:大多数 Web 应用,对一致性要求不高,但需要高并发性能。
2. REPEATABLE READ
在此级别下:
- 事务在第一次读取数据时建立快照,后续所有读取都基于此快照。
- 同一事务内多次读取同一数据,结果一致。
- 防止脏读、不可重复读和幻读(得益于 MVCC)。
示例场景
事务 A:
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE id = 1; — 返回 100
— 事务 B 提交了 UPDATE accounts SET balance = 200 WHERE id = 1;
SELECT balance FROM accounts WHERE id = 1; — 仍然返回 100!
COMMIT;
即使事务 B 修改并提交了数据,事务 A 仍看到初始快照。
⚠️ 注意:如果事务 A 尝试更新已被其他事务修改的行,可能会遇到 序列化失败(serialization failure),需要重试。
✅ 适用场景:需要强一致性的报表生成、金融对账等。
3. SERIALIZABLE
这是最强的隔离级别。PostgreSQL 从 9.1 版本开始使用 Serializable Snapshot Isolation (SSI) 技术实现真正的可串行化。
- 不仅防止幻读,还能检测并阻止可能导致串行化异常的并发操作。
- 如果检测到冲突,会抛出 ERROR: could not serialize access due to concurrent update,要求应用重试。
何时需要 SERIALIZABLE?
只有在 REPEATABLE READ 无法满足业务逻辑时才使用。例如:
- 多个事务同时检查“余额是否足够”并扣款,可能导致超支。
- 复杂的多表一致性检查。
由于性能开销较大,不建议默认使用。
在 PostgreSQL 中设置隔离级别 🛠️
方法一:SQL 语句设置
1. 设置当前会话的默认隔离级别
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ COMMITTED;
此后,所有新开启的事务都将使用此级别(除非显式指定)。
2. 在事务开始时指定
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
— 或直接
BEGIN ISOLATION LEVEL REPEATABLE READ;
⚠️ 必须在事务中第一条 SQL 语句之前设置,否则会报错。
方法二:通过 JDBC 设置(Java 应用)
在 Java 应用中,通常通过 Connection 对象设置隔离级别。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class PostgresIsolationExample {
public static void main(String[] args) throws SQLException {
String url = "jdbc:postgresql://localhost:5432/mydb";
String user = "user";
String password = "password";
try (Connection conn = DriverManager.getConnection(url, user, password)) {
// 设置隔离级别为 REPEATABLE READ
conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
// 开始事务
conn.setAutoCommit(false);
// 执行 SQL…
// conn.createStatement().execute("…");
conn.commit();
}
}
}
Java 中的隔离级别常量
| READ COMMITTED | TRANSACTION_READ_COMMITTED |
| REPEATABLE READ | TRANSACTION_REPEATABLE_READ |
| SERIALIZABLE | TRANSACTION_SERIALIZABLE |
❗ 注意:Java 没有 TRANSACTION_READ_UNCOMMITTED 的实际对应,PostgreSQL 会忽略它并使用 READ COMMITTED。
方法三:通过 Spring Framework 设置
在 Spring Boot 项目中,通常使用 @Transactional 注解。
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AccountService {
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// 业务逻辑
}
@Transactional(isolation = Isolation.SERIALIZABLE)
public void complexReport() {
// 生成复杂报表
}
}
Spring 会在开启事务时自动调用 Connection.setTransactionIsolation()。
💡 提示:Spring 的 Isolation 枚举与 JDBC 常量一一对应。
Java 代码实战:模拟并发问题与隔离效果 🧪
下面我们通过一个完整的 Java 示例,演示不同隔离级别下的行为差异。
场景:银行转账
有两个账户,初始余额均为 100。两个线程同时尝试从账户 1 转账 60 到账户 2。
数据库准备
CREATE TABLE accounts (
id SERIAL PRIMARY KEY,
balance NUMERIC(10, 2) NOT NULL CHECK (balance >= 0)
);
INSERT INTO accounts (id, balance) VALUES (1, 100), (2, 100);
Java 代码
import java.math.BigDecimal;
import java.sql.*;
import java.util.concurrent.CountDownLatch;
public class IsolationDemo {
private static final String URL = "jdbc:postgresql://localhost:5432/testdb";
private static final String USER = "postgres";
private static final String PASSWORD = "password";
public static void main(String[] args) throws InterruptedException {
// 初始化数据库
initDatabase();
// 测试 READ COMMITTED
System.out.println("=== Testing READ COMMITTED ===");
testWithIsolation(Connection.TRANSACTION_READ_COMMITTED);
// 重置数据
resetData();
// 测试 REPEATABLE READ
System.out.println("\\n=== Testing REPEATABLE READ ===");
testWithIsolation(Connection.TRANSACTION_REPEATABLE_READ);
}
private static void initDatabase() {
try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD)) {
conn.createStatement().executeUpdate("DROP TABLE IF EXISTS accounts");
conn.createStatement().executeUpdate(
"CREATE TABLE accounts (id SERIAL PRIMARY KEY, balance NUMERIC(10,2) NOT NULL CHECK (balance >= 0))"
);
conn.createStatement().executeUpdate("INSERT INTO accounts (id, balance) VALUES (1, 100), (2, 100)");
} catch (SQLException e) {
e.printStackTrace();
}
}
private static void resetData() {
try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD)) {
conn.createStatement().executeUpdate("UPDATE accounts SET balance = 100");
} catch (SQLException e) {
e.printStackTrace();
}
}
private static void testWithIsolation(int isolationLevel) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(2);
Thread t1 = new Thread(() -> {
try {
transfer(1, 2, new BigDecimal("60"), isolationLevel);
} catch (SQLException e) {
System.err.println("Thread 1 error: " + e.getMessage());
} finally {
latch.countDown();
}
});
Thread t2 = new Thread(() -> {
try {
transfer(1, 2, new BigDecimal("60"), isolationLevel);
} catch (SQLException e) {
System.err.println("Thread 2 error: " + e.getMessage());
} finally {
latch.countDown();
}
});
t1.start();
t2.start();
latch.await();
// 查看最终结果
try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD)) {
ResultSet rs = conn.createStatement().executeQuery("SELECT id, balance FROM accounts ORDER BY id");
while (rs.next()) {
System.out.println("Account " + rs.getInt("id") + ": " + rs.getBigDecimal("balance"));
}
} catch (SQLException e) {
e.printStackTrace();
}
}
private static void transfer(long fromId, long toId, BigDecimal amount, int isolationLevel) throws SQLException {
try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD)) {
conn.setTransactionIsolation(isolationLevel);
conn.setAutoCommit(false);
// 检查余额
PreparedStatement checkStmt = conn.prepareStatement("SELECT balance FROM accounts WHERE id = ?");
checkStmt.setLong(1, fromId);
ResultSet rs = checkStmt.executeQuery();
if (rs.next()) {
BigDecimal balance = rs.getBigDecimal("balance");
if (balance.compareTo(amount) < 0) {
throw new RuntimeException("Insufficient balance");
}
}
// 模拟处理时间(让并发更明显)
Thread.sleep(100);
// 扣款
PreparedStatement debitStmt = conn.prepareStatement("UPDATE accounts SET balance = balance – ? WHERE id = ?");
debitStmt.setBigDecimal(1, amount);
debitStmt.setLong(2, fromId);
debitStmt.executeUpdate();
// 入账
PreparedStatement creditStmt = conn.prepareStatement("UPDATE accounts SET balance = balance + ? WHERE id = ?");
creditStmt.setBigDecimal(1, amount);
creditStmt.setLong(2, toId);
creditStmt.executeUpdate();
conn.commit();
System.out.println("Transfer completed: " + fromId + " -> " + toId + " (" + amount + ")");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
}
运行结果分析
- READ COMMITTED:两个线程都可能读到余额 100,都认为足够转账,最终导致账户 1 余额为 -20(违反约束,实际会因 CHECK 约束失败而回滚,但若无约束则会出现负余额)。
- REPEATABLE READ:第一个提交的事务成功,第二个在提交时会因并发更新而失败(PostgreSQL 会检测到冲突并抛出异常),从而避免数据不一致。
✅ 这正是为什么在涉及“读-改-写”逻辑时,应使用 REPEATABLE READ 或更高隔离级别。
隔离级别与性能权衡 ⚖️
更高的隔离级别带来更强的一致性,但也可能带来性能代价:
- READ COMMITTED:性能最好,适合大多数 OLTP 场景。
- REPEATABLE READ:内存开销略大(需维护快照),但通常可接受。
- SERIALIZABLE:需要额外的锁和冲突检测,吞吐量可能下降 10%~30%,仅在必要时使用。
#mermaid-svg-URiegI3gA34LPVQQ{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-URiegI3gA34LPVQQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-URiegI3gA34LPVQQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-URiegI3gA34LPVQQ .error-icon{fill:#552222;}#mermaid-svg-URiegI3gA34LPVQQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-URiegI3gA34LPVQQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-URiegI3gA34LPVQQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-URiegI3gA34LPVQQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-URiegI3gA34LPVQQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-URiegI3gA34LPVQQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-URiegI3gA34LPVQQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-URiegI3gA34LPVQQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-URiegI3gA34LPVQQ .marker.cross{stroke:#333333;}#mermaid-svg-URiegI3gA34LPVQQ svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-URiegI3gA34LPVQQ p{margin:0;}#mermaid-svg-URiegI3gA34LPVQQ .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-URiegI3gA34LPVQQ .cluster-label text{fill:#333;}#mermaid-svg-URiegI3gA34LPVQQ .cluster-label span{color:#333;}#mermaid-svg-URiegI3gA34LPVQQ .cluster-label span p{background-color:transparent;}#mermaid-svg-URiegI3gA34LPVQQ .label text,#mermaid-svg-URiegI3gA34LPVQQ span{fill:#333;color:#333;}#mermaid-svg-URiegI3gA34LPVQQ .node rect,#mermaid-svg-URiegI3gA34LPVQQ .node circle,#mermaid-svg-URiegI3gA34LPVQQ .node ellipse,#mermaid-svg-URiegI3gA34LPVQQ .node polygon,#mermaid-svg-URiegI3gA34LPVQQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-URiegI3gA34LPVQQ .rough-node .label text,#mermaid-svg-URiegI3gA34LPVQQ .node .label text,#mermaid-svg-URiegI3gA34LPVQQ .image-shape .label,#mermaid-svg-URiegI3gA34LPVQQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-URiegI3gA34LPVQQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-URiegI3gA34LPVQQ .rough-node .label,#mermaid-svg-URiegI3gA34LPVQQ .node .label,#mermaid-svg-URiegI3gA34LPVQQ .image-shape .label,#mermaid-svg-URiegI3gA34LPVQQ .icon-shape .label{text-align:center;}#mermaid-svg-URiegI3gA34LPVQQ .node.clickable{cursor:pointer;}#mermaid-svg-URiegI3gA34LPVQQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-URiegI3gA34LPVQQ .arrowheadPath{fill:#333333;}#mermaid-svg-URiegI3gA34LPVQQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-URiegI3gA34LPVQQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-URiegI3gA34LPVQQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-URiegI3gA34LPVQQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-URiegI3gA34LPVQQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-URiegI3gA34LPVQQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-URiegI3gA34LPVQQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-URiegI3gA34LPVQQ .cluster text{fill:#333;}#mermaid-svg-URiegI3gA34LPVQQ .cluster span{color:#333;}#mermaid-svg-URiegI3gA34LPVQQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-URiegI3gA34LPVQQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-URiegI3gA34LPVQQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-URiegI3gA34LPVQQ .icon-shape,#mermaid-svg-URiegI3gA34LPVQQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-URiegI3gA34LPVQQ .icon-shape p,#mermaid-svg-URiegI3gA34LPVQQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-URiegI3gA34LPVQQ .icon-shape rect,#mermaid-svg-URiegI3gA34LPVQQ .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-URiegI3gA34LPVQQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-URiegI3gA34LPVQQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-URiegI3gA34LPVQQ :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
性能最高
平衡一致性与性能
最强一致性
READ COMMITTED
适合高并发 Web 应用
REPEATABLE READ
适合金融、报表
SERIALIZABLE
适合复杂业务规则
选择隔离级别的原则:
常见误区与最佳实践 🚫✅
误区 1:认为 REPEATABLE READ 不能防止幻读
在 PostgreSQL 中,REPEATABLE READ 能防止幻读!这是 MVCC 的优势。例如:
— 事务 A
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT COUNT(*) FROM orders WHERE status = 'pending'; — 返回 5
— 事务 B 插入新订单并提交
SELECT COUNT(*) FROM orders WHERE status = 'pending'; — 仍然返回 5
这与 MySQL InnoDB 不同(InnoDB 的 RR 级别允许幻读)。
误区 2:在事务中途设置隔离级别
BEGIN;
SELECT * FROM table;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; — ❌ 错误!会报错
必须在事务开始后、第一条 SQL 前设置。
最佳实践 1:显式声明隔离级别
即使使用默认级别,也建议在关键事务中显式声明:
@Transactional(isolation = Isolation.READ_COMMITTED)
public void updateProfile(User user) { ... }
提高代码可读性和可维护性。
最佳实践 2:处理序列化失败
在 REPEATABLE READ 或 SERIALIZABLE 下,可能遇到 SQLState: 40001(serialization failure)。应实现重试机制:
int retries = 3;
while (retries— > 0) {
try {
performTransaction();
break;
} catch (SQLException e) {
if ("40001".equals(e.getSQLState()) && retries > 0) {
Thread.sleep(100); // 短暂等待后重试
} else {
throw e;
}
}
}
Spring 也提供了 @Retryable 注解来简化此过程。
连接池中的隔离级别设置 🏊♂️
在生产环境中,通常使用连接池(如 HikariCP、Tomcat JDBC Pool)。需要注意:
- 连接池中的连接是复用的。
- 如果一个连接上次使用了 SERIALIZABLE,下次获取时可能仍处于该级别。
因此,强烈建议:
HikariCP 示例:
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost/test");
config.setUsername("user");
config.setPassword("pass");
// 不要设置 connectionInitSql 或 defaultTransactionIsolation
HikariDataSource ds = new HikariDataSource(config);
然后在业务代码中:
try (Connection conn = ds.getConnection()) {
conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
// …
}
这样确保每次事务都使用正确的隔离级别。
监控与调试事务行为 🔍
PostgreSQL 提供了多种方式监控事务:
1. 查看当前事务隔离级别
SHOW transaction_isolation;
2. 查看活跃事务
SELECT pid, usename, xact_start, query
FROM pg_stat_activity
WHERE state = 'active';
3. 使用 pg_blocking_pids()
查看阻塞当前事务的进程:
SELECT pg_blocking_pids(12345); — 12345 是目标 pid
4. 日志记录
在 postgresql.conf 中启用:
log_statement = 'all'
log_lock_waits = on
log_min_duration_statement = 1000
有助于排查死锁或长事务问题。
高级话题:SSI 与真正的可串行化 🧠
PostgreSQL 的 SERIALIZABLE 级别基于 Serializable Snapshot Isolation (SSI),由 Dan R. K. Ports 和 Kevin Grittner 在 2010 年提出。
SSI 的核心思想是:
- 允许事务并发执行,但记录读写依赖关系。
- 如果检测到可能产生非串行化结果的模式(如“危险结构”),则中止其中一个事务。
DB
T2
T1
DB
T2
T1
#mermaid-svg-BvIVZ71Aphx4X2dg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BvIVZ71Aphx4X2dg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BvIVZ71Aphx4X2dg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BvIVZ71Aphx4X2dg .error-icon{fill:#552222;}#mermaid-svg-BvIVZ71Aphx4X2dg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BvIVZ71Aphx4X2dg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BvIVZ71Aphx4X2dg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BvIVZ71Aphx4X2dg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BvIVZ71Aphx4X2dg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BvIVZ71Aphx4X2dg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BvIVZ71Aphx4X2dg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BvIVZ71Aphx4X2dg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BvIVZ71Aphx4X2dg .marker.cross{stroke:#333333;}#mermaid-svg-BvIVZ71Aphx4X2dg svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BvIVZ71Aphx4X2dg p{margin:0;}#mermaid-svg-BvIVZ71Aphx4X2dg .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-BvIVZ71Aphx4X2dg text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-BvIVZ71Aphx4X2dg .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-BvIVZ71Aphx4X2dg .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-BvIVZ71Aphx4X2dg .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-BvIVZ71Aphx4X2dg .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-BvIVZ71Aphx4X2dg #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-BvIVZ71Aphx4X2dg .sequenceNumber{fill:white;}#mermaid-svg-BvIVZ71Aphx4X2dg #sequencenumber{fill:#333;}#mermaid-svg-BvIVZ71Aphx4X2dg #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-BvIVZ71Aphx4X2dg .messageText{fill:#333;stroke:none;}#mermaid-svg-BvIVZ71Aphx4X2dg .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-BvIVZ71Aphx4X2dg .labelText,#mermaid-svg-BvIVZ71Aphx4X2dg .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-BvIVZ71Aphx4X2dg .loopText,#mermaid-svg-BvIVZ71Aphx4X2dg .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-BvIVZ71Aphx4X2dg .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-BvIVZ71Aphx4X2dg .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-BvIVZ71Aphx4X2dg .noteText,#mermaid-svg-BvIVZ71Aphx4X2dg .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-BvIVZ71Aphx4X2dg .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-BvIVZ71Aphx4X2dg .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-BvIVZ71Aphx4X2dg .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-BvIVZ71Aphx4X2dg .actorPopupMenu{position:absolute;}#mermaid-svg-BvIVZ71Aphx4X2dg .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-BvIVZ71Aphx4X2dg .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-BvIVZ71Aphx4X2dg .actor-man circle,#mermaid-svg-BvIVZ71Aphx4X2dg line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-BvIVZ71Aphx4X2dg :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
T2 被中止!
因为存在写偏斜(Write Skew)
BEGIN SERIALIZABLE
BEGIN SERIALIZABLE
SELECT * FROM accounts WHERE id=1
SELECT * FROM accounts WHERE id=1
UPDATE accounts SET balance=90 WHERE id=1
UPDATE accounts SET balance=80 WHERE id=1
COMMIT
COMMIT
📚 想深入了解 SSI?推荐阅读 PostgreSQL 官方文档 – Serializable Isolation
总结:如何选择合适的隔离级别? 🎯
| 普通 CRUD 操作 | READ COMMITTED | 默认,性能好 |
| 生成报表、统计 | REPEATABLE READ | 保证多次查询结果一致 |
| 金融转账、库存扣减 | REPEATABLE READ | 防止超卖、超支 |
| 复杂业务规则(如“总和约束”) | SERIALIZABLE | 防止写偏斜等高级异常 |
记住:
- 不要盲目使用最高隔离级别。
- 理解你的业务需求:是否真的需要防止幻读?
- 测试并发场景:使用 JUnit + Testcontainers 模拟多线程访问。
- 监控生产环境:关注序列化失败率。
PostgreSQL 的事务系统是其强大之处。掌握隔离级别,你就能在一致性与性能之间找到最佳平衡点,构建出既可靠又高效的系统。
🌐 延伸阅读:
- PostgreSQL 官方文档 – 事务隔离
- MVCC in PostgreSQL — Explained
- Serializable Isolation for Snapshot Databases (论文)
Happy coding! 🚀
🙌 感谢你读到这里! 🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。 💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友! 💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿 🔔 关注我,不错过下一篇干货!我们下期再见!✨


