事务是数据库的最基本概念,在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 6月 8 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 6月 8 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 6月 9 16:17 00000219
movead@movead-PC:/h2/data/pg_twophase$
如上00000219文件就是为2PC事务537创建的存储文件,PostgreSQL如果发生重启,在恢复到正常状态之前会从pg_twophase目录加载所有的2PC事务到内存中。
后记
本文从相对简单的角度记录了PostgreSQL的事务,子事务,多事务,2PC事务的使用方法和每一种事务的特性和其存储方式。
本文作者:李传成,中国PG分会认证专家,瀚高软件资深内核研发工程师
I Love PG
关于我们
中国开源软件推进联盟PostgreSQL分会(简称:中国PG分会)于2017年成立,由国内多家PostgreSQL生态企业所共同发起,业务上接受工信部中国电子信息产业发展研究院指导。中国PG分会是一个非盈利行业协会组织。我们致力于在中国构建PostgreSQL产业生态,推动PostgreSQL产学研用发展。
技术文章精彩回顾 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认证考试成绩公布