WEBKT

PostgreSQL 并发控制:深入理解 MVCC、xmin/xmax 和隔离级别

60 0 0 0

为什么需要并发控制?

什么是 MVCC?

深入理解 xmin、xmax 和 ctid

MVCC 如何工作?

事务隔离级别

脏读、不可重复读和幻读

PostgreSQL 中的隔离级别

Read Committed(读已提交)

Repeatable Read(可重复读)

Serializable(串行化)

如何避免并发问题

总结

你好!今天咱们来聊聊 PostgreSQL (PG) 数据库里一个非常核心的概念——并发控制。特别是要深入探讨一下 MVCC(多版本并发控制)、xmin/xmax 这些隐藏字段,以及不同的隔离级别下 MVCC 的行为差异。相信通过这篇文章,你能够对 PG 的并发机制有更深入的理解,并能在实际开发中更好地避免并发事务带来的问题。

为什么需要并发控制?

在数据库系统中,多个事务同时访问、修改数据是很常见的。如果没有有效的并发控制机制,就会出现各种问题,比如数据不一致、更新丢失等等。 想象一下,两个人同时修改同一行数据,如果没有并发控制,结果会怎样?

为了解决这些问题,数据库引入了事务(Transaction)的概念,并提供了一套并发控制机制来保证事务的 ACID 特性(原子性、一致性、隔离性、持久性)。

什么是 MVCC?

MVCC(Multi-Version Concurrency Control,多版本并发控制)是一种常见的并发控制方法,它通过维护数据的多个版本来实现并发访问的隔离性。简单来说,就是每个事务看到的都是数据的一个快照(Snapshot),而不是最新的数据。

PostgreSQL 正是采用了 MVCC 来实现其并发控制的。在 PG 中,每一行数据都有多个版本,每个版本都包含了创建该版本的事务 ID(xmin)和删除该版本的事务 ID(xmax)。

深入理解 xmin、xmax 和 ctid

在 PostgreSQL 中,每个数据行都有几个隐藏的系统字段,其中对理解 MVCC 最重要的就是 xminxmaxctid

  • xmin: 创建该行版本的事务 ID。当一个事务插入一行数据时,该行的 xmin 就被设置为该事务的 ID。
  • xmax: 删除该行版本的事务 ID。当一个事务删除一行数据时,该行的 xmax 就被设置为该事务的 ID。如果该行未被删除,xmax 为 0。
  • ctid: 指向该行数据物理位置的指针。它由两个部分组成:块号和块内偏移量. 一个元组的ctid在它的生命周期中可能会改变多次,比如VACUUM FULL, UPDATE操作等。

这些隐藏字段,我们可以通过安装 pageinspect 插件后进行查看。

-- 安装 pageinspect 插件
CREATE EXTENSION pageinspect;
-- 创建一个测试表
CREATE TABLE mytest (id int, name text);
-- 插入一些数据
INSERT INTO mytest VALUES (1, 'Alice');
INSERT INTO mytest VALUES (2, 'Bob');
-- 查看数据行的 xmin、xmax 和 ctid
SELECT xmin, xmax, ctid, * FROM mytest;

可以看到类似如下输出 (实际事务ID会不同):

xmin | xmax | ctid | id | name
------+------+-------+----+-------
1234 | 0 | (0,1) | 1 | Alice
1235 | 0 | (0,2) | 2 | Bob

这里 xmin 分别是 1234 和 1235,表示这两行数据分别由事务 ID 为 1234 和 1235 的事务插入。xmax 都是 0,表示这两行数据当前都有效,没有被删除。ctid 表示数据行的物理位置,例如 (0,1) 表示在第 0 个数据块的第 1 个位置。

MVCC 如何工作?

当一个事务开始时,PostgreSQL 会为其分配一个事务 ID(Transaction ID,简称 XID),并记录当前所有正在运行的事务。事务在读取数据时,会根据以下规则判断哪些版本的数据对它是可见的:

  1. 如果数据行的 xmin 小于当前事务的 XID,且 xmax 为 0 或者大于当前事务的 XID,则该版本对当前事务可见。
  2. 如果数据行的 xmin 大于或等于当前事务的 XID,或者 xmax 小于当前事务的 XID 且不为 0,则该版本对当前事务不可见。

简单理解就是,每个事务只能看到:

  • 由更早的事务创建的,且未被删除的数据。
  • 由当前事务自己创建的数据。

而看不到:

  • 由更晚的事务创建的数据。
  • 已经被更早的事务删除的数据。

这样就实现了事务间的隔离,每个事务都好像在操作一个独立的数据副本一样。

事务隔离级别

SQL 标准定义了四种事务隔离级别,不同的隔离级别对并发事务的隔离程度不同:

  1. Read Uncommitted(读未提交): 一个事务可以读取到另一个未提交事务修改的数据。可能出现脏读、不可重复读和幻读。
  2. Read Committed(读已提交): 一个事务只能读取到另一个已提交事务修改的数据。可以避免脏读,但仍可能出现不可重复读和幻读。这是 PostgreSQL 的默认隔离级别。
  3. Repeatable Read(可重复读): 一个事务在整个过程中,多次读取同一数据会得到相同的结果。可以避免脏读和不可重复读,但仍可能出现幻读。
  4. Serializable(串行化): 事务完全串行执行,可以避免脏读、不可重复读和幻读。隔离级别最高,但性能也最低。

PostgreSQL 支持这四种隔离级别,但其实现的行为与标准定义略有不同。尤其是在 Repeatable Read 级别,PostgreSQL 实际上可以避免幻读,达到了类似于 Serializable 的效果。

脏读、不可重复读和幻读

