前言
今天有位朋友在群里提到了一个XA事务的问题,表现很怪异,将我绕了进去。话不多说,看看现象
现象
操作的SQL顺序如下
1.begin;
2.drop table if exists t1;
3.create table t1(id int);
4.insert into t1 values(1),(2);
5.prepare transaction 'pt1';
6.select * from pg_prepared_xacts;
7.select * from t1;
8.commit prepared 'pt1';
我们按照这个顺序来操作一下
postgres=# select * from pg_prepared_xacts ;
transaction | gid | prepared | owner | database
-------------+-----+----------+-------+----------
(0 rows)
postgres=# begin;
BEGIN
postgres=*# drop table if exists t1;
NOTICE: table "t1" does not exist, skipping
DROP TABLE
postgres=*# create table t1(id int);
CREATE TABLE
postgres=*# insert into t1 values(1),(2);
INSERT 0 2
postgres=*# prepare transaction 'pt1';
PREPARE TRANSACTION
postgres=# select * from pg_prepared_xacts;
transaction | gid | prepared | owner | database
-------------+-----+-------------------------------+----------+----------
1036 | pt1 | 2022-03-10 10:39:32.885823+08 | postgres | postgres
(1 row)
postgres=# select * from t1;
ERROR: relation "t1" does not exist
LINE 1: select * from t1;
^
postgres=# commit prepared 'pt1';
COMMIT PREPARED
postgres=# \d t1
Table "public.t1"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
id | integer | | |
这个情况,符合我们的预期。因为还没有提交XA事务,表是看不到的。
那么再操作一次会怎样呢?这一次就被阻塞了。
postgres=# begin;
BEGIN
postgres=*# drop table if exists t1;
DROP TABLE
postgres=*# create table t1(id int);
CREATE TABLE
postgres=*# insert into t1 values(1),(2);
INSERT 0 2
postgres=*# prepare transaction 'pt1';
PREPARE TRANSACTION
postgres=# select * from pg_prepared_xacts;
transaction | gid | prepared | owner | database
-------------+-----+------------------------------+----------+----------
1037 | pt1 | 2022-03-10 10:41:47.49236+08 | postgres | postgres
(1 row)
postgres=# select * from t1;
---此处被阻塞
让我们看下被什么阻塞
postgres=# select pg_blocking_pids(pid) as blocked,wait_event_type,wait_event,backend_xid,backend_xmin,query from pg_stat_activity where pid = 22154;
-[ RECORD 1 ]---+------------------
blocked | {0}
wait_event_type | Lock
wait_event | relation
backend_xid |
backend_xmin | 1041
query | select * from t1;
可以看到,在等待锁,同时被0号pid阻塞了?0号pid十分具有迷惑性。回顾一下以前的文章,0号pid代表着XA事务
postgres=# select * from pg_prepared_xacts ;
-[ RECORD 1 ]------------------------------
transaction | 1041
gid | pt1
prepared | 2022-03-10 10:47:21.893145+08
owner | postgres
database | postgres
在PostgreSQL内部对应的则是2PC,两阶段提交,也可以叫做prepare transaction(预备事务)。
预备事务是独立于会话,抗崩溃和状态维护的事务。事务的状态会持久化在磁盘上,这一点不难理解,因为两阶段提交中一般都涉及多台数据库之间的协同,各台数据库收到prepare transaction的命令后,如果要返回成功,那么该节点必须要确保自己后续能在被要求提交事务的时候去提交,或后续能在被要求回滚的时候回滚,所以PostgreSQL需要把相关信息持久化到存储上,随时响应。既然持久化了,那么数据库服务器即使从崩溃中重新启动后也可以恢复事务,直到在对prepared transaction执行回滚或提交操作之前,将一直维护该事务。
[postgres@xiongcc ~]$ pg_ctl -D pgdata/ stop
waiting for server to shut down.... done
server stopped
[postgres@xiongcc ~]$ ll pgdata/pg_twophase/
total 4
-rw------- 1 postgres postgres 1412 Mar 10 10:54 00000411
重启之后,还是可以看到这个预备事务
postgres=# select * from pg_prepared_xacts ;
-[ RECORD 1 ]------------------------------
transaction | 1041
gid | pt1
prepared | 2022-03-10 10:47:21.893145+08
owner | postgres
database | postgres
那么为什么我操作两次的结果会不一样呢?第一次直接提示表不存在,第二次会阻塞?
第一次操作
不难分析,第一次因为t1表不存在,不过值得注意的是,XA事务也会对系统表写入。为了演示,先vacuum full一下
postgres=# vacuum full;
VACUUM
postgres=# select relpages,reltuples from pg_class where relname = 'pg_class';
relpages | reltuples
----------+-----------
26 | 411
(1 row)
postgres=# begin;
BEGIN
postgres=*# drop table if exists t1;
NOTICE: table "t1" does not exist, skipping
DROP TABLE
postgres=*# create table t1(id int);
CREATE TABLE
postgres=*# insert into t1 values(1),(2);
INSERT 0 2
postgres=*# prepare transaction 'pt1';
PREPARE TRANSACTION
postgres=# select * from pg_prepared_xacts;
transaction | gid | prepared | owner | database
-------------+-----+-------------------------------+----------+----------
1521 | pt1 | 2022-03-10 11:28:13.080183+08 | postgres | postgres
(1 row)
postgres=# select * from t1;
ERROR: relation "t1" does not exist
LINE 1: select * from t1;
^
然后使用pageinspect观察一下pg_class这个系统表,此处直接观察最后一个数据块。
postgres=# SELECT tuple_data_split('pg_class'::regclass, t_data, t_infomask, t_infomask2, t_bits) FROM heap_page_items(get_raw_page('pg_class', 25)) limit 10 offset 29;
tuple_data_split
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
{"\\xeb040000","\\x70675f636c6173730000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","\\x0b000000","\\x53000000","\\x00000000","\\x0a000000","\\x02000000","\\x00000000","\\x00000000","\\x1a000000","\\x0080cd43","\\x19000000","\\x00000000","\\x01","\\x00","\\x70","\\x72","\\x2100","\\x0000","\\x00","\\x00","\\x00","\\x00","\\x00","\\x01","\\x6e","\\x00","\\x00000000","\\x95070000","\\x01000000","\\x5b01000000000000000904000002000000010000000a0000000a0000007f000000000000000a00000002000000",NULL,NULL}
{"\\x89550000","\\
x7431
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","\\x98080000","\\x8b550000","\\x00000000","\\x0a000000","\\x02000000","\\x89550000","\\x00000000","\\x00000000","\\x000080bf","\\x00000000","\\x00000000","\\x00","\\x00","\\x70","\\x72","\\x0100","\\x0000","\\x00","\\x00","\\x00","\\x00","\\x00","\\x01","\\x64","\\x00","\\x00000000","\\xe2070000","\\x01000000",NULL,NULL,NULL}
(2 rows)
最后一行的\0x7431即十六进制的"t1",可以看到,pg_class里面也写入了这条数据。
postgres=# commit prepared 'pt1';
COMMIT PREPARED
postgres=# \d t1
Table "public.t1"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
id | integer | | |
postgres=# select 1 from pg_class where relname = 't1';
?column?
----------
1
(1 row)
当然,假如回滚这个预备事务,pg_class里面会留有这个"死元组",所以也要注意系统表的膨胀。
postgres=# rollback prepared 'pt1';
ROLLBACK PREPARED
postgres=# \d t1
Did not find any relation named "t1".
postgres=# SELECT tuple_data_split('pg_class'::regclass, t_data, t_infomask, t_infomask2, t_bits) FROM heap_page_items(get_raw_page('pg_class', 25)) limit 10 offset 29;
tuple_data_split
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
{"\\xeb040000","\\x70675f636c6173730000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","\\x0b000000","\\x53000000","\\x00000000","\\x0a000000","\\x02000000","\\x00000000","\\x00000000","\\x1a000000","\\x0080cd43","\\x19000000","\\x00000000","\\x01","\\x00","\\x70","\\x72","\\x2100","\\x0000","\\x00","\\x00","\\x00","\\x00","\\x00","\\x01","\\x6e","\\x00","\\x00000000","\\x95070000","\\x01000000","\\x5b01000000000000000904000002000000010000000a0000000a0000007f000000000000000a00000002000000",NULL,NULL}
{"\\x89550000","\\x74310000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","\\x98080000","\\x8b550000","\\x00000000","\\x0a000000","\\x02000000","\\x89550000","\\x00000000","\\x00000000","\\x000080bf","\\x00000000","\\x00000000","\\x00","\\x00","\\x70","\\x72","\\x0100","\\x0000","\\x00","\\x00","\\x00","\\x00","\\x00","\\x01","\\x64","\\x00","\\x00000000","\\xe2070000","\\x01000000",NULL,NULL,NULL}
(2 rows)
postgres=# vacuum pg_class;
VACUUM
postgres=# SELECT tuple_data_split('pg_class'::regclass, t_data, t_infomask, t_infomask2, t_bits) FROM heap_page_items(get_raw_page('pg_class', 25)) limit 10 offset 29;
tuple_data_split
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
{"\\xeb040000","\\x70675f636c6173730000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","\\x0b000000","\\x53000000","\\x00000000","\\x0a000000","\\x02000000","\\x00000000","\\x00000000","\\x1a000000","\\x0080cd43","\\x1a000000","\\x00000000","\\x01","\\x00","\\x70","\\x72","\\x2100","\\x0000","\\x00","\\x00","\\x00","\\x00","\\x00","\\x01","\\x6e","\\x00","\\x00000000","\\x95070000","\\x01000000","\\x5b01000000000000000904000002000000010000000a0000000a0000007f000000000000000a00000002000000",NULL,NULL}
(1 row)
第二次操作
postgres=# select * from pg_prepared_xacts;
transaction | gid | prepared | owner | database
-------------+-----+----------+-------+----------
(0 rows)
postgres=# begin;
BEGIN
postgres=*# drop table if exists t1;
DROP TABLE
postgres=*# create table t1(id int);
CREATE TABLE
postgres=*# insert into t1 values(1),(2);
INSERT 0 2
postgres=*# prepare transaction 'pt1';
PREPARE TRANSACTION
postgres=# select * from pg_prepared_xacts;
transaction | gid | prepared | owner | database
-------------+-----+-------------------------------+----------+----------
1526 | pt1 | 2022-03-10 11:32:59.955417+08 | postgres | postgres
(1 row)
postgres=# select * from t1;
---被阻塞
那么为什么第二次被阻塞了?因为第一次操作已经创建了t1表,同时pt1预备事务因为执行了drop操作,已经对表持有了AccessExclusiveLock(注意这里,pid是空的)
postgres=# select * from pg_locks where relation='t1'::regclass;
-[ RECORD 1 ]------+------------------------------
locktype | relation
database | 13890
relation | 19297
page |
tuple |
virtualxid |
transactionid |
classid |
objid |
objsubid |
virtualtransaction | 3/105
pid | 22632
mode | AccessShareLock
granted | f
fastpath | f
waitstart | 2022-03-10 11:32:59.956774+08
-[ RECORD 2 ]------+------------------------------
locktype | relation
database | 13890
relation | 19297
page |
tuple |
virtualxid |
transactionid |
classid |
objid |
objsubid |
virtualtransaction | -1/1526
pid |
mode | AccessExclusiveLock
granted | t
fastpath | f
waitstart |
提交或者回滚XA事务
postgres=# rollback prepared 'pt1';
ROLLBACK PREPARED
查询即可正常执行了
postgres=# select * from t1;
id
----
1
2
(2 rows)
小结
这个案例为什么最开始把我搞懵了,主要是这个SQL的顺序,不管你是否提交预备事务,查询(假如成功的话)都是两条数据。🤣🤣🤣
除此之外,还得知晓2PC的危害,占着茅坑不拉屎:
•假如执行了DML,就会持有相应的锁,直到释放为止,所以会产生锁冲突,更糟心的是做了DDL又忘了提交•同理,假如持有了事务ID,会造成膨胀、年龄回收失败等,参考长事务的危害•可能会导致系统表膨胀•2PC的阻塞情况,pid显示的是0,十分具有迷惑性•基于流复制的备库,2PC事务也会复制过去•临时表不支持预备事务
不过好在默认情况下,PostgreSQL并不开启两阶段提交,除非知道自己在做什么,否则不建议使用预备事务。