ITPub博客

首页 > 数据库 > 数据库开发技术 > 一则有趣的XA事务

一则有趣的XA事务

原创 数据库开发技术 作者:u_a9af4a561c3b 时间:2022-03-10 14:55:03 0 删除 编辑

前言

今天有位朋友在群里提到了一个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","\\
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)

最后一行的\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并不开启两阶段提交,除非知道自己在做什么,否则不建议使用预备事务。


原文作者:PostgreSQL学徒;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2020-07-14

  • 博文量
    306
  • 访问量
    139324