PostgreSQL中表以元组(堆)的形式存储,元组的头结点中包含了一些重要的节点信息。介绍MVCC前只针对需要重点阐述的信息进行介绍。

    元组的 INSERT/DELETE/UPDATE 操作

    上文提到PostgreSQL中对记录的修改不是直接修改tuple结构,而是重新生成一个tuple,旧的tuple通过t_cid指向新的tuple。下面针对数据库的三种典型操作,增加、删除、修改进行分析

    INSERT

    INSERT操作最简单,直接将一条记录放到page中即可

    PgSQL· 引擎特性 · 多版本并发控制介绍及实例分析 - 图2

    插入操作的过程和结果分析:

    DELETE

    PostgreSQL中的删除并不是真正的删除,实际上该元组依然存在于数据库的存储页面,只是因为其对元组进行处理使得查询不可见。

    删除操作使得tuple 的t_xmax字段被修改了,即被改成了1187,删除这个tuple数据的事务txid

    当txid为1187的事务提交时,tuple就成了无效元组,称为“dead tuple”

    但是这个tuple依然残留在页面上, 随着数据库的运行,这种 “dead tuple” 越来越多,它们会在 VACUUM时最终被清理掉。

    UPDATE

    相比于前面的操作,update操作复杂一些。同样按照准则:PostgreSQL对记录的修改不会修改数据,而是先删除旧数据,后新增加数据。(将旧数据标记为删除状态,再插入一条新数据)

    PgSQL· 引擎特性 · 多版本并发控制介绍及实例分析 - 图4

    这里涉及的操作较多,依次理一下思路

    1 . 首先第一条语句 是一次普通的更新操作

    1184 - 1187 - 0 - (0, 2) 。

    • 对于 t_max 来说,其更新为了新的事务ID,也标识该tuple为“dead tuple”

    第二元组,新插入一条 元组,这条元组就像新插入的数据一样,t_cid 指向其本身

    2 . 第二条升级语句是同一事务中,多次更新的例子

    第一元组 已经过期,且为“dead tuple” 不更新

    第二元组为当前的活跃元组,其被更新,方式如同上文,指向第三元组

    第三元组新插入一条数据,这里不同的是,由于在同一事务中,其事务号不变,而是事务次序+1(t_cid)

    事务快照是一个很形象的词,所谓快照就像是相机按下快门,记录当前瞬间的信息。 观察者通过快照只能获取当前时间点之前的信息,而按下快门以后的信息便无法察觉,即 INVISIBLE

    类比于快照,事务快照就是当一个事务执行期间,那些事务active、那些非active。即这个事务要么在执行中,要么还没开始。

    快照由这样一个序列构成 xmin:xmax:xip_list

    事务快照举例

    在PostgreSQL中,当一个事务的隔离级别是 已提交读 时, 在该事务的每个命令执行之前都会重新获取一次 snapshot, 如果事务的隔离级别是 可重复度 和 可串行化时,事务只在第一条命令获取 snapshot。

    PgSQL· 引擎特性 · 多版本并发控制介绍及实例分析 - 图6

    现在假设一个多事务的场景,其中事务 A 事务 B 的隔离级别是 已提交读,而 事务 C 的隔离级别是 可重复读。这些阶段我们分别来讨论

    • T1

    Transaction_A 开始并执行第一条命令,获取 txid 和 snapshot ,事务系统分配给 Transaction_A 分配的 txid 为 200, 并获取当前快照为 200:200

    • T2

    Transaction_B 开始执行第一条命令,获取txid 和 snapshot, 事务系统给 Transaction_B 分配 txid 为 201并获取当前快照为 200:200 ,因为Transaction_A正在运行,所以Transaction_B 无法看到 A中的修改

    • T3
    • T4

    Transaction_A 进行了commit,事务管理系统删除了 Transaction_A 信息

    • T5

    Transaction_B Transaction_C 分别执行其 SELECT 命令,此时 Transaction_B 获取一个新的 snapshot(其隔离级别是已提交读),该snapshot为 201:201 。因为Transaction_A 已提交,所以Transaction_A 对 Transaction_B 可见。

    同时,Transaction_C 隔离级别是 已提交读,所以它不会更新自己的 snapshot,因此 Transaction_A和Transaction_B仍然对Transaction_C不可见。

    元组可见性利用以下几个点确定:

    判断一个tuple对当前执行的事务是否可见,既事务是否会处理该tuple。下面只针对最简单的情形进行讨论

    1 .t_xmin 的状态为 ABORTED

    定则1:如果t_xmin事务废弃其不可见

    2 .t_xmin 的状态为 IN_PROGRESS

    定则2:如果t_xmin事务正在运行且非当前事务,事务中的tuple不可见(不然就是读脏数据)

    定则3:如果t_xmin事务运行中,且tuple状态为死亡(t_xmax!=0)不可见

    定则4:如果t_xmin事务运行中,且tuple状态为活跃(t_xmax=0)可见

    3. t_xmin的状态为 COMMITTED

    定则5:如果t_xmin事务已提交,当前快照中t_xmin活跃,则不可见

    定则6:如果t_xmin事务已提交,t_xmax事务在运行中且t_max事务为当前事务,则不可见

    定则7:如果t_xmin事务已提交,t_xmax事务在运行中且t_max事务不为当前事务,则可见

    定则8:如果t_xmin事务已提交,t_xmax事务状态为已提交,当前快照中t_xmax活跃,则可见

    定则9: 如果t_xmin事务已提交,t_xmax事务状态为已提交,当前快照t_xmax不活跃,不可见

    PostgreSQL 中的 MVCC

    PostgreSQL 多版本并发控制是种乐观锁的体现,解决了读多写少场景下的大规模读并发问题。一个事务在读取数据时应该读取哪个版本的数据,取决于该数据对于该事务的可见性。这种元组可见性检测可以帮助数据库找到”正确“版本的tuple,而且实现了ANSI SQL-92 标准中定义的异常:脏读、不可重复读、幻读