ITPub博客

首页 > > > PostgreSQL和Mysql的MVCC实现机制的差异对比

PostgreSQL和Mysql的MVCC实现机制的差异对比

作者:zuike2000 时间:2020-02-20 13:21:27 0 删除 编辑

任何数据库的主要要求之一就是实现可伸缩性。只有将争用(锁定)最小化(如果不能一起删除),才可以实现。由于读/写/更新/删除是数据库中发生的一些主要的频繁操作,因此对于这些操作并发进行而不被阻塞非常重要。为了实现这一目标,大多数主要数据库都采用了一种称为多版本并发控制的并发模型,该模型将争用降低到最低限度。

什么是MVCC

多版本并发控制(以下简称MVCC)是一种算法,可通过维护同一对象的多个版本来提供精细的并发控制,以使READ和WRITE操作不会冲突。这里的WRITE表示UPDATE和DELETE,因为无论如何新插入的记录都将按照隔离级别受到保护。每个WRITE操作都会生成对象的新版本,并且每个并发读取操作都会根据隔离级别读取对象的不同版本。由于读取和写入操作均在同一对象的不同版本上进行,因此这些操作都不需要完全锁定,因此两者都可以并发操作。争用仍然存在的唯一情况是当两个并发事务尝试写入同一记录时。

当前大多数主要数据库都支持MVCC。该算法的目的是维护同一对象的多个版本,因此MVCC的实现因数据库而异,仅在创建和维护多个版本方面有所不同。因此,相应的数据库操作和数据存储发生了变化。

公认的实现MVCC的方法是PostgreSQL和Firebird / Interbase使用的一种,而InnoDB和Oracle使用的另一种。在随后的章节中,我们将详细讨论如何在PostgreSQL和InnoDB中实现它。

PostgreSQL中的MVCC

为了支持多个版本,PostgreSQL维护每个对象的其他字段(PostgreSQL术语为Tuple),如下所述:

  1. xmin –插入或更新元组的交易的交易ID。如果是UPDATE,则将使用此事务ID分配更新版本的元组。
  2. xmax –删除或更新元组的交易的交易ID。如果是UPDATE,则为元组的当前现有版本分配此事务ID。在新创建的元组上,此字段的默认值为null。

PostgreSQL将所有数据存储在称为HEAP的主存储中(页面的默认大小为8KB)。所有新元组都将xmin作为创建它的事务进行处理,而旧版本元组(已更新或删除)将分配给xmax。从旧版本元组到新版本始终存在链接。在隔离的情况下,较旧版本的元组可用于在回滚的情况下重新创建元组,并可通过READ语句读取较旧版本的元组。

考虑到表有两个元组,T1(值1)和T2(值2),可以在下面的3个步骤中演示新行的创建:


MVCC:在PostgreSQL中存储多个版本

如图所示,数据库中最初有两个元组,其值分别为1和2。

然后,在第二步中,将值2的行T2更新为值3。这时,将使用新值创建一个新版本,并将其存储为与现有元组相邻并将其存储在同一存储区域中。在此之前,较旧的版本将分配给xmax并指向最新版本的元组。

类似地,在第三步中,当删除具有值1的行T1时,将在同一位置虚拟删除现有行(即,它为当前事务分配了xmax)。没有为此创建新版本。

接下来,让我们看一下每个操作如何创建多个版本,以及如何保持事务隔离级别而不用一些实际示例进行锁定。在下面的所有示例中,默认情况下使用“ READ COMMITTED”隔离。

插入

每次插入记录时,都会创建一个新的元组,并将其添加到属于相应表的页面之一中。


PostgreSQL并发INSERT操作

正如我们在这里看到的逐步:

  1. 会话A启动事务并获取事务ID 495。
  2. 会话B启动事务并获取事务ID 496。
  3. 会话A插入一个新的元组(获取存储在HEAP中)
  4. 现在,添加了将xmin设置为当前事务ID 495的新元组。
  5. 但是从会话B中看不到相同的内容,因为xmin(即495)仍未提交。
  6. 一旦提交。
  7. 数据对两个会话均可见。

