ICode9

精准搜索请尝试: 精确搜索
首页 > 数据库> 文章详细

PostgreSQL的MVCC(3)--Row Versions

2020-08-26 17:01:19  阅读:314  来源: 互联网

标签:事务 PostgreSQL normal -- xmax xmin ctid SELECT Row


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

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/

标签:事务,PostgreSQL,normal,--,xmax,xmin,ctid,SELECT,Row
来源: https://www.cnblogs.com/abclife/p/13524044.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有