概述
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、将找到的事务块的状态置为TBLOC