暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

PostgreSQL的事务及其实现机制

事务是数据库的最基本概念,在PostgreSQL中使用begin;end;命令可以开启和提交事务,当然这是最常见的PostgreSQL事务,除此之外PostgreSQL中还有子事务、多事务、2PC事务的概念。在本文中,我将会演示这些PostgreSQL事务的出现场景和内核实现方式。

普通事务

使用PostgreSQL客户端连接至PostgreSQL服务器,是默认开启事务自动提交的,也就是说每执行一条DML会自动完成提交过程。用户可以使用\set AUTOCOMMIT off命令关闭自动提交,也可以使用begin命令开启一个事务块。

    \echo :AUTOCOMMIT
    create table t1(i int);
    insert into t1 values(1);
    rollback;
    select * from t1;


    \set AUTOCOMMIT off
    \echo :AUTOCOMMIT
    insert into t1 values(2);
    rollback;
    select * from t1;
    insert into t1 values(22);
    commit;
    select * from t1;
    commit;


    \set AUTOCOMMIT on
    \echo :AUTOCOMMIT
    begin;
    insert into t1 values(3);
    rollback;
    select * from t1;
    begin;
    insert into t1 values(33);
    commit;
    select * from t1;

      postgres=# \echo :AUTOCOMMIT
      on
      postgres=# create table t1(i int);
      CREATE TABLE
      postgres=# insert into t1 values(1);
      INSERT 0 1
      postgres=# rollback;
      2020-06-08 15:15:50.261 CST [29689] WARNING: there is no transaction in progress
      WARNING: there is no transaction in progress
      ROLLBACK
      postgres=# select * from t1;
      i
      ---
      1
      (1 row)


      postgres=#

      --自动提交开启的情况下,我们执行的DML语句已成功提交,rollback没有对我们造成影响

        postgres=# \set AUTOCOMMIT off
        postgres=# \echo :AUTOCOMMIT
        off
        postgres=# insert into t1 values(2);
        INSERT 0 1
        postgres=# rollback;
        ROLLBACK
        postgres=# select * from t1;
        i
        ---
        1
        (1 row)

        postgres=# insert into t1 values(22);
        INSERT 0 1
        postgres=# commit;
        COMMIT
        postgres=# select * from t1;
        i
        ----
        1
        22
        (2 rows)

        postgres=#

        --自动提交关闭的情况下,我们执行的DML语句都被rollback回滚,也可被commit提交 

          postgres=# commit;
          COMMIT
          postgres=#
          postgres=# \set AUTOCOMMIT on
          postgres=# \echo :AUTOCOMMIT
          on
          postgres=# begin;
          BEGIN
          postgres=# insert into t1 values(3);
          INSERT 0 1
          postgres=# rollback;
          ROLLBACK
          postgres=# select * from t1;
          i
          ----
          1
          22
          (2 rows)

          postgres=# begin;
          BEGIN
          postgres=# insert into t1 values(33);
          INSERT 0 1
          postgres=# commit;
          COMMIT
          postgres=# select * from t1;
          i
          ----
          1
          22
          33
          (3 rows)

          postgres=#
          ----在begin事务块,我们执行的DML语句可以被rollback回滚,也可被commit提交

           

          现在我们借助pageinspact工具来看t1表的数据

            postgres=# select lp,t_xmin,t_xmax,t_ctid,t_infomask,t_data from 
            heap_page_items(get_raw_page('t1',0));
            lp | t_xmin | t_xmax | t_ctid | t_infomask | t_data
            ----+--------+--------+--------+------------+------------
            1 | 625 | 0 | (0,1) | 2304 | \x01000000
            2 | 626 | 0 | (0,2) | 2560 | \x02000000
            3 | 627 | 0 | (0,3) | 2304 | \x16000000
            4 | 628 | 0 | (0,4) | 2560 | \x03000000
            5 | 629 | 0 | (0,5) | 2304 | \x21000000
            (5 rows)
            postgres=# select ctid,xmin,* from t1;
            ctid | xmin | i
            -------+------+----
            (0,1) | 625 | 1
            (0,3) | 627 | 22
            (0,5) | 629 | 33
            (3 rows)


            postgres=#

             

            通过pageinspact工具可以看到t1表有5条数据,但是查询结果来看t1有3条数据。根据我们上面的操作可以得知,事务626和事务628被rollback了,因此PostgreSQL的MVCC机制根据事务626和事务628的提交状态判断t_ctid=(0,2)和t_ctid=(0,4)的两条记录是不可见的。这里不探讨MVCC这个庞大的机制,我们的关注点在PostgreSQL如何获取每一个事务的提交状态。

             

            在PostgreSQL的PGDATA目录下有pg_xact文件夹:

              movead@movead-PC:/h2/data/pg_xact$ ll
              -rw-------+ 1 movead movead 8192 68 15:23 0000
              movead@movead-PC:/h2/data/pg_xact$

               

              这里的文件记录了事务的提交状态。2bit记录一个事务的状态因此一个byte可以记录4个事务,这里每一个文件的size在内核中规定为32*BLCKSZ,如上所示0000文件所记录的事务ID范围是(1~32*BLCKSZ*4)。因此想要获取一个事务的提交状态,到pg_xact目录下取就可以了。

               

              子事务

              子事务是伴随着savepoint或者带有 exception的函数出现的,我们用savepoint来举例说明

                postgres=# truncate t1;
                TRUNCATE TABLE
                postgres=# select txid_current();
                txid_current
                --------------
                10495
                (1 row)

                postgres=# begin;
                BEGIN
                postgres=# select txid_current();
                txid_current
                --------------
                10496
                (1 row)

                postgres=# insert into t1 values(1);
                INSERT 0 1
                postgres=# savepoint s1;
                SAVEPOINT
                postgres=# insert into t1 values(2);
                INSERT 0 1
                postgres=# savepoint s2;
                SAVEPOINT
                postgres=# insert into t1 values(3);
                INSERT 0 1
                postgres=# savepoint s3;
                SAVEPOINT
                postgres=# insert into t1 values(4);
                INSERT 0 1
                postgres=# select txid_current();
                txid_current
                --------------
                10496
                (1 row)

                postgres=# commit;
                COMMIT
                postgres=# select ctid,xmin,* from t1;
                ctid | xmin | i
                -------+-------+---
                (0,1) | 10496 | 1
                (0,2) | 10497 | 2
                (0,3) | 10498 | 3
                (0,4) | 10499 | 4
                (4 rows)

                postgres=#

                 

                我们可以看到,我们在同一个事务块中插入的四条记录拥有不同的事务ID,这其中第一个事务10496是他们的父事务,10497,10498,10499是子事务。子事务同时也是一个特殊的普通事务,他们也有提交状态,也会将状态刷写到pg_xact目录中,在10496父事务提交或者abort后,这些子事务的行为与普通事务一致。

                有所不同的是提交过程。所有父事务和子事务,最终都要标记为commit或者abort状态(中间rollback的事务,可以不看为是子事务)。为了保证整个父事务和子事务原子性,PostgreSQL为子事务的提交设计了一套提交机制:

                1.首先将子事务状态标记为‘子事务提交’(TRANSACTION_STATUS_SUB_COMMITTED)

                2.将父事务状态标记为‘已提交’(TRANSACTION_STATUS_COMMITTED)

                3.将子事务状态标记为‘已提交’(TRANSACTION_STATUS_COMMITTED)

                 

                判断一个子事务是否提交的逻辑是:

                如果子事务在pg_xact目录中记录的状态为‘子事务提交’(TRANSACTION_STATUS_SUB_COMMITTED),那么需要去pg_subtrans目录下查找其父事务ID,根据父事务的提交状态判断子事务的提交状态;

                 

                如果子事务在pg_xact目录中记录的状态为‘已提交’(TRANSACTION_STATUS_COMMITTED),那么子事务为已提交状态;

                 

                子事务在pg_xact目录中记录的状态为其他情况时,都是未提交。

                 

                最后我们来看一下pg_subtrans这个记录子事务与父事务对应关系的目录:

                  movead@movead-PC:/h2/data/pg_subtrans$ ll
                  -rw-------+ 1 movead movead 49152 68 17:33 0000
                  movead@movead-PC:/h2/data/pg_subtrans$

                   

                  在这个文件里,使用4byte记录一个子事务父事务ID,这里每一个文件的size在内核中规定为32*BLCKSZ,如上所示0000文件所记录的事务ID范围是(1~32*BLCKSZ/4)。

                   

                  多事务

                  多事务是因为行级锁出现的,当有多个session对同一行记录添加行级锁时,就会出现多事务。下面我们使用一个例子来演示一下多事务的出现。

                    -- 创建测试表并插入数据,并使用pageinspact工具查看数据在磁盘上的分布
                    postgres=# create table t1(i int, j text);
                    CREATE TABLE
                    postgres=# insert into t1 values(1,'PostgreSQL');
                    INSERT 0 1
                    postgres=# insert into t1 values(2,'Postgres');
                    INSERT 0 1
                    postgres=# insert into t1 values(3,'pg');
                    INSERT 0 1
                    postgres=# select t_ctid,t_infomask2,t_infomask,t_xmin,t_xmax,t_data from heap_page_items(get_raw_page('t1',0));
                    t_ctid | t_infomask2 | t_infomask | t_xmin | t_xmax | t_data
                    --------+-------------+------------+--------+--------+----------------------------------
                    (0,1) | 2 | 2050 | 500 | 0 | \x0100000017506f737467726553514c
                    (0,2) | 2 | 2050 | 501 | 0 | \x0200000013506f737467726573
                    (0,3) | 2 | 2050 | 502 | 0 | \x03000000077067
                    (3 rows)


                    postgres=#



                    -- 在一个session中开启事务块,然后对一行数据加锁
                    postgres=# begin;
                    BEGIN
                    postgres=# select * from t1 where i = 2 for share;
                    i | j
                    ---+----------
                    2 | Postgres
                    (1 row)


                    postgres=# select t_ctid,t_infomask2,t_infomask,t_xmin,t_xmax,t_data from heap_page_items(get_raw_page('t1',0));
                    t_ctid | t_infomask2 | t_infomask | t_xmin | t_xmax | t_data
                    --------+-------------+------------+--------+--------+----------------------------------
                    (0,1) | 2 | 2306 | 500 | 0 | \x0100000017506f737467726553514c
                    (0,2) | 2 | 466 | 501 | 504 | \x0200000013506f737467726573
                    (0,3) | 2 | 2306 | 502 | 0 | \x03000000077067
                    (3 rows)
                    postgres=# select txid_current();
                    txid_current
                    --------------
                    504
                    postgres=#

                     

                    这里我们可以看到t_ctid=(0,2)这行数据的t_xmax发生了变化。

                      -- 在另外一个session中,做同样的操作
                      postgres=# begin;
                      BEGIN
                      postgres=# select * from t1 where i = 2 for share;
                      i | j
                      ---+----------
                      2 | Postgres
                      (1 row)

                      postgres=# select txid_current();
                      txid_current
                      --------------
                      505
                      (1 row)

                      postgres=# select t_ctid,t_infomask2,t_infomask,t_xmin,t_xmax,t_data from
                      heap_page_items(get_raw_page('t1',0));
                      t_ctid | t_infomask2 | t_infomask | t_xmin | t_xmax | t_data
                      --------+-------------+------------+--------+--------+----------------------------------
                      (0,1) | 2 | 2306 | 500 | 0 | \x0100000017506f737467726553514c
                      (0,2) | 2 | 4562 | 501 | 2 | \x0200000013506f737467726573
                      (0,3) | 2 | 2306 | 502 | 0 | \x03000000077067
                      (3 rows)

                      postgres=#

                       

                      我们发现,t_ctid=(0,2)这行数据的t_xmax变成了2,这个2就是一个多事务,表示有多个事务(504,505)锁定了这一行,多事务2是否提交就要看t_infomask的值与(504,505)的提交状态。下面我们就探究一下PostgreSQL中这个2与(504,505)是如何联系在一起的。

                        movead@movead-PC:/h2/data/pg_multixact$ tree .
                        .
                        ├── members
                        │ └── 0000
                        └── offsets
                        └── 0000

                        2 directories, 2 files
                        movead@movead-PC:/h2/data/pg_multixact$

                        在数据目录里有如上所示的文件,这里存储了多事务与其对应的事务列表的映射关系。事务列表中的事务的属性决定了多事务的提交状态。

                         

                        2PC事务

                        两阶段提交(2PC)事务是实现分布式提交的必要条件,同样下面将会演示什么是2PC事务,以及PostgreSQL是如何实现2PC事务的。

                          postgres=# begin;
                          BEGIN
                          postgres=# insert into t1 values(1,'PostgreSQL');
                          INSERT 0 1
                          postgres=#
                          postgres=# select * from t1;
                          i | j
                          ---+------------
                          1 | PostgreSQL
                          (1 row)

                          postgres=# select t_ctid,t_infomask2,t_infomask,t_xmin,t_xmax,t_data from
                          heap_page_items(get_raw_page('t1',0));
                          t_ctid | t_infomask2 | t_infomask | t_xmin | t_xmax | t_data
                          --------+-------------+------------+--------+--------+----------------------------------
                          (0,1) | 2 | 2050 | 537 | 0 | \x0100000017506f737467726553514c
                          (1 row)

                          postgres=# prepare transaction 'test_2pc_trans';
                          PREPARE TRANSACTION
                          postgres=# select * from t1;
                          i | j
                          ---+---
                          (0 rows)

                          postgres=# commit prepared 'test_2pc_trans';
                          COMMIT PREPARED
                          postgres=# select * from t1;
                          i | j
                          ---+------------
                          1 | PostgreSQL
                          (1 row)

                          postgres=#

                           

                          在同一个session中执行如上SQL,prepare transaction 'test_2pc_trans'是第一阶段提交,commit prepared 'test_2pc_trans';是第二阶段提交。

                          一阶段提交后我们已经退出了事务块,而事务没有完全提交所以我们无法查询到事务块内插入的数据,二阶段提交后,2PC事务已完成,所以又重新获取到了数据。

                           

                          2PC事务也是一种特殊的普通事务,2PC事务提交状态的判断与正常事务一样,只不过2PC事务有‘自我保护机制’:2PC事务可以独立于session连接而存活,即使数据库关闭也不会影响2PC事务的状态。为了实现这个保护机制,PostgreSQL会为长时间存在2PC事务创建保存事务数据的文件:

                            movead@movead-PC:/h2/data/pg_twophase$ ll
                            -rw-------+ 1 movead movead 252 69 16:17 00000219
                            movead@movead-PC:/h2/data/pg_twophase$

                            如上00000219文件就是为2PC事务537创建的存储文件,PostgreSQL如果发生重启,在恢复到正常状态之前会从pg_twophase目录加载所有的2PC事务到内存中。

                            后记

                            本文从相对简单的角度记录了PostgreSQL的事务,子事务,多事务,2PC事务的使用方法和每一种事务的特性和其存储方式。 

                            本文作者:李传成,中国PG分会认证专家,瀚高软件资深内核研发工程师

                             

                            I Love PG

                            PostgreSQLPG2017PostgreSQLPGPostgreSQLPostgreSQL


                            稿

                            做你的舞台,show出自己的才华 。

                            投稿邮箱:partner@postgresqlchina.com

                                                

                                                ——愿能安放你不羁的灵魂

                            技术文章精彩回顾




                            PostgreSQL学习的九层宝塔
                            PostgreSQL职业发展与学习攻略
                            2019,年度数据库舍 PostgreSQL 其谁?
                            Postgres是最好的开源软件
                            PostgreSQL是世界上最好的数据库
                            从Oracle迁移到PostgreSQL的十大理由
                            从“非主流”到“潮流”,开源早已值得拥有

                            PG活动精彩回顾




                            创建PG全球生态!PostgresConf.CN2019大会盛大召开
                            首站起航!2019“让PG‘象’前行”上海站成功举行
                            走进蓉城丨2019“让PG‘象’前行”成都站成功举行
                            中国PG象牙塔计划发布,首批合作高校授牌仪式在天津举行
                            群英论道聚北京,共话PostgreSQL
                            相聚巴厘岛| PG Conf.Asia 2019  DAY0、DAY1简报
                            相知巴厘岛| PG Conf.Asia 2019 DAY2简报
                            独家|硅谷Postgres大会简报
                            直播回顾 | Bruce Momjian:原生分布式将在PG 14版本发布

                            PG培训认证精彩回顾




                            中国首批PGCA认证考试圆满结束,203位考生成功获得认证!
                            中国第二批PGCA认证考试圆满结束,115位考生喜获认证!
                            重要通知:三方共建,中国PostgreSQL认证权威升级!
                            近500人参与!首次PGCE中级、第三批次PGCA初级认证考试落幕!
                            2020年首批 | 中国PostgreSQL初级认证考试圆满结束
                            一分耕耘一分收获,第五批次PostgreSQL认证考试成绩公布

                            文章转载自开源软件联盟PostgreSQL分会,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                            评论