更新

PostgreSQL UPDATE不是“ IN-PLACE”更新,即它不会使用所需的新值来修改现有对象。相反,它将创建该对象的新版本。因此,UPDATE大致涉及以下步骤:

  1. 它将当前对象标记为已删除。
  2. 然后,它添加该对象的新版本。
  3. 将对象的旧版本重定向到新版本。

因此,即使许多记录保持不变,HEAP也会占用空间,就好像插入了多条记录一样。


PostgreSQL并发INSERT操作

正如我们在这里看到的逐步:

  1. 会话A启动事务并获取事务ID 497。
  2. 会话B启动事务并获取事务ID 498。
  3. 会话A更新现有记录。
  4. 在这里,会话A看到一个版本的元组(更新的元组),而会话B看到另一个版本(旧元组,但xmax设置为497)。两个元组版本都存储在HEAP存储中(甚至同一页面也取决于空间可用性)
  5. 一旦会话A提交了事务,则由于提交了旧元组的xmax,旧元组将过期。
  6. 现在,两个会话都看到相同版本的记录。

删除

删除几乎与UPDATE操作类似,只是它不必添加新版本。只是将当前对象标记为DELETED,如UPDATE情况中所述。


PostgreSQL并发DELETE操作

  1. 会话A启动事务并获取事务ID 499。
  2. 会话B启动事务并获取事务ID 500。
  3. 会话A删除现有记录。
  4. 在这里,会话A没有看到任何从当前事务中删除的元组。而Session-B看到该元组的较旧版本(xmax为499;删除该记录的事务)。
  5. 一旦会话A提交了事务,则由于提交了旧元组的xmax,旧元组将过期。
  6. 现在,两个会话都看不到已删除的元组。

如我们所见,没有任何操作可以直接删除对象的现有版本,并且在需要的地方都可以添加对象的其他版本。

现在,让我们看一下如何在具有多个版本的元组上执行SELECT查询:SELECT需要读取所有版本的元组,直到根据隔离级别找到合适的元组为止。假设有元组T1,它已更新并创建了新版本T1',并在更新时又创建了T1'':

  1. SELECT操作将通过此表的堆存储并首先检查T1。如果提交了T1 xmax事务,则它将移至该元组的下一个版本。
  2. 假设现在也提交了T1'元组xmax,然后再次移动到该元组的下一个版本。
  3. 最后,它找到T1''并看到xmax未提交(或为null),并且根据隔离级别,T1''xmin对于当前事务可见。最后,它将读取T1''元组。

如我们所见,它需要遍历元组的所有3个版本,以便找到合适的可见元组,直到过期的元组被垃圾收集器(VACUUM)删除为止。

InnoDB中的MVCC

为了支持多个版本,InnoDB为每行维护其他字段,如下所述:

  1. DB_TRX_ID:插入或更新该行的事务的事务ID。
  2. DB_ROLL_PTR:它也称为回滚指针,它指向写入回滚段的撤消日志记录(在下一个更多内容中)。

与PostgreSQL一样,InnoDB在所有操作中也会创建该行的多个版本,但是旧版本的存储有所不同。

对于InnoDB,更改后的行的旧版本保存在单独的表空间/存储中(称为undo段)。因此,与PostgreSQL不同,InnoDB在主存储区中仅保留行的最新版本,而在undo段中保留较旧的行。还原段中的行版本用于回滚时的撤消操作,并根据隔离级别通过READ语句读取旧版本的行。

考虑到表有两行,T1(值1)和T2(值2),可以通过以下3个步骤来演示新行的创建:


MVCC:在InnoDB中存储多个版本

从图中可以看出,数据库中最初有两行,其值分别为1和2。

然后在第二阶段中,将值2的行T2更新为值3。这时,将使用新值创建新版本,并替换旧版本。在此之前,较旧的版本将存储在撤消段中(请注意,UNDO段版本仅具有增量值)。另外,请注意,回滚段中有一个从新版本到旧版本的指针。因此,与PostgreSQL不同,InnoDB更新是“ IN-PLACE”。

