[2020-12-08] PostgreSQL的事务及其实现机制

转载:https://zhuanlan.zhihu.com/p/147605189?d=1601190486925

事务是数据库的最基本概念,在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事务的使用方法和每一种事务的特性和其存储方式。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对于aarch64-centos7离线部署PostgreSQL 15,您可以按照以下步骤进行操作: 1. 下载PostgreSQL 15二进制文件: 您可以从PostgreSQL官方网站(https://www.postgresql.org/ftp/source/)下载适用于aarch64架构的二进制文件。 2. 安装依赖项: 在离线部署之前,您需要确保安装了所有必需的依赖项。运行以下命令安装这些依赖项: ``` sudo yum install -y readline-devel zlib-devel openssl-devel libxml2-devel libxslt-devel ``` 3. 创建系统用户和组: 为了安全起见,您可以为PostgreSQL创建一个专用的系统用户和组。运行以下命令创建用户和组: ``` sudo groupadd postgres sudo useradd -g postgres postgres ``` 4. 解压并安装二进制文件: 将下载的PostgreSQL 15二进制文件解压到适当的目录,并设置正确的权限。例如,您可以将其解压到`/opt/postgresql-15`目录: ``` sudo tar -xf postgresql-15.tar.gz -C /opt/ sudo chown -R postgres:postgres /opt/postgresql-15 ``` 5. 初始化数据库集群: 在进行任何其他配置之前,您需要初始化一个空的数据库集群。切换到`postgres`用户并运行以下命令: ``` sudo su - postgres /opt/postgresql-15/bin/initdb -D /opt/postgresql-15/data ``` 6. 配置数据库: 编辑`/opt/postgresql-15/data/postgresql.conf`文件来配置数据库的设置。根据您的需求,您可以设置参数如`listen_addresses`、`max_connections`等。 7. 启动数据库: 运行以下命令来启动PostgreSQL数据库: ``` /opt/postgresql-15/bin/pg_ctl -D /opt/postgresql-15/data -l logfile start ``` 8. 连接到数据库: 您可以使用psql命令行工具连接到已启动的PostgreSQL数据库: ``` /opt/postgresql-15/bin/psql -h localhost -U postgres ``` 这样,您就成功在aarch64-centos7上离线部署了PostgreSQL 15。请确保按照实际需求进行适当的配置和安全设置。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值