在详细讨论隔离级别之前,我们先来明确一下这几个概念:

  • 脏读(Dirty Read): 一个事务读取到了另一个事务未提交的数据。如果未提交的事务回滚,那么读取到的数据就是无效的。
  • 不可重复读(Non-Repeatable Read): 一个事务在两次读取同一数据之间,另一个事务修改并提交了该数据,导致两次读取的结果不一致。
  • 幻读(Phantom Read): 一个事务在两次读取同一范围的数据之间,另一个事务插入或删除了符合条件的数据,导致两次读取的结果集不一致。

PostgreSQL 中的隔离级别

Read Committed(读已提交)

这是 PostgreSQL 的默认隔离级别。在 Read Committed 级别下,每个查询语句都会看到一个单独的快照,该快照包含所有在该语句开始时已经提交的事务的更改。 这意味着:

  • 可以避免脏读。
  • 可能会出现不可重复读:在同一个事务中,两次相同的查询可能会返回不同的结果,因为在这两次查询之间可能有其他事务提交了对数据的修改。
  • 可能会出现幻读: 在同一个事务中, 范围查询的结果可能会随着其他事务提交了插入或删除操作而改变。

示例:

-- 事务 1
BEGIN;
SELECT * FROM mytest; -- 假设结果是 (1, 'Alice'), (2, 'Bob')
-- 事务 2 (在事务 1 的 SELECT 之后开始并提交)
BEGIN;
UPDATE mytest SET name = 'Charlie' WHERE id = 1;
COMMIT;
-- 事务 1
SELECT * FROM mytest; -- 结果可能是 (1, 'Charlie'), (2, 'Bob'),出现了不可重复读
COMMIT;

Repeatable Read(可重复读)

在 Repeatable Read 级别下,事务中的所有查询都看到同一个快照,该快照包含在事务开始时已经提交的事务的更改。这意味着:

  • 可以避免脏读。
  • 可以避免不可重复读:在同一个事务中,多次读取同一数据会得到相同的结果。
  • PostgreSQL的实现通常可以避免幻读(但SQL标准并没有要求),PostgreSQL的Repeatable Read隔离级别下,如果一个事务读取了某个范围的数据,即使其他事务插入了满足该范围条件的新数据并提交,当前事务也不会看到这些新数据。这是通过在事务开始时获取一个快照,并在整个事务期间使用这个快照来实现的。

示例:

-- 事务 1
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT * FROM mytest; -- 假设结果是 (1, 'Alice'), (2, 'Bob')
-- 事务 2 (在事务 1 的 SELECT 之后开始并提交)
BEGIN;
INSERT INTO mytest VALUES (3, 'David');
COMMIT;
-- 事务 1
SELECT * FROM mytest; -- 结果仍然是 (1, 'Alice'), (2, 'Bob'),没有幻读
COMMIT;

Serializable(串行化)

Serializable 是最高的隔离级别。在 PostgreSQL 中,Serializable 隔离级别通过“可串行化快照隔离”(Serializable Snapshot Isolation,SSI)来实现。SSI 不仅提供了 Repeatable Read 的所有保证,还防止了所有可能的并发异常,包括幻读和其他类型的异常。

如果在 Serializable 级别下检测到可能导致并发异常的情况,事务会被回滚,并抛出一个“serialization failure”错误。这意味着应用程序需要准备好在遇到这种错误时重试事务。

示例:

-- 假设 mytest 表中当前有 (1, 'Alice'), (2, 'Bob')
-- 事务 1
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT * FROM mytest WHERE id = 1;
-- 事务 2 (在事务 1 的 SELECT 之后开始)
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
UPDATE mytest SET name = 'Eve' WHERE id = 2;
-- 事务 1
UPDATE mytest SET name = 'Frank' WHERE id = 2; -- 可能会导致 serialization failure
-- 事务 2
COMMIT; -- 如果事务 1 先提交,这里可能会导致 serialization failure

在这个例子中,如果事务 1 和事务 2 的操作顺序可能导致结果与任何串行执行顺序都不一致,那么其中一个事务(通常是后提交的那个)会遇到 serialization failure 错误。

如何避免并发问题

了解了 MVCC 和隔离级别后,我们就可以更好地避免并发事务带来的问题了。以下是一些常用的方法:

  1. 选择合适的隔离级别: 根据应用的实际需求,选择合适的隔离级别。如果不需要太高的隔离级别,可以使用 Read Committed,以提高并发性能。如果需要避免不可重复读或幻读,可以使用 Repeatable Read 或 Serializable。
  2. 使用显式锁: 在某些情况下,即使使用了较高的隔离级别,也可能无法完全避免并发问题。这时可以使用显式锁(例如 SELECT ... FOR UPDATE)来锁定特定的行或表,以保证数据的一致性。
  3. 优化事务设计: 尽量缩小事务的范围,减少事务的执行时间,可以降低并发冲突的概率。
  4. 处理 serialization failure: 如果使用了 Serializable 隔离级别,需要准备好处理可能出现的 serialization failure 错误,并在必要时重试事务。
  5. 合理使用索引:正确使用索引可以大幅度提高查询性能,减少锁的竞争。
  6. 避免长事务:长时间运行的事务会持有更多的锁,增加并发冲突的可能性。 尽量将长事务拆分为多个小事务。

总结

PostgreSQL 的并发控制机制是一个复杂但非常重要的主题。通过深入理解 MVCC、xmin/xmax 这些隐藏字段,以及不同的隔离级别,我们可以更好地理解 PG 是如何处理并发事务的,从而在实际开发中避免各种并发问题。 MVCC 允许多个事务同时读取和修改数据,而不会相互阻塞,从而提高了数据库的并发性能。

希望这篇文章对你有所帮助!如果你有任何问题或想法,欢迎留言讨论。

爱编程的 PG 极客 PostgreSQLMVCC并发控制

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/7753