类似地,在第三步中,当删除具有值1的行T1时,则在主存储区域中虚拟删除了现有行(即,它只是在行中标记了一个特殊位),并在其中添加了与之对应的新版本。撤消段。同样,从主存储器到撤消段只有一个滚动指针。

从外部看,所有操作的行为均与PostgreSQL相同。只是多个版本的内部存储有所不同。

MVCC:PostgreSQL与InnoDB

现在,让我们分析PostgreSQL和InnoDB在MVCC实现方面的主要区别是什么:

  1. 旧版本的大小
    PostgreSQL只是在元组的较旧版本上更新xmax,因此较旧版本的大小与相应的插入记录相同。这意味着,如果您有3个版本的旧元组,则所有版本都将具有相同的大小(除非每次更新时实际数据大小有所不同)。
    而对于InnoDB,存储在Undo段中的对象版本通常小于相应的插入记录。这是因为仅将更改的值(即差分)写入UNDO日志。
  2. INSERT操作
    即使对于INSERT,InnoDB也需要在UNDO段中写入一条额外的记录,而PostgreSQL仅在UPDATE的情况下才创建新版本。
  3. 在回滚的情况下还原旧版本
    PostgreSQL不需要任何特定的东西就可以在回滚的情况下恢复旧版本。请记住,旧版本的xmax等于更新此元组的事务。因此,在提交该事务ID之前,对于并发快照,它被视为活动元组。事务回滚后,所有事务都会自动将相应的事务视为活动事务,因为这将是中止的事务。
    而对于InnoDB,则明确要求在回滚发生后重建对象的旧版本。
  4. 回收旧版本占用的空间
    对于PostgreSQL,仅当没有并行快照读取该版本时,才可以将较早版本占用的空间视为已耗尽。旧版本失效后,VACUUM操作可以回收它们所占用的空间。VACUUM可以手动触发,也可以作为后台任务触发,具体取决于配置。
    InnoDB UNDO日志主要分为INSERT UNDO和UPDATE UNDO。相应的事务提交后,第一个将被丢弃。第二个需要保留,直到与任何其他快照平行为止。InnoDB没有显式的VACUUM操作,但是在类似的行上,它具有异步的PURGE来丢弃作为后台任务运行的UNDO日志。
  5. 延迟真空的影响
    如前所述,在PostgreSQL的情况下延迟真空会产生巨大的影响。即使不断删除记录,它也会导致表开始膨胀并导致存储空间增加。它也可能达到需要完全抽真空的地步,这是非常昂贵的操作。
  6. 如果表格过大,则顺序扫描
    PostgreSQL顺序扫描必须遍历对象的所有旧版本,即使所有旧版本都已失效(直到使用真空将其删除为止)。这是PostgreSQL中最典型且讨论最多的问题。请记住,PostgreSQL将所有版本的元组存储在同一存储中。
    而对于InnoDB,除非需要,否则不需要读取撤消记录。如果所有撤消记录都已失效,则仅足以读取对象的所有最新版本。
  7. 指数
    PostgreSQL将索引存储在单独的存储中,该存储与HEAP中的实际数据保持一个链接。因此,即使INDEX不变,PostgreSQL也必须更新INDEX部分。尽管稍后通过实现HOT(仅堆元组)更新解决了此问题,但仍然存在一个局限,即如果无法在同一页面中容纳新的堆元组,则它将回退到常规UPDATE。
    InnoDB没有问题,因为它们使用聚集索引。

结论

PostgreSQL MVCC没有什么缺点,特别是在工作负载频繁更新/删除的情况下,存储空间过大。因此,如果您决定使用PostgreSQL,则应该非常小心地配置VACUUM。

PostgreSQL社区也承认这是一个主要问题,他们已经开始研究基于UNDO的MVCC方法(临时名称为ZHEAP),我们可能会在以后的版本中看到同样的情况。

原文作者:;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2019-04-16

  • 博文量
    404
  • 访问量
    2062