概述
PostgreSQL是当今最广泛应用的数据库系统(DBMS)之一。除了由于其具有优秀的性能、良好的兼容性之外,其完全开源的特性、完整的事务能力也是其中重要的原因。PostgreSQL支持完整的ACID特性,支持RC/RR/SSI等隔离级别。
PostgreSQL除了基础的事务能力之外,还提供了子事务的能力,也就是保存点(SAVEPOINT)的功能。保存点功能能够支持在发生错误时自动回滚到上一保存点,而无需回滚整个事务;或者在任意时刻回滚到某一特定保存点的状态,而放弃事务中的部分修改。
由于PostgreSQL保存点/子事务的强大能力,其被广泛应用与PLSQL/UDF/Function、Webservice(django),数据同步服务保证exactly-once等诸多场景。
本文主要面向数据库内核开发人员、高级DBA群体、Web开发工程师群体,会首先介绍下保存点的基础用法;然后结合源码介绍PostgreSQL中子事务的基础实现,并着重介绍其可见性、事务日志相关的实现;并且讨论Postgresql 子事务使用过程中可能会出现的性能问题,还会介绍Greenplum中的两阶段事务与子事务的结合,之后会简要介绍MySQL中的子事务做简单对比,最后会对本文内容做个总结并介绍下PostgreSQL的未来方向。
子事务怎么用
PostgreSQL在基础的事务能力之外,提供了子事务的能力。具体来说,PG支持创建子事务(SAVEPOINT)、回滚子事务(ROLLBACK TO SAVEPOINT)、释放子事务(RELEASE SAVEPOINT)等操作。PostgreSQL能够提供在发生错误时自动回滚到上一保存点,而无需回滚整个事务;或者在任意时刻回滚到某一特定保存点的状态,而放弃事务中的部分修改的能力。
SAVEPOINT语法可以定义一个保存点;
ROLLBACK TO SAVEPOINT语法支持回滚到指定保存点;
RELEASE SAVEPOINT语法支持释放之前定义的保存点,但是保存点被释放,并不会导致中间做的修改失效。
详见例子:
BEGIN;
INSERT INTO table1 VALUES (1);
SAVEPOINT my_savepoint;
INSERT INTO table1 VALUES (2);
ROLLBACK TO SAVEPOINT my_savepoint;
INSERT INTO table1 VALUES (3);
COMMIT;
上面的事务将插入值 1 和 3,但不会插入 2。
要建立并且稍后销毁一个保存点:
BEGIN;
INSERT INTO table1 VALUES (3);
SAVEPOINT my_savepoint;
INSERT INTO table1 VALUES (4);
RELEASE SAVEPOINT my_savepoint;
COMMIT;
上面的事务将插入 3 和 4。
值得注意的是子事务与主事务在COMMIT和ABORT时与主事务的区别和联系。
0)子事务一定需要在父事务中定义,无法不定义父事务而单独定义某个子事务;
1)无法单独COMMIT某个子事务/保存点,父事务提交时会自动提交其中定义的所有子事务;
2)在没有子事务的场景下,内部错误或者外部ROLLBACK命令都一定必须ABORT整个事务;
3)接受外部ROLLBACK指令而产生的ROLLBACK,会ABORT当前父事务和其中定义的所有的子事务;
4)内部错误产生的ROLLBACK会ROLLBACK当前子事务,而不影响之前定义的子事务的状态。
CREATE TABLE table1(a int);
postgres=# BEGIN;
BEGIN
postgres=# INSERT INTO table1 VALUES (1);
INSERT 0 1
postgres=# INSERT INTO table1 VALUES (1, 2);
ERROR: INSERT has more expressions than target columns
LINE 1: INSERT INTO table1 VALUES (1, 2);
^
postgres=# COMMIT;
ROLLBACK
上面的COMMIT将自动执行ROLLBACK,任何值都不会被插入;事务一定出错,必须ABORT整个事务。
BEGIN;
INSERT INTO table1 VALUES (1);
SAVEPOINT my_savepoint;
INSERT INTO table1 VALUES (2);
ROLLBACK;
上面的事务将不会插入任何值;
BEGIN;
INSERT INTO table1 VALUES (1);
SAVEPOINT my_savepoint;
INSERT INTO table1 VALUES (1,2);
ROLLBACK TO SAVEPOINT my_savepoint;
COMMIT;
上面的事务会成功插入1;
再次总结,PG通过子事务提供:
0)发生错误时自动回滚到上一保存点,而无需回滚整个事务;
1)或者在任意时刻回滚到某一特定保存点的状态,而放弃事务中的部分修改;
的能力。
子事务实现——基础实现
在前面的章节里面,我们介绍了PG中子事务/保存点的使用方法,实际上PG生态的大多数数据库,比如Greenplum,GaussDB对于子事务的使用都是一致的。这一章节我们主要结合源码(PG9.4)介绍PG关于子事务的实现原理。
本章节主要面向数据库内核开发者和高级DBA,推荐阅读本章节之前,需要对PG的基于状态机的事务模型有一些基本了解;这一方面的资料推荐阅读笔者的前一篇关于PG事务与分布式事务的文章。
总体来说,PG的子事务是栈模式,每次新创建子事务就压栈进去。而如果当前事务中如果有多个子事务,则前一个子事务是它后面一个子事务的父事务(parent);通过不断地压栈和出栈,修改事务块状态实现子事务的定义、回滚、提交。后续章节会结合子事务的基本操作来讲述PG的子事务基础实现。
DEFINE SAVEPOINT
定义保存点是子事务的基本使用方法:
SAVEPOINT savepoint_name;
执行SAVEPOINT的时候会调用DefineSavepoint()函数,主要是调用PushTransaction()进行如下操作:
1、检查当前事务的子事务数目(currentSavepointTotal)是否超过总数限制,如果超限则打出WARN;
2、新建一个TransactionState,关联当前子事务;
3、为当前子事务分配SubTransactionId,如果超过了2^32-1个子事务,则打出ERROR;
4、将当前子事务的TransactionState的parent指针指向当前事务的父事务CurrentTransactionState,填入subTransactionId和nestingLevel;
5、将当前的全局事务CurrentTransactionState置为新分配的子事务;
在这个Command对应的CommitTransactionCommand中,还会对新建的子事务调用StartSubTransaction,进一步完成一些事务的初始化。
而PG中子事务的TransactionId的分配是lazy模式,在实际需要事务ID(比如具体的DML)的时候才会分配。
其实PG的子事务是栈模式,每次新创建子事务就压栈进去。而如果当前事务中如果有多个子事务,则前一个子事务是它后面一个子事务的父事务(parent);
ROLLBACK TO SAVEPOINT
这个命令将当前事务ROLLBACK到一个指定的SAVEPOINT点,后台具体实现为:
1、从当前事务CurrentTransactionState开始,逐层向上遍历,寻找要rollback的savepoint对应的事务;
2、修改最新事务到被rollback到的指定事务之间的事务的状态为TBLOCK_SUBABORT_PENDING 或TBLOCK_SUBABORT_END;
3、将找到的事务块的状态置为TBLOCK_SUBRESTART;
通过将当前savepoint与指定savepoint之间的子事务的状态置为abort来实现rollback to savepoint;
在这个Command对应的CommitTransactionCommand中,还会对刚才状态被标记为ABORT状态的子事务逐层进行AbortSubTransaction和CleanupSubTransaction:
void
CommitTransactionCommand(void)
{
TransactionState s = CurrentTransactionState;
···
switch (s->blockState)
{
···
/*
* The current already-failed subtransaction is ending due to a
* ROLLBACK or ROLLBACK TO command, so pop it and recursively
* examine the parent (which could be in any of several states).
*/
case TBLOCK_SUBABORT_END:
CleanupSubTransaction();
CommitTransactionCommand();
break;
/*
* As above, but it's not dead yet, so abort first.
*/
case TBLOCK_SUBABORT_PENDING:
AbortSubTransaction();
CleanupSubTransaction();
CommitTransactionCommand();
break;
···
case TBLOCK_SUBRESTART:
{
char *name;
int savepointLevel;
/* save name and keep Cleanup from freeing it */
name = s->name;
s->name = NULL;
savepointLevel = s->savepointLevel;
AbortSubTransaction();
CleanupSubTransaction();
if (Gp_role == GP_ROLE_DISPATCH)
{
DispatchRollbackToSavepoint(name);
}
DefineSavepoint(name);
s = CurrentTransactionState; /* changed by push */
if (name)
{
pfree(name);
}
s->savepointLevel = savepointLevel;
/* This is the same as TBLOCK_SUBBEGIN case */
AssertState(s->blockState == TBLOCK_SUBBEGIN);
StartSubTransaction();
s->blockState = TBLOCK_SUBINPROGRESS;
}
break;
/*
* Same as above, but the subtransaction had already failed, so we
* don't need AbortSubTransaction.
*/
case TBLOCK_SUBABORT_RESTART:
{
char *name;
int savepointLevel;
/* save name and keep Cleanup from freeing it */
name = s->name;
s->name = NULL;
savepointLevel = s->savepointLevel;
CleanupSubTransaction();
if (Gp_role == GP_ROLE_DISPATCH)
{
DispatchRollbackToSavepoint(name);
}
DefineSavepoint(name);
s = CurrentTransactionState; /* changed by push */
s->name = name;
s->savepointLevel = savepointLevel;
/* This is the same as TBLOCK_SUBBEGIN case */
AssertState(s->blockState == TBLOCK_SUBBEGIN);
StartSubTransaction();
s->blockState = TBLOCK_SUBINPROGRESS;
}
break;
}
}
}
RELEASE SAVEPOINT
RELEASE SAVEPOINT销毁在当前事务 中之前定义的一个保存点。
销毁一个保存点会使得它不能再作为一个回滚点,但是它没有其他用户可见的行为。它不会撤销在该保存点被建立之后执行的命令的效果
RELEASE [SAVEPOINT] savepoint_name:
BEGIN;
INSERT INTO table1 VALUES (3);
SAVEPOINT my_savepoint;
INSERT INTO table1 VALUES (4);
RELEASE SAVEPOINT my_savepoint;
COMMIT;
后台具体实现为:
1、检测当前事务状态一定处于:TBLOCK_SUBINPROGRESS
2、将指定子事务的状态修改为TBLOCK_SUBRELEASE;
COMMIT TRANSACTION
PG子事务的逻辑中无法单独commit某个SAVEPOINT,提交事务时会自动COMMIT全部正常状态的SAVEPOINT;
子事务相关提交流程:
当存在子事务时,会从当前子事务开始逐层提交,一直提交到外层父事务:
1、修改当前子事务状态为TRANS_COMMIT;
2、CommandCounterIncrement();
3、关闭大对象,drop portal,释放resoureowner等等,做实际的事务提交;
4、修改子事务状态为TRANS_DEFAULT;
5、调用AtSubCommit_childXids将正常完成提交的子事务TransactionId计入parent的childXids结构,供主事务提交时
6、PopTransaction(),继续去释放上层子事务;
7、当没有上层事务,已经到达最外层的话,commit transaction; 实际的文件删除会在父事务提交时操作;
主事务提交流程:
在外层父事务的提交过程中,会调用CommitTransaction提交整个事务,里面会调用RecordTransactionCommit,在clog中写入父事务TransactionId和所有子事务TransactionId,并修改子事务和父事务的状态为committed。
ABORT TRANSACTION
1、如果当前由于内部错误导致subtransaction自动rollback,仅abort当前subtransaction到上一层事务,执行AbortCurrentTransaction。
case TBLOCK_SUBINPROGRESS: AbortSubTransaction(); s->blockState = TBLOCK_SUBABORT; break; /* * If we failed while trying to create a subtransaction, clean up * the broken subtransaction and abort the parent. The same * applies if we get a failure while ending a subtransaction. */ case TBLOCK_SUBBEGIN: case TBLOCK_SUBRELEASE: case TBLOCK_SUBCOMMIT: case TBLOCK_SUBABORT_PENDING: case TBLOCK_SUBRESTART: AbortSubTransaction(); CleanupSubTransaction(); AbortCurrentTransaction(); break;
2、如果用户执行rollback命令导致rollback,执行UserAbortTransactionBlock,
当前大事务块直接rollback,会直接rollback到最外部,abort全部事务(包括子事务和父事务)。
子事务实现——可见性
PG支持完整的事务,支持ACID特性,支持RC/RR/SSI等隔离级别,PG基于MVCC(multi-version-concurrency-control)机制进行数据可见性的判断。
我们在RC的隔离级别下,举个相对简单的例子来简化地说明这个判断的原理:
postgres=# create table test2(a int);
CREATE TABLE
postgres=# insert into test2 values(1),(2),(3);
INSERT 0 3
postgres=# select xmin,xmax,* from test2;
xmin | xmax | a
--------+------+---
309534 | 0 | 1
309534 | 0 | 2
309534 | 0 | 3
对于每条数据,会在header中存储创建数据的事务ID-xmin,以及删除/更新数据的事务ID-xmax。在某个进程读取这些数据时,会通过读取共享内存,获取一个包含当前活跃事务列表的快照。通过快照中的活跃事务列表,可以知道xid小于某个id的事务都已经提交了,xid大于某个id的事务都没有提交。
对于某条扫描到的数据,如果发现看到的xmin相对于当前进程的快照还没有提交,就认为这个事务不可见;如果发现看到xmax相对于当前进程的快照已经提交了,就认为这个事务不可见;如果发现看到的xmin对于当前事务的快照已经提交了,但是xmax对于当前的快照还没有提交,就认为这个事务不可见。
当然了,以上是相当简化的情况,PG相对于这个过程做了很多优化,比如说会在header里面加一些直观的标志来对其可见性做个简单的标记,比如创建这个tuple的事务已经abort了,就对于所有事务不可见了,这些标记存储在infomask字段,以这些方法来加速可见性判断。
在我们加入子事务之后,对于可见性的判断有哪些不同呢:
create table test1(a int);
begin;
insert into test1 select 1;
insert into test1 select 2;
savepoint s2;
savepoint s3;
insert into test1 select 3;
commit;
select ctid, xmin, xmax, a from test1;
ctid | xmin | xmax | a
-------+------+------+---
(0,1) | 497 | 0 | 1
(0,2) | 497 | 0 | 2
(0,3) | 499 | 0 | 3
(3 rows)
对于元组的存储,xmin,xmax存储的是子事务xid,而不是所在父事务xid;
对于快照的获取,快照中会存储父事务的活跃事务列表,也会存储下所有当前活跃的子事务列表,来加速可见性判断。当然了,共享内存中能够存储的子事务个数是有限的,如果超过了预设的一个大小,就会在内存中标记为一个suboverflow,以后在可见性判断与活跃事务列表进行比较的时候,可能会检索磁盘。
对于可见性的判断过程:如果产生和更新(更新xmin,xmax)的进程,与当前获取快照进行可见性判断的进程不是同一个进程,这个问题和普通事务的活跃事务列表那种通过比较xmin与快照大小前后来判断并没有什么区别。为什么呢?因为父事务与各个子事务的xid大小是严格有序的。
而如果产生和更新(更新xmin,xmax)的进程,与当前获取快照进行可见性判断的进程是同一个进程,判断的方式会略有不同,PG会检测内存中的子事务栈,虽然这些子事务可能都并不提交,但是一些处于正常INPROGRESS状态的前序子事务产生的数据,还是可见的。
这其中可能会调用TransactionIdIsCurrentTransactionId,来判断tuple header的xmax是否是当前进程中,其中会二分的检查子事务栈来判断指定xid是否是当前事务中的子事务xid。TransactionIdIsInProgress函数会对指定xid检索子事务日志,找到对于指定子事务最上层的父事务了,并检索共享内存中的进程状态量,来判断当前事务或者子事务对应的最外层事务是否处于INPROGRESS(运行中)状态,如果xmax处于运行中,这个tuple对于当前进程很有可能就处于可见状态。以上两个函数是加入子事务之后,涉及子事务逻辑改造标记多的两个函数。PG在上述函数中引入子事务逻辑以完成上述判断过程。
以上是对子事务可见性判断一个很简化的阐述,尝试用比较简化的模型阐述包含子事务的判断逻辑,对更多细节感兴趣可以详细查看tqual.c中的HeapTupleSatisfiesMVCC函数。
子事务实现——事务日志
日志是实现PG事务的保证,PG中包括事务日志(PG_CLOG/PG_XACT),事务提交日志(PG_XLOG/PG_WAL),以及审计日志(PG_LOG)等。PG_CLOG里面记录了顶层事务状态,事务id及其状态,提交或是ABORT。
同时,为了记录子事务的状态,以支持包含子事务的可见性判断,以及故障恢复等场景,PG同时记录pg_subtrans子事务日志,记录子事务的状态,具体来说,子事务日志记录每个子事务的父事务ID。
值得注意的是,PG并不直接读写磁盘,而是维护一组缓冲池,通过LRU算法来实现页面置换与映射,这种算法叫做slru(simple least recently used algorithm)。
PG的子事务日志以8KB为一个page,一个segment(文件段)包含32个page。单个子事务对应的记录需要32bit来存储,所以一个page最多能够包含2^11条记录,一个文件段(最多256KB)最多包含2^11*32=2^16条子事务记录。
其结构非常简单,对于每个子事务,能够通过其事务号计算其所处page和偏移,在偏移处直接读写32bit即可获取其父事务id。
#define TransactionIdToPage(xid) ((xid) / (uint32) SUBTRANS_XACTS_PER_PAGE)
#define TransactionIdToEntry(xid) ((xid) % (uint32) SUBTRANS_XACTS_PER_PAGE)
子事务日志的写流程。子事务日志的写一般都是在记录子事务的父事务ID时(SubTransSetParent)进行调用,其基本流程如下:
0)申请共享缓冲区的锁;
1)调用SimpleLruReadPge读取相关Page,将其加载到共享缓冲区;
2)在共享缓冲区中修改相关Page内容;
3)将修改过的共享缓冲区页面标记为脏;
4)释放共享缓冲区的锁。
子事务日志的读流程。子事务日志的写一般都是在记录子事务的父事务ID时(SubTransGetParent)进行调用,其基本流程如下:
0)申请共享缓冲区的锁;
1)调用SimpleLruReadPge读取相关Page,将其加载到共享缓冲区;
2)通过想要读取的子事务id,计算出其位于的page中的位置;
3)释放共享缓冲区的锁。
子事务注意事项与性能问题
使用子事务可能会带来一些问题,了解以下注意事项可以帮助你更加高效的使用子事务:
1)子事务带来的xid膨胀问题:
我们知道,PG运行过程中能够分配的活跃事务id(xid)数量是有限的,如果分配的活跃事务id数量超过了限制,需要执行vacuum freeze来回收一部分xid使其标记不可见,否则数据库系统将会处于不可用状态,需要停机重启处理。
对于普通事务,不考虑xid的懒分配等因素,一个事务一般会分配一个xid;而对于子事务,我们刚才有介绍过,通过栈式模式进行分配,每个子事务都会分配一个xid,这就大大加剧了事务id消耗的速度,缩短了需要回收事务id的时间间隔。
create table test1(a int);
begin;
insert into test1 select 1;
insert into test1 select 2;
savepoint s2;
savepoint s3;
insert into test1 select 3;
commit;
select ctid, xmin, xmax, a from test1;
ctid | xmin | xmax | a
-------+------+------+---
(0,1) | 497 | 0 | 1
(0,2) | 497 | 0 | 2
(0,3) | 499 | 0 | 3
(3 rows)
在上面这个例子中,我们看到一个事务中,由于使用了子事务,由父事务的消耗1个事务id,变成了消耗了一共4个事务id,大大增大了子事务消耗的速度。
所以,在使用子事务的场景下,我们需要根据业务特点,平衡子事务数量与做vacuum freeze间隔,避免停机风险。
2)子事务缓存溢出(cache overflow)带来的性能问题
首先回顾一些背景知识,PG会将子事务信息写入pg_subtrans目录用于持久化以及故障恢复,而在可见性判断的时候,PG会结合当前活跃的子事务信息进行可见性判断。为了避免每次都去磁盘上去读取子事务,PG在共享内存中对子事务信息进行了缓存。
当然足够成熟的我们知道,命运对每一次馈赠的礼物都标好了价格。是缓存都会有大小限制,尤其是共享内存这种同步代价比较高的存储结构,对于子事务来说,缓存的子事务的大小限制是多少呢?
/*
* Each backend advertises up to PGPROC_MAX_CACHED_SUBXIDS TransactionIds
* for non-aborted subtransactions of its current top transaction. These
* have to be treated as running XIDs by other backends.
*
* We also keep track of whether the cache overflowed (ie, the transaction has
* generated at least one subtransaction that didn't fit in the cache).
* If none of the caches have overflowed, we can assume that an XID that's not
* listed anywhere in the PGPROC array is not a running transaction. Else we
* have to look at pg_subtrans.
*/
#define PGPROC_MAX_CACHED_SUBXIDS 64 /* XXX guessed-at value */
答案是对于每个PG进程(每个用户连接)默认限制最多缓存最多64个子事务限制,当超过这个限制之后,PG会在共享内存中的进程状态中标记suboverflow,PG进行可见性判断的时候会频繁的通过slru机制读取磁盘文件,以及进行锁争抢,从而产生性能的迅速下降。
我们通过这个简单的实验来验证子事务超过64个之后的性能下降,我们在下面的测试脚本中调整子事务个数来验证性能问题:
BEGIN;
PREPARE sel(integer) AS
SELECT count(*)
FROM contend
WHERE id BETWEEN $1 AND $1 + 100;
PREPARE upd(integer) AS
UPDATE contend SET val = val + 1
WHERE id IN ($1, $1 + 10, $1 + 20, $1 + 30);
SAVEPOINT a;
\set rnd random(1,990)
EXECUTE sel(10 * :rnd + 1 + 1);
EXECUTE upd(10 * :rnd + 1);
SAVEPOINT a;
\set rnd random(1,990)
EXECUTE sel(10 * :rnd + 1 + 1);
EXECUTE upd(10 * :rnd + 1);
......
SAVEPOINT a;
\set rnd random(1,990)
EXECUTE sel(10 * :rnd + 1 + 1);
EXECUTE upd(10 * :rnd + 1);
SAVEPOINT a;
\set rnd random(1,990)
EXECUTE sel(10 * :rnd + 1 + 1);
EXECUTE upd(10 * :rnd + 1);
DEALLOCATE ALL;
COMMIT;
pgbench -p 5432 -d postgres -f subtrans.sql -n -c 20 -T 60
对于保存点个数为40的性能perf:
+ 1.86% [.] tbm_iterate + 1.77% [.] hash_search_with_hash_value 1.75% [.] AllocSetAlloc + 1.36% [.] pg_qsort + 1.12% [.] base_yyparse + 1.10% [.] TransactionIdIsCurrentTransactionId + 0.96% [.] heap_hot_search_buffer + 0.96% [.] LWLockAttemptLock + 0.85% [.] HeapTupleSatisfiesVisibility + 0.82% [.] heap_page_prune + 0.81% [.] ExecInterpExpr + 0.80% [.] SearchCatCache1 + 0.79% [.] BitmapHeapNext + 0.64% [.] LWLockRelease + 0.62% [.] MemoryContextAllocZeroAligned + 0.55% [.] _bt_checkkeys 0.54% [.] hash_any + 0.52% [.] _bt_compare 0.51% [.] ExecScan
对于保存点个数为80的性能perf:
+ 10.59% [.] LWLockAttemptLock + 7.12% [.] LWLockRelease + 2.70% [.] LWLockAcquire + 2.40% [.] SimpleLruReadPage_ReadOnly + 1.30% [.] TransactionIdIsCurrentTransactionId + 1.26% [.] tbm_iterate + 1.22% [.] hash_search_with_hash_value + 1.08% [.] AllocSetAlloc + 0.77% [.] heap_hot_search_buffer + 0.72% [.] pg_qsort + 0.72% [.] base_yyparse + 0.66% [.] SubTransGetParent + 0.62% [.] HeapTupleSatisfiesVisibility + 0.54% [.] ExecInterpExpr + 0.51% [.] SearchCatCache1
我们可以清楚的看出,在保存点超过64之后,在锁争夺上会有较大的性能损耗。
总结来说,PG在磁盘记录子事务状态,为了加速可见性判断,在共享内存中为子事务状态信息做了缓存,默认缓存的最大子事务数是64,超过这个限制之后性能会迅速下降,为了取得良好的性能实践,推荐在PG单个事务中不使用超过64个子事务。
Greenplum中的子事务
Greenplum是一种广泛使用的,基于PG进行开发的MPP架构的分布式数据库。GP不仅高度兼容PG生态,还保持了PG包含支持完整事务的优点,本章节主要介绍一下GP对于子事务的相关使用和实现。
在使用上,GP子事务的用法与PG是完全一致的,包括定义(SAVEPOINT)、回滚(ROLLBACK TO)释放保存点(RELEASE SAVEPOINT)等。
在实现上,由于GP由单个Master和多个Segment节点组成,其在PG子事务的基础实现上进行了一些添加。具体来说,其具有以下特点:
0)Master和Segment各自独自开启自己的事务,各自基于自己本地事务状态进行本机上的可见性判断;
1)Master与Segment的事务状态必须完全一致,Master上的事务层次与各个层次的事务状态必须与Segment上完全一致。
2)Master需要将操作子事务的命令dispatch到各个segment,以定义子事务为例,会经历以下过程:Master本地事务上定义子事务(DefineSavepoint)->分发到各个segment上定义子事务(DIspatchDefineSavepoint)->segment执行CommitTransactionCommand更改事务状态->Master本地执行CommitTransactionCommand更改事务状态。
3)为了保证各个节点的一致性,不存在在部分节点COMMIT成功,部分节点COMMIT失败的情况,GP引入了两阶段提交来支持分布式事务。于是,GP的事务提交分为了prepare和commit两个阶段,对于子事务来说,其原始的commit过程,包括事务提交日志的写入和子事务嵌入事务的提交的过程,在prepare阶段完成。
MySQL中的子事务
MySQL中子事务/保存点的使用方式与PG是一致的,也是包括定义保存点(SAVEPOINT)、回滚保存点(ROLLBACK TO)、释放保存点(RELEASE SAVEPOINT)等。具体的实现方式与PG略有不同。
void Rpl_transaction_write_set_ctx::add_savepoint(char *name) {
DBUG_TRACE;
std::string identifier(name);
DBUG_EXECUTE_IF("transaction_write_set_savepoint_clear_on_commit_rollback", {
assert(savepoint.size() == 0);
assert(write_set.size() == 0);
assert(savepoint_list.size() == 0);
});
DBUG_EXECUTE_IF("transaction_write_set_savepoint_level",
assert(savepoint.size() == 0););
std::map<std::string, size_t>::iterator it;
/*
Savepoint with the same name, the old savepoint is deleted and a new one
is set
*/
if ((it = savepoint.find(name)) != savepoint.end()) savepoint.erase(it);
savepoint.insert(
std::pair<std::string, size_t>(identifier, write_set.size()));
DBUG_EXECUTE_IF(
"transaction_write_set_savepoint_add_savepoint",
assert(savepoint.find(identifier)->second == write_set.size()););
}
与PG中用栈式结构存储子事务不同,Mysql用map存储子事务,并且在rollback to、release等操作时对应修改map中的元素。
本文总结与PG中关于子事务的未来方向
针对PG子事务相关问题,PG社区和开发者们提出了一些方向,这里挑选一些影响比较大的供大家讨论:
- Andrey Borodin 提交了一些关于优化子事务日志slru cache size的patch: a set of patches that allow controlling the sizes of SLRU caches (including Subtrans SLRU) and make the SLRU mechanism more performant.
- 子事务监测工具的优化:Postgres observability tooling could be extended
- it would be really good to understand what transactions are using subtransactions and how large the nesting depth is,
- pageinspect module could be extended to allow checking the contents of the SLRU caches
- Pengcheng Liu 提交了一些patch以优化
PGPROC_MAX_CACHED_SUBXIDS
导致的subtransaction overflow等问题: has sent a new patch to the -hackers mailing list,
本文主要介绍PG子事务的各方面内容,首先介绍子事务的应用场景,进一步介绍PG中子事务的操作方式和使用特性,再进一步本文结合源码介绍PG中子事务的实现,特别介绍了子事务日志和可见性判断相关的实现,之后本文介绍子事务使用的一些注意事项和性能问题,在之后本文介绍了基于PG的MPP数据仓库GP的子事务实现,与另一种广泛应用的DBMS-MySQL中的子事务情况,最后本文介绍了社区和开发者们对于PG子事务特性后续发展的一些方向,并对本文进行了总结。本文面向PG子事务领域,会持续优化,持续追踪PG子事务的最新动态,对PG子事务感兴趣的朋友也欢迎指正错误、提供主题,共同关注PG子事务的发展。
参考文献:
How PostgreSQL Handles Sub Transaction Visibility In Streaming Replication - Highgo Software Inc.
PostgreSQL Subtransactions Considered Harmful | Database Lab · Instant clones of PostgreSQL databases · Postgres.ai
Subtransactions and performance degradation. Part 1: PGPROC_MAX_CACHED_SUBXIDS (#20) · Issues · Postgres.ai / PostgreSQL Consulting / tests and benchmarks · GitLab
Why we spent the last month eliminating PostgreSQL subtransactions|GitLab