PostgreSQL 教程

数据库并发与事务隔离级别

在数据库系统中,事务是维护数据完整性的基础,尤其是在多个用户或应用程序并发访问和修改数据时。隔离级别(Isolation levels) 定义了事务在多大程度上免受其他并发事务操作的影响。

本章将带你深入探索三种最常见的隔离级别:读已提交(Read Committed)、可重复读(Repeatable Read)和可串行化(Serializable)。我们将解释它们的特性、潜在问题以及它们如何影响数据一致性。

1. 理解隔离级别

隔离级别控制着并发事务之间相互隔离的程度。较高的隔离级别能提供更强的保护,防止并发相关的问题,但同时也会降低并发性能,并增加数据库系统的开销。每种隔离级别都在“数据一致性”和“系统性能”之间提供了一种不同的权衡方案。让我们逐一详细剖析。

2. 读已提交 (Read Committed)

读已提交(Read Committed) 是 PostgreSQL 等许多数据库的默认隔离级别。它保证一个事务只能读取到其他事务已经提交的数据。这意味着事务永远不会看到其他事务未提交的“半成品”数据,从而彻底防止了“脏读(Dirty Reads)”。

核心特性:

  • 防止脏读: 事务只能读取已提交的数据。
  • 可能发生不可重复读(Non-Repeatable Reads): 如果一个事务两次读取同一行数据,在此期间如果另一个事务修改了该行并提交,那么这两次读取的结果可能会不同。
  • 可能发生幻读(Phantom Reads): 事务可能会看到其他事务新插入并提交的、符合其查询条件的“幽灵”数据行。

2.1 案例演示

想象有两个并发事务 T1 和 T2,它们都在操作一个 products(产品)表。该表包含 idnameprice

  • 初始状态: products 表中有一个产品:id = 1, name = 'Laptop', price = 1200

步骤拆解:

  1. 事务 T1: 开启事务,读取 id = 1 的产品价格,得到 1200。
  2. 事务 T2: 开启事务,将 id = 1 的产品价格更新为 1300,并提交事务。
  3. 事务 T1: 再次读取 id = 1 的产品价格,此时得到了 1300。T1 继续使用这个更新后的价格进行后续操作。

在这个场景中,T1 经历了不可重复读,因为在它的两次读取操作之间,由于 T2 提交了修改,产品价格发生了变化。

2.2 SQL 场景实战

-- 会话 1 (事务 T1)
BEGIN;
SELECT price FROM products WHERE id = 1; -- 返回 1200

-- (上下文切换至 会话 2)
-- 会话 2 (事务 T2)
BEGIN;
UPDATE products SET price = 1300 WHERE id = 1;
COMMIT;

-- (上下文切换回 会话 1)
SELECT price FROM products WHERE id = 1; -- 返回 1300 (发生不可重复读)
COMMIT;

解析: 事务 T1 启动并读取初始价格(1200)。随后,事务 T2 将价格更新为 1300 并提交。当 T1 再次读取价格时,它看到了更新后的值(1300),这就完美演示了什么叫“不可重复读”。

3. 可重复读 (Repeatable Read)

可重复读(Repeatable Read) 提供了比“读已提交”更强的保证。它确保在一个事务内,任何被读取的数据在整个事务期间如果再次被读取,其值都不会发生改变。这成功阻止了不可重复读。然而,在标准 SQL 定义中,幻读依然可能发生。

核心特性:

  • 防止脏读: 同样防止读取未提交的数据。
  • 防止不可重复读: 如果事务读取了一行数据,那么在该事务期间再次读取该行时,无论其他已提交的事务做了什么修改,它看到的始终是相同的数据。
  • 可能发生幻读: 事务仍然可能会看到其他事务插入的、符合搜索条件的新数据行。

3.1 案例演示

考虑两个并发事务 T1 和 T2,操作 employees(员工)表,包含 idnamesalary

  • 初始状态: employees 表中有一个员工:id = 1, name = 'Alice', salary = 60000

步骤拆解:

  1. 事务 T1:Repeatable Read 隔离级别开启事务。读取 id = 1 的员工薪水,得到 60000。
  2. 事务 T2: 开启事务。将 id = 1 的员工薪水更新为 65000 并提交。
  3. 事务 T1: 再次读取 id = 1 的员工薪水,依然得到 60000。T1 使用最初读取的薪水继续执行。

在这个例子中,T1 没有经历不可重复读。因为 Repeatable Read 隔离级别确保了它在整个事务期间看到了数据的一致视图,即使 T2 已经提交了对薪水的修改。

