PostgreSQL中的事务隔离级别
MVCC 是 PostgreSQL 使用的并发控制技术之一。它的工作原理是在写入操作期间创建多个版本的数据项,同时保留旧版本。当事务读取数据项时,系统会选择适当的版本以确保该事务的隔离。MVCC 的主要优势在于它允许并发读取和写入而不会相互阻塞,另一类处理并发控制的方法是2PL(两阶段锁)及其衍生策略。
PostgreSQL 采用了一种称为快照隔离 (SI) 的 MVCC 变体。在 SI 中,写入新数据项时,旧版本将在存储中保留。然后,新版本将直接插入到相关的表格页面中。在读取操作期间,PostgreSQL 会应用可见性检查规则,为每个事务选择适当的版本。SI 可防止 ANSI SQL-92 标准中定义的三种异常,即脏读、非重复读取和幻读。
PostgreSQL 使用的另一种并发控制技术是 Optimistic Concurrency Control (OCC)。与创建多个版本的数据项的 MVCC 不同,OCC 假定事务之间的冲突很少见。它允许事务在不最初获取锁的情况下继续进行,但在提交阶段检查冲突。如果检测到冲突,则会回滚其中一个冲突的事务,并且必须重试。
虽然 SI 提供了良好的隔离并防止了 三种 异常,但它无法实现真正的可序列化性。为了解决这一限制,PostgreSQL 版本引入了可序列化快照隔离 (SSI)。SSI 可以检测序列化异常,例如 Write Skew 和 Read-only Transaction Skew,并解决由此类异常引起的冲突。借助 SSI,PostgreSQL 提供了真正的 SERIALIZABLE 隔离级别,即使在存在复杂的并发场景的情况下也能确保严格的一致性。
PostgreSQL 提供三个事务隔离级别:READ COMMITTED、REPEATABLE READ 和 SERIALIZABLE。这些级别决定了并发事务期间数据的可见性和行为。下表总结了 PostgreSQL 中的隔离级别及其特征。
隔离级别 | Dirty Reads 脏读 | Non-repeatable Read 不可重复读 | Phantom Read 幻读 | Serialization Anomaly 序列化异常 |
READ COMMITTED | 不可能 | 可能 | 可能 | 可能 |
REPEATABLE READ | 不可能 | 不可能 | 不可能 | 可能 |
SERIALIZABLE | 不可能 | 不可能 | 不可能 | 不可能 |
PostgreSQL 中的事务 ID (txid)
在 PostgreSQL 中,每个事务都分配有一个称为事务 ID (xid) 的唯一标识符。它是一个 32 位无符号整数,允许大约 42 亿个交易 ID。PostgreSQL 保留特殊的 xid:0 表示无效的 xid,1 表示 Bootstrap xid(在数据库初始化期间使用),2 表示冻结的 txid。
比较事务 ID
PostgreSQL 中的事务 ID 可以相互比较。以 xid 100 为例。任何大于 100 的 xid 都被视为“将来”,从 xid 100 的角度来看是不可见的。另一方面,小于 100 的 xid 被视为“过去”,并且对 xid 100 可见。由于 xid 的空间有限,PostgreSQL 将其视为一个循环空间。之前的 21 亿个 xid 被归类为“过去”,接下来的 21 亿个 xid 被归类为“未来”。
事务 ID 回卷问题
注意 xid 回卷问题,当 xid 空间耗尽时,会发生该问题。在实际系统中,可用的 xid 空间可能不够。当 xid 回卷问题发生时,可能会导致数据不一致和系统故障。为了缓解这个问题,PostgreSQL 采用了各种机制(如 autovacuum)来定期清理死元组并回收事务 ID。通过确保不再需要和回收旧的事务 ID,可以防止 回卷问题。
PostgreSQL 中的元组结构
在 PostgreSQL 中,表页中的堆元组由三个主要组件组成:HeapTupleHeaderData 结构、NULL 位图和用户数据。就我们的目的而言,我们将重点关注四个重要系统字段:
- t_xmin:此字段保存插入元组的事务的 txid。
- t_xmax:它存储删除或更新元组的事务的 txid。如果元组尚未修改,则 t_xmax 设置为 0 (INVALID)。
- t_cid:命令 ID (cid) 表示同一事务中当前命令之前执行的 SQL 命令数。
- t_ctid:元组标识符 (tid) 指向自身或新元组。它用于标识表中的元组。如果 Tuples 已更新,则 t_ctid 指向新 Tuples;否则,它指向自身
插入、删除和更新 Tuples
让我们探索一下如何在 PostgreSQL 中插入、删除和更新元组。我们还将介绍用于这些操作的自由空间地图 (FSM)。
1. Insertion:插入新元组时,会直接插入到目标表的页面中。例如,如果 Tuples 由 txid 为 99 的事务插入,则 Tuples 的 Headers 字段将相应地设置。
t_xmin:99(由 txid 99 插入)
t_xmax:0(未删除或更新)
t_cid:0(txid 99 插入的第一个元组)
t_ctid:(0,1)(指向自身)
2. 删除:为了逻辑删除元组,执行 DELETE 命令的 txid 被设置为元组的 t_xmax。这会将 Tuples 标记为已删除,并且它将成为死 Tuples。
t_xmax:111(由 txid 111 删除)
3. Update:更新元组时,PostgreSQL 会对现有元组执行逻辑删除并插入一个新元组。旧元组的 t_ctid 字段将更新为指向新元组。
-
第一次更新:
- Tuple_1:
- t_xmax:100(由 txid 100 逻辑删除)
- t_ctid:(0,2)(指向新元组)
- Tuple_1:
-
第二次更新:
- Tuple_2:
- t_xmax:150(由 txid 150 逻辑删除)
- t_ctid:(0,3)(指向新元组)
- Tuple_2:
在这两种更新方案中,通过将原始元组的 t_xmax 值设置为相应的事务 ID 来逻辑删除原始元组。这可确保更新的 Tuples 具有单独的条目,并保持事务的一致性和隔离性。
Free Space Map (FSM)
Free Space Map (FSM) 是 PostgreSQL 存储管理系统的关键组件。它跟踪表的每一页中的可用空间。FSM 在元组插入期间使用,并帮助确定适当的页面以容纳新元组。插入 Tuples 时,PostgreSQL 会查阅 FSM 以查找具有足够可用空间的页面。如果现有页面没有足够的空间,系统会查找具有更多空间的可用页面。找到合适的页面后,将插入新 Tuples,并更新 FSM 以反映可用空间的变化。同样,当删除元组时,元组占用的空间将在 FSM 中标记为 free,从而允许将其重新用于将来的插入。
事务管理和元组可见性
事务管理和元组可见性在 PostgreSQL 中密切相关。事务 ID (txid) 在确定哪些元组对事务可见方面起着至关重要的作用。当事务开始时,它会被分配一个唯一的 txid。事务可以看到 txid 小于其自身 (过去) 的所有 Tuples。txid 大于事务 ID(将来)的元组被视为对事务不可见。此机制可确保事务隔离,因为每个事务都在数据库的一致快照上运行,只能看到在其开始时间之前提交的 Tuples。
快照隔离和多版本并发控制 (MVCC)
PostgreSQL 采用多版本并发控制 (MVCC) 技术来实现快照隔离。MVCC 允许并发事务在数据库上运行,而不会过度阻塞彼此。在 MVCC 下,每个事务都使用其数据库快照,该快照由基于其事务 ID 对该事务可见的 Tuples 组成。此方法可在保持数据完整性的同时实现更高程度的并发。
事务状态
PostgreSQL 定义了四种事务状态:
• IN_PROGRESS:表示正在进行的事务。
• COMMITTED:表示事务已成功提交。
• ABORTED:表示已中止的事务。
• SUB_COMMITTED:保留用于子交易 。
示例:假设我们有两个事务:
• txid 为 200 提交的事务 T1,将其状态从 IN_PROGRESS 更改为 COMMITTED。
• txid 为 201 的事务 T2 中止,将其状态从 IN_PROGRESS 更改为 ABORTED。
Clog 如何执行
PostgreSQL 中的提交日志 (Clog) 是存储在共享内存中的逻辑数组。它由一个或多个 8 KB 页面组成,其中每个页面代表一个事务 ID (txid) 并保存相应事务的状态。当堵塞达到其容量时,将附加一个新页面。为了确定事务的状态,内部函数读取 clog 并检索相关信息。
当 PostgreSQL 关闭时或在检查点进程期间,clog中的数据会定期写入存储在 pg_xact 子目录中的文件中。这些文件命名为 0000、0001 等,最大文件大小为 256 KB。启动时,PostgreSQL 会加载存储在 pg_xact 文件中的数据以初始化 clog。此外,PostgreSQL 会定期执行 vacuum 处理,以从clog中删除不必要的旧数据。注意:在以前的版本(9.6 之前)中,pg_xact 称为 pg_clog。
事务快照
PostgreSQL 中的事务快照是一个数据集,用于存储有关特定时间点事务活动状态的信息。活动事务是指正在进行或尚未开始的事务。PostgreSQL 在内部使用文本格式表示事务快照:'xmin:xmax:xip_list。例如,“100:104:100,102”表示小于 99 的 txid 未处于活动状态,而 txid 100、102 和更大的 txid 处于活动状态。
例如执行查询 SELECT pg_current_snapshot();返回事务快照 '100:104:100,102':
• xmin:最早的活动 txid。
• xmax:第一个未分配的 txid。
• xip_list:xmin 和 xmax 之间的活动 txid。
可见性检查规则
可见性检查规则根据元组的 t_xmin、t_xmax、堵塞和获取的事务快照来确定元组是可见的还是不可见的。让我们关注可见性检查的基本规则(忽略 sub-transactions 和 t_ctid 讨论)。
- t_xmin的状态为 ABORTED
- 规则 1:如果t_xmin状态为 'ABORTED',则元组始终不可见。
- t_xmin 的状态为 COMMITTED 且未设置 t_xmax •
- 规则 2:如果t_xmin状态为 'COMMITTED' 且未设置 t_xmax,则元组始终可见。
- t_xmin 的状态为 COMMITTED 且 t_xmax 已设置•
- 规则 3:如果t_xmin状态为 'COMMITTED' 且已设置t_xmax,则如果当前事务快照早于 t_xmax,则元组可见。
- 规则 4:如果t_xmin状态为 'COMMITTED' 且已设置t_xmax,则当当前事务快照晚于或等于 t_xmax 时,元组不可见。
- t_xmin的状态为 IN_PROGRESS
- 规则 5:如果t_xmin状态为“IN_PROGRESS”且未设置t_xmax,则如果当前事务快照早于启动事务的快照 (t_xmin),则元组可见。
- 规则 6:如果t_xmin状态为 'IN_PROGRESS' 且未设置 t_xmax,则当当前事务快照晚于或等于启动事务的快照 (t_xmin) 时,元组将不可见。
- t_xmin 的状态为 IN_PROGRESS 且 t_xmax 已设置
- 规则 7:如果t_xmin状态为 'IN_PROGRESS' 且已设置t_xmax,则如果当前事务快照早于 t_xmax 且晚于或等于 t_xmin,则元组可见。
- 规则 8:如果t_xmin状态为 'IN_PROGRESS' 且设置了 t_xmax,则当当前事务快照早于或等于 t_xmin 或晚于或等于 t_xmax 时,元组不可见。
- t_xmin 的状态为 IN_PROGRESS 且未设置 t_xmax,并且已设置当前事务快照的 xmax
- 规则 9:如果t_xmin状态为“IN_PROGRESS”,t_xmax未设置,并且设置了当前事务快照的 xmax,则如果当前事务快照晚于或等于 t_xmax,则元组不可见。
- t_xmin 的状态为 IN_PROGRESS 且 t_xmax 已设置,并且当前事务快照的 xmax 已设置
- 规则 10:如果t_xmin状态为 'IN_PROGRESS t_xmax 已设置,并且设置了当前事务快照的 xmax,则如果当前事务快照晚于 或 等于 t_xmax,则元组不可见。通过应用这些可见性检查规则,PostgreSQL 可确保事务根据其事务状态和其他并发事务的状态看到一致且适当的数据。
防止异常
PostgreSQL 实施了各种措施来防止异常并确保数据一致性。让我们看看它如何解决三个 ANSI SQL-92 异常问题:脏读、可重复读和幻读。
脏读
当事务从仍在进行的另一个事务中读取未提交的数据时,会发生脏读。PostgreSQL 通过遵循 Read Committed 隔离级别来防止脏读,这可确保事务只能看到其他事务提交的更改。
Repeatable Reads 可重复读取
Repeatable Reads 保证事务在整个执行过程中看到相同的数据快照,即使其他事务提交更改也是如此。PostgreSQL 通过使用多版本并发控制 (MVCC) 和事务 ID (txid) 来实现这一点。每个事务都有一个唯一的 txid,PostgreSQL 确保事务只看到在其快照的 txid 处可见的元组。
Phantom Reads 幻象读取
当事务由于其他事务的并发插入或删除而在连续读取期间看到一组不同的行时,就会发生幻读。PostgreSQL 通过使用 MVCC 和谓词锁定的组合来解决幻像读取。
了解 PostgreSQL 中的可序列化快照隔离 (SSI)
在 PostgreSQL 中,有一个强大的隔离级别,称为可序列化快照隔离 (SSI),可确保事务像串行运行一样执行,即使在并发环境中也是如此。SSI 是一种健壮的机制,可以防止 Write-Skew 等序列化异常,并保证数据的一致性。在此博客中,我们将探讨 PostgreSQL 中 SSI 的实现,并通过适当的示例了解它的工作原理。
结论
总之,PostgreSQL 中的并发控制在维护多用户环境中的数据一致性和隔离性方面起着至关重要的作用。PostgreSQL 利用多版本并发控制 (MVCC)、快照隔离 (SI)、乐观并发控制 (OCC) 和可序列化快照隔离 (SSI) 等各种技术来有效处理并发事务。
这些技术允许事务同时读取和写入数据而不会相互阻塞,同时确保防止脏读、不可重复读取和幻读等异常情况。PostgreSQL 的事务隔离级别(包括 READ COMMITTED、REPEATABLE READ 和 SERIALIZABLE)为并发事务提供不同级别的可见性和行为。
为了管理并发,PostgreSQL 为每个事务分配一个唯一的事务 ID (txid),并实施 autovacuum 等机制来防止事务 ID 环绕问题。了解元组结构、插入、删除和更新操作,以及自由空间映射 (FSM) 的作用,对于理解 PostgreSQL 中的并发控制至关重要。
通过有效管理事务、执行可见性检查和利用 MVCC,PostgreSQL 即使在复杂的并发场景中也能确保数据的一致性和隔离性。这允许多个用户同时访问和修改数据库,而不会影响数据的完整性。PostgreSQL 强大的并发控制机制使其成为需要并发访问数据同时保持数据一致性和隔离性的应用程序的可靠选择。