zoukankan      html  css  js  c++  java
  • PostgreSQL的MVCC(3)--Row Versions

    我们已经讨论过隔离,并且对底层数据结构做了介绍。现在介绍一下行版本(元组)。

    Tuple header

    如前所述,数据库中同一行记录在同一时刻可以有多个版本可用。我们需要以某种方式将一个版本与另一个版本区分开。为此,每个版本都标有有效的“time”(xmin)和到期的”time”(xmax)。引号表示使用特殊的递增计数器,而不是时间本身。该计数器是事务标识符。

    (通常,实际中更复杂:由于计数器的bit深度有限,事务ID不能总是递增。但是,当我们的讨论陷入僵局时,我们将探索其更多细节。)

    创建行时,将xmin的值设置为等于执行INSERT命令的事务的ID,而未填写xmax。

    删除一行后,当前版本的xmax值将标记为执行DELETE的事务的ID。

    UPDATE命令实际上执行两个后续操作:DELETE和INSERT。在该行的当前版本中,将xmax设置为等于执行UPDATE的事务的ID。然后,创建同一行的新版本,其中xmin的值与先前版本的xmax相同。

    xmin和xmax字段包含在行版本的header中。除这些字段外,tuple header还包含其他字段,例如:

    ·infomask —占用几个bits,用于确定给定元组的属性。后面我们将逐步讨论。

    ·ctid—对同一行的下一个更新版本的引用。最新的行版本的ctid引用该版本。 该数字采用(x,y)形式,其中x是页面的编号,而y是数组中指针的顺序号。

    ·NULL位图,用于标记给定版本中包含NULL的列。NULL不是常规的数据类型值,因此,我们必须单独存储此特征。

    因此,header看起来非常大:每个元组至少23个字节,但是由于NULL位图而通常更大。如果一个表是“ narrow”(窄)(也就是说,它包含几列),那么开销字节会比有用信息占用更多的空间。

    插入(insert)

    让我们更详细地了解如何在底层执行对行的操作,我们从插入开始。

    为了进行实验,我们将创建一个有两列的新表,其中一列上有一个索引:

    => CREATE TABLE t(
      id serial,
      s text
    );
    => CREATE INDEX ON t(s);
    

    启动一个事务插入一行:

    => BEGIN;
    => INSERT INTO t(s) VALUES ('FOO');
    

    我们当前事务的id是:

    => SELECT txid_current();
     txid_current 
    --------------
             3664
    (1 row)
    

    让我们看看这个页面的内容。来自«pageinspect»扩展的heap_page_items函数使我们能够获得关于指针和行版本的信息:

    => SELECT * FROM heap_page_items(get_raw_page('t',0)) gx
    -[ RECORD 1 ]-------------------
    lp          | 1
    lp_off      | 8160
    lp_flags    | 1
    lp_len      | 32
    t_xmin      | 3664
    t_xmax      | 0
    t_field3    | 0
    t_ctid      | (0,1)
    t_infomask2 | 2
    t_infomask  | 2050
    t_hoff      | 24
    t_bits      | 
    t_oid       | 
    t_data      | x0100000009464f4f
    

    注意,PostgreSQL中的单词«heap»表示表。这是该术语的另一种奇怪用法:堆是一种已知的数据结构,与表无关。这个词在这里使用的意义是«所有都被堆积起来»,不像在有序索引中。

    这个函数以一种难以理解的格式显示«当前»数据。为了澄清问题,我们只留下部分信息并加以解释:

    => SELECT '(0,'||lp||')' AS ctid,
           CASE lp_flags
             WHEN 0 THEN 'unused'
             WHEN 1 THEN 'normal'
             WHEN 2 THEN 'redirect to '||lp_off
             WHEN 3 THEN 'dead'
           END AS state,
           t_xmin as xmin,
           t_xmax as xmax,
           (t_infomask & 256) > 0  AS xmin_commited,
           (t_infomask & 512) > 0  AS xmin_aborted,
           (t_infomask & 1024) > 0 AS xmax_commited,
           (t_infomask & 2048) > 0 AS xmax_aborted,
           t_ctid
    FROM heap_page_items(get_raw_page('t',0)) gx
    -[ RECORD 1 ]-+-------
    ctid          | (0,1)
    state         | normal
    xmin          | 3664
    xmax          | 0
    xmin_commited | f
    xmin_aborted  | f
    xmax_commited | f
    xmax_aborted  | t
    t_ctid        | (0,1)
    

    我们做了以下工作:

    ·在指针编号上添加了零,使其看起来像t_ctid :(页面编号,指针编号)。
    ·解释了lp_flags指针的状态。这里是«normal»,这意味着指针实际上引用了行版本。稍后我们将讨论其他值。
    ·到目前为止,在所有信息位(bits)中,我们仅选择了两对。xmin_committed和xmin_aborted位显示ID为xmin的事务是否已提交(回滚)。一对相似的位与事务ID的xmax有关。

    我们观察到什么?当插入一行时,在表页面中会出现一个指针,该指针的编号为1,并引用该行的第一个且唯一版本。

    元组中的xmin字段填充为当前事务的ID。由于事务仍处于活动状态,因此xmin_committed和xmin_aborted位均未设置。

    行版本的ctid字段引用同一行。这意味着没有新版本可用。

    由于没有删除元组(即最新),因此xmax字段用常规数字0填充。由于设置了xmax_aborted位,事务将忽略该数字。

    通过将信息位附加到事务ID上,我们又向前迈出了一步,以提高可读性。并且创建函数,因为我们将多次查询:

    => CREATE FUNCTION heap_page(relname text, pageno integer)
    RETURNS TABLE(ctid tid, state text, xmin text, xmax text, t_ctid tid)
    AS $$
    SELECT (pageno,lp)::text::tid AS ctid,
           CASE lp_flags
             WHEN 0 THEN 'unused'
             WHEN 1 THEN 'normal'
             WHEN 2 THEN 'redirect to '||lp_off
             WHEN 3 THEN 'dead'
           END AS state,
           t_xmin || CASE
             WHEN (t_infomask & 256) > 0 THEN ' (c)'
             WHEN (t_infomask & 512) > 0 THEN ' (a)'
             ELSE ''
           END AS xmin,
           t_xmax || CASE
             WHEN (t_infomask & 1024) > 0 THEN ' (c)'
             WHEN (t_infomask & 2048) > 0 THEN ' (a)'
             ELSE ''
           END AS xmax,
           t_ctid
    FROM heap_page_items(get_raw_page(relname,pageno))
    ORDER BY lp;
    $$ LANGUAGE SQL;
    

    行版本的header更清楚的形式:

    => SELECT * FROM heap_page('t',0);
     ctid  | state  | xmin | xmax  | t_ctid 
    -------+--------+------+-------+--------
     (0,1) | normal | 3664 | 0 (a) | (0,1)
    (1 row)
    

    通过使用xmin和xmax伪列,我们可以从表本身获得类似的信息,但远没有那么详细:

    => SELECT xmin, xmax, * FROM t;
     xmin | xmax | id |  s  
    ------+------+----+-----
     3664 |    0 |  1 | FOO
    (1 row)

    提交(commit)

    事务成功后,必须记住其状态,即必须将事务标记为已提交。为此,使用了XACT结构。(在版本10之前,它被称为CLOG(提交日志),仍然很可能会遇到此名称)

    XACT不是系统目录的表,而是PGDATA /pg_xact目录中的文件。在这些文件中为每个事务分配了两个bits(即«committed»和«aborted»),其方式与元组头中的方式完全相同。此信息只是为了方便而散布在几个文件中。在讨论freezing时,我们将回到这一点。PostgreSQL与所有其他文件一样,逐页处理这些文件。

    因此,提交事务后,将在XACT中为此事务设置“ committed”位。这就是提交事务时发生的所有事情(尽管我们还没有提到预写日志)。

    当其他事务访问我们刚刚查看的表页面时,前者将不得不回答一些问题:

    1.事务xmin是否已完成? 如果没有,则创建的元组必须不可见。通过查看位于实例的共享内存中的另一个结构ProcArray来检查此情况。此结构包含所有活动进程的列表,以及每个进程的当前(活动)事务的ID。

    2.如果事务完成,那么是提交还是回滚? 如果已回滚,则该元组也不得可见。这正是为什么需要XACT。但是,尽管XACT的最后一页存储在共享内存的缓冲区中,但每次检查XACT的开销都很高。 因此,一旦确定了事务状态,就将其写入元组的xmin_committed和xmin_aborted位。如果设置了这些位中的任何一个,则将事务状态视为已知,并且下一个事务将不需要检查XACT。

    为什么执行插入的事务未设置这些位? 当执行插入操作时,事务尚不知道它是否将成功完成。在提交时,还不清楚哪些行和哪些页面已更改。这样的页面可能很多,要跟踪它们是不切实际的。此外,某些页面可能从缓冲区高速缓存中被逐出到磁盘上。再次读取它们以更改位将意味着提交的速度大大降低。

    节省成本的反面是,更新之后,任何事务(甚至是执行SELECT的事务)都可以开始更改缓冲区高速缓存中的数据页。

    因此,我们提交更改:

    => COMMIT;
    

    页面中没有任何更改(但我们知道事务状态已经写入XACT):

    => SELECT * FROM heap_page('t',0);
     ctid  | state  | xmin | xmax  | t_ctid 
    -------+--------+------+-------+--------
     (0,1) | normal | 3664 | 0 (a) | (0,1)
    (1 row)
    

    现在,第一次访问该页面的事务将需要确定事务xmin的状态,并将其写入信息位(bit):

    => SELECT * FROM t;
     id |  s  
    ----+-----
      1 | FOO
    (1 row)
    
    => SELECT * FROM heap_page('t',0);
     ctid  | state  |   xmin   | xmax  | t_ctid 
    -------+--------+----------+-------+--------
     (0,1) | normal | 3664 (c) | 0 (a) | (0,1)
    (1 row)

    删除(delete)

    当删除一行时,当前删除事务的ID会写入最新版本的xmax字段,并且xmax_aborted位将被重置。

    请注意,与活动事务相对应的xmax值用作行锁。如果另一个事务要更新或删除该行,则必须等到xmax事务完成。稍后我们将更详细地讨论锁。此时,仅注意行锁的数量根本没有限制。它们不占用内存,并且该数量不会影响系统性能。但是,长时间的事务还有其他缺点,稍后将对此进行讨论。

    让我们删除一行。

    => BEGIN;
    => DELETE FROM t;
    => SELECT txid_current();
     txid_current 
    --------------
             3665
    (1 row)
    

    我们看到事务ID被写到xmax字段,但是信息位未被设置:  

    => SELECT * FROM heap_page('t',0);
     ctid  | state  |   xmin   | xmax | t_ctid 
    -------+--------+----------+------+--------
     (0,1) | normal | 3664 (c) | 3665 | (0,1)
    (1 row)
    

    中止(abort)

    事务的Abort与commit的工作原理类似,只是«aborted»位是在XACT中设置的。中止和提交一样快。尽管该命令被称为ROLLBACK,但更改不会回滚:事务已经更改的所有内容将保持不变。

    => ROLLBACK;
    => SELECT * FROM heap_page('t',0);
     ctid  | state  |   xmin   | xmax | t_ctid 
    -------+--------+----------+------+--------
     (0,1) | normal | 3664 (c) | 3665 | (0,1)
    (1 row)
    

    当访问页面时,状态将被检查,提示位xmax_aborted将被设置。虽然数字xmax本身仍然在页面中,但是它不会被查看。

    => SELECT * FROM t;
     id |  s  
    ----+-----
      1 | FOO
    (1 row)
    
    => SELECT * FROM heap_page('t',0);
     ctid  | state  |   xmin   |   xmax   | t_ctid 
    -------+--------+----------+----------+--------
     (0,1) | normal | 3664 (c) | 3665 (a) | (0,1)
    (1 row)

    更新(update)

    更新的工作原理是先删除当前版本,然后再插入新版本。

    => BEGIN;
    => UPDATE t SET s = 'BAR';
    => SELECT txid_current();
     txid_current 
    --------------
             3666
    (1 row)
    

    查询返回一行(新版本):

    => SELECT * FROM t;
     id |  s  
    ----+-----
      1 | BAR
    (1 row)
    

    但是我们可以在页面上看到两个版本:

    => SELECT * FROM heap_page('t',0);
     ctid  | state  |   xmin   | xmax  | t_ctid 
    -------+--------+----------+-------+--------
     (0,1) | normal | 3664 (c) | 3666  | (0,2)
     (0,2) | normal | 3666     | 0 (a) | (0,2)
    (2 rows)
    

    删除的版本在xmax字段中用当前事务的ID标记。而且,自从上一个事务回滚以来,这个值已经覆盖了原来的值。并且xmax_aborted位被重置,因为当前事务的状态还未知。

    该行的第一个版本现在引用第二个版本作为更新的行。

    索引页现在包含第二个指针和第二行,它引用表页中的第二个版本。

    与删除操作相同,第一个版本中的xmax值表示该行被锁定。

    最后,我们提交事务:

    => COMMIT;

    索引(indexes)

    到目前为止,我们仅谈论表页。但是索引内部会发生什么?

    索引页中的信息取决于特定的索引类型。而且,即使一种类型的索引也可以具有不同种类的页。例如:B树具有元数据页面和“普通”页面。

    但是,索引页通常具有一个指向行和行本身的指针数组(就像表页一样)。此外,页面末尾的一些空间还分配给特殊数据。

    根据索引类型,索引中的行也可以具有不同的结构。例如:在B树中,与叶子页相关的行包含索引键的值和对相应表行的引用(ctid)。通常,索引的结构可以完全不同。

    要点是,在任何类型的索引中都没有行版本。或者我们可以考虑每一行仅由一个版本表示。换句话说,索引行的header不包含xmin和xmax字段。现在,我们可以假定从索引指向表行的所有版本的引用。因此,要确定事务中可见哪些行版本,PostgreSQL需要查看表。(像往常一样,这不是全部内容。有时可见性视图可以优化流程,但我们稍后会讨论)

    在这里,在索引页面中,我们找到两个版本的指针:最新版本和之前版本:

    => SELECT itemoffset, ctid FROM bt_page_items('t_s_idx',1);
     itemoffset | ctid  
    ------------+-------
              1 | (0,2)
              2 | (0,1)
    (2 rows)
    

    (这里,我做过测试,对表执行vacuum full之后,再次查询就只有一条记录了。至于可见性视图优化流程暂时没有测试!!!)

    虚拟事务(virtual transactions)

    实际上,PostgreSQL利用了优化的优势,该优化允许“少量”消耗事务ID。

    如果事务仅读取数据,则根本不影响元组的可见性。 因此,首先,后端进程将虚拟ID(虚拟xid)分配给事务。该ID由进程标识符和序列号组成。

    分配此虚拟ID不需要所有进程之间的同步,因此可以非常快速地执行。 在讨论freezing时,我们将了解使用虚拟ID的另一个原因。

    数据快照根本不考虑虚拟ID。

    在不同的时间点,系统可以使用已经使用过的id进行虚拟事务,这很好。但是这个ID不能写入数据页,因为当下一次访问该页时,这个ID可能会变得毫无意义。

    => BEGIN;
    => SELECT txid_current_if_assigned();
     txid_current_if_assigned 
    --------------------------
                             
    (1 row)
    

    但是,如果事务开始更改数据,它将接收一个真实的、惟一的事务ID。

    => UPDATE accounts SET amount = amount - 1.00;
    => SELECT txid_current_if_assigned();
     txid_current_if_assigned 
    --------------------------
                         3667
    (1 row)
    
    => COMMIT;
    

      

    子事务(Subtransactions)

    检查点(savepoints)

    在SQL中,定义了savepoint,这些保存点允许回滚事务的某些操作而不会完全中止。但这与上述模型不兼容,因为事务状态是所有更改的结果之一,并且没有物理回滚的数据。

    为了实现此功能,带有保存点的事务被分为几个单独的子事务,其状态可以分别进行管理。

    子事务具有自己的ID(大于主事务的ID)。子事务的状态以通常的方式写入XACT,但最终状态取决于主事务的状态:如果回滚,则所有子事务也将回滚。

    有关子事务嵌套的信息存储在PGDATA/pg_subtrans目录的文件中。这些文件是通过实例共享内存中的缓冲区访问的,这些缓冲区的结构与XACT缓冲区相同。

    不要将子事务与匿名事务混淆。匿名事务绝不相互依赖,而子事务却相互依赖。常规PostgreSQL中没有匿名事务:实际上很少需要它们,并且它们在其他DBMS中的可用性会导致滥用,每个人都会遭受痛苦。

    让我们清除表,开始事务并插入一行:

    => TRUNCATE TABLE t;
    => BEGIN;
    => INSERT INTO t(s) VALUES ('FOO');
    => SELECT txid_current();
     txid_current 
    --------------
             3669
    (1 row)
    
    => SELECT xmin, xmax, * FROM t;
     xmin | xmax | id |  s  
    ------+------+----+-----
     3669 |    0 |  2 | FOO
    (1 row)
    
    => SELECT * FROM heap_page('t',0);
     ctid  | state  | xmin | xmax  | t_ctid 
    -------+--------+------+-------+--------
     (0,1) | normal | 3669 | 0 (a) | (0,1)
    (1 row)
    

    现在我们建立一个保存点并插入另一行:

    => SAVEPOINT sp;
    => INSERT INTO t(s) VALUES ('XYZ');
    => SELECT txid_current();
     txid_current 
    --------------
             3669
    (1 row)
    

    请注意,txid_current函数返回主事务的ID,而不是子事务的ID。

    => SELECT xmin, xmax, * FROM t;
     xmin | xmax | id |  s  
    ------+------+----+-----
     3669 |    0 |  2 | FOO
     3670 |    0 |  3 | XYZ
    (2 rows)
    
    => SELECT * FROM heap_page('t',0);
     ctid  | state  | xmin | xmax  | t_ctid 
    -------+--------+------+-------+--------
     (0,1) | normal | 3669 | 0 (a) | (0,1)
     (0,2) | normal | 3670 | 0 (a) | (0,2)
    (2 rows)
    

    让我们回滚到保存点并插入第三行:

    => ROLLBACK TO sp;
    => INSERT INTO t VALUES ('BAR');
    => SELECT xmin, xmax, * FROM t;
     xmin | xmax | id |  s  
    ------+------+----+-----
     3669 |    0 |  2 | FOO
     3671 |    0 |  4 | BAR
    (2 rows)
    
    => SELECT * FROM heap_page('t',0);
     ctid  | state  |   xmin   | xmax  | t_ctid 
    -------+--------+----------+-------+--------
     (0,1) | normal | 3669     | 0 (a) | (0,1)
     (0,2) | normal | 3670 (a) | 0 (a) | (0,2)
     (0,3) | normal | 3671     | 0 (a) | (0,3)
    (3 rows)
    

    在该页中,我们继续看到回滚子事务添加的行。

    提交更改:

    => COMMIT;
    => SELECT xmin, xmax, * FROM t;
     xmin | xmax | id |  s  
    ------+------+----+-----
     3669 |    0 |  2 | FOO
     3671 |    0 |  4 | BAR
    (2 rows)
    
    => SELECT * FROM heap_page('t',0);
     ctid  | state  |   xmin   | xmax  | t_ctid 
    -------+--------+----------+-------+--------
     (0,1) | normal | 3669 (c) | 0 (a) | (0,1)
     (0,2) | normal | 3670 (a) | 0 (a) | (0,2)
     (0,3) | normal | 3671 (c) | 0 (a) | (0,3)
    (3 rows)
    

    现在可以清楚地看到,每个子事务都有自己的状态。

    注意,SQL不允许显式使用子事务,也就是说,在完成当前事务之前不能启动新事务。在使用保存点、处理PL/pgSQL异常以及其他一些更奇怪的情况下,这种技术会隐式地涉及到。

    => BEGIN;
    BEGIN
    => BEGIN;
    WARNING:  there is already a transaction in progress
    BEGIN
    => COMMIT;
    COMMIT
    => COMMIT;
    WARNING:  there is no transaction in progress
    COMMIT
    

    错误和原子性操作

    如果在执行操作时发生错误,会发生什么?例如,像这样:

    => BEGIN;
    => SELECT * FROM t;
     id |  s  
    ----+-----
      2 | FOO
      4 | BAR
    (2 rows)
    
    => UPDATE t SET s = repeat('X', 1/(id-4));
    ERROR:  division by zero
    

    一个错误发生。现在事务被视为中止,不允许任何操作:

    => SELECT * FROM t;
    ERROR:  current transaction is aborted, commands ignored until end of transaction block
    

    即使我们尝试提交更改,PostgreSQL也会报告回滚:

    => COMMIT;
    ROLLBACK
    

    为什么失败后无法继续执行事务? 问题是可能发生错误,以至我们可以访问部分更改,也就是说,不仅对于事务,甚至对于单个操作,原子性都将被破坏。例如,在我们的示例中,操作可以在发生错误之前更新一行:

    => SELECT * FROM heap_page('t',0);
     ctid  | state  |   xmin   | xmax  | t_ctid 
    -------+--------+----------+-------+--------
     (0,1) | normal | 3669 (c) | 3672  | (0,4)
     (0,2) | normal | 3670 (a) | 0 (a) | (0,2)
     (0,3) | normal | 3671 (c) | 0 (a) | (0,3)
     (0,4) | normal | 3672     | 0 (a) | (0,4)
    (4 rows)
    

    值得注意的是,psql有一种模式,它允许在失败后继续事务,就像错误操作符的影响被回滚一样。

    => set ON_ERROR_ROLLBACK on
    => BEGIN;
    => SELECT * FROM t;
     id |  s  
    ----+-----
      2 | FOO
      4 | BAR
    (2 rows)
    
    => UPDATE t SET s = repeat('X', 1/(id-4));
    ERROR:  division by zero
    
    => SELECT * FROM t;
     id |  s  
    ----+-----
      2 | FOO
      4 | BAR
    (2 rows)
    
    => COMMIT;
    

    很容易看出,在这种模式下,psql实际上在每个命令之前建立一个隐式保存点,并在失败时对其发起回滚。默认情况下不使用此模式,因为建立保存点(即使不回滚保存点)会带来很大的开销。

    原文地址:

    https://habr.com/en/company/postgrespro/blog/477648/

  • 相关阅读:
    关于SEL数据类型的简单知识点
    小结RunLoop
    iOS-静态库的创建与使用
    MRC 下block 小结
    Native与H5交互的一些解决方法
    iOS UIPickerView 显示全国省市
    iOS开发 首次启动显示用户引导,第二次启动直接进入App,UIScrollView,UIPageControl,NSUserDefaults
    去掉tableView的header view的粘黏性
    黑苹果-IOS学习的开始
    IOS中程序如何进行推送消息(本地推送,远程推送)
  • 原文地址:https://www.cnblogs.com/abclife/p/13524044.html
Copyright © 2011-2022 走看看