3.2 幻读案例与 SQL 实战

  • 初始状态: employees 表最初有 5 名员工。
-- 会话 1 (事务 T1)
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT salary FROM employees WHERE id = 1; -- 返回 60000

-- (上下文切换至 会话 2)
-- 会话 2 (事务 T2)
BEGIN;
UPDATE employees SET salary = 65000 WHERE id = 1;
COMMIT;

-- (上下文切换回 会话 1)
SELECT salary FROM employees WHERE id = 1; -- 依然返回 60000 (成功实现了可重复读)
COMMIT;


-- 幻读 (Phantom Read) 示例:
-- 会话 1 (事务 T1)
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT COUNT(*) FROM employees; -- 返回 5

-- (上下文切换至 会话 2)
-- 会话 2 (事务 T2)
BEGIN;
INSERT INTO employees (id, name, salary) VALUES (6, 'Eve', 70000);
COMMIT;

-- (上下文切换回 会话 1)
SELECT COUNT(*) FROM employees; -- 返回 6 (发生幻读)
COMMIT;

解析: 在第一部分中,T1 在可重复读级别下读取初始薪水(60000)。T2 更新薪水并提交。当 T1 再次读取时,依然是 60000。在第二部分中,T1 统计了初始员工人数(5)。接着,T2 插入了一名新员工并提交。当 T1 再次统计人数时,它看到了新员工(变为 6),这就是幻读。

4. 可串行化 (Serializable)

可串行化(Serializable) 是最高级别的隔离级别。它提供了最严格的保证,确保并发事务的执行结果,看起来就像是这些事务是一个接一个(串行)执行的。这彻底消除了脏读、不可重复读和幻读。在 PostgreSQL 中,Serializable 隔离级别通过 可序列化快照隔离 (SSI) 来实现,这比传统的基于锁的串行化允许更高的并发度。

核心特性:

  • 防止脏读: 绝对不读取未提交数据。
  • 防止不可重复读: 整个事务期间数据视图保持一致。
  • 防止幻读: 其他事务插入的新行不会影响当前事务。
  • 序列化错误 (Serialization Errors): 如果数据库检测到并发操作会破坏串行化规则,它会强制回滚其中一个事务,并抛出序列化错误。

4.1 案例与 SQL 实战

考虑 T1 和 T2 都在尝试更新 inventory(库存)表中某个产品的可用数量。

  • 初始状态: inventory 表中有一个产品 id = 1quantity = 10
-- 会话 1 (事务 T1)
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT quantity FROM inventory WHERE id = 1; -- 返回 10
-- (模拟一些处理时间,比如程序计算扣减 3 个库存,准备更新为 7)

-- (上下文切换至 会话 2)
-- 会话 2 (事务 T2)
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT quantity FROM inventory WHERE id = 1; -- 返回 10
-- (模拟一些处理时间,比如程序计算扣减 5 个库存,准备更新为 5)
UPDATE inventory SET quantity = 5 WHERE id = 1;
COMMIT;

-- (上下文切换回 会话 1)
UPDATE inventory SET quantity = 7 WHERE id = 1;
COMMIT; -- 这里可能会导致一个序列化错误 (Serialization error)

解析: 两个事务都启动并读取了初始数量(10),然后尝试基于各自的计算进行更新。因为它们都读取了相同的数据,然后试图修改它,数据库检测到了潜在的序列化异常。为了维持可串行化,数据库会允许其中一个事务提交,并强制回滚另一个事务。具体回滚哪一个,由数据库的底层并发控制机制决定。

5. 如何选择合适的隔离级别

隔离级别的选择取决于你应用程序的具体业务需求:

  • Read Committed (读已提交): 适合对高并发有强烈需求,且在业务逻辑上能够容忍偶尔出现不可重复读或幻读的应用程序。
  • Repeatable Read (可重复读): 适合需要确保在一次事务中多次读取数据保持一致的场景,同时业务可以容忍幻读现象。
  • Serializable (可串行化): 适合对数据一致性要求极其严苛(例如财务结算、高精度库存扣减),且绝对不能容忍任何并发异常的场景。但请注意,使用此级别可能会显著降低系统并发能力,并增加程序处理序列化错误重试的逻辑负担。

在实际开发中,许多应用默认使用 Read Committed,但作为开发者,你需要仔细权衡利弊,为特定业务模块选择最贴合需求的隔离级别。