openGauss子事务管理分析(PLpgSQL中的异常子事务)

2023-10-13 10:25:26 浏览数 (1)

1 背景

PostgreSQL中的存储过程不支持使用savepoint、rollback to。原因是PG的存储过程中,异常处理使用子事务来实现的,也就是一旦发生异常,当前procedure的begin块中执行过的所有语句都会直接回滚:

代码语言:javascript复制
procedure
	begin
		insert into tbl values (1);
		x = 1 / 0;   -- 异常发生
		insert into tbl values (2);
	exception
		..
		..
	end;

当异常发生后,第二条insert没有执行到就跳转了,比较容易理解;但是第一条insert会被回滚,这种行为是PG特有的,和Oracle是有区别的,Oracle中异常发生只会跳转,不会回滚也不存在子事务。

2 PLpgSQL中实现检查点的困难

  1. 由于PG异常处理本身会启动子事务,就等于启动检查点了,那么如果在begin块中再执行savepoint,会把PG的异常检查点从 事务堆栈顶层 向下压一层, 那么如果异常没发生、或发生了,都需结束异常检查点,但异常检查点现在不在事务堆栈顶层,回滚或提交都会比较困难。
  2. 造成困难的原因主要是PG的大部分资源都会绑定事务ID,跟随事务释放,调整事务堆栈的位置需要一并调整资源与事务的绑定关系。

3 openGauss如何解决?

总结

  • 场景一:对于正常结束的block,如果执行过savepoint,则异常子事务在savepoint子事务下面一层,高斯的处理是不提交异常子事务,就放在事务堆栈中。
  • 场景二:对于正常结果的block,如果执行rollback to函数外层savepoint,则已经把异常子事务提交了,高斯的处理是不提交异常子事务,异常子事务已经被rollback to路过回滚了。
  • 场景三:对于正常结果的block,如果执行rollback to函数外层savepoint,且把SPI依赖的检查点也沿路回滚掉了,也没创建出来,会出现SPI挂到3号子事务上,但事务堆栈只有1、2号子事务的情况,没有子事务的调整机制。
  • openGauss中,通过SPI执行的事务操作,AtEOSubXact_SPI有跳过机制,也就是SPI调过来的情况AtEOSubXact_SPI什么都不释放。

还有一个最最重要的区别:openGassdb的子事务重新生成后,子事务ID不变,和nestinglevel基本是保持一致的。

为什么呢?因为在PopTransaction时,将父事务的值set到计数器上了:

代码语言:javascript复制
PushTransaction
	t_thrd.xact_cxt.currentSubTransactionId = s->parent->subTransactionId;

而PG中这个计数器是一直递增的,不能减小。

4 分析(可忽略)

问题一:正常执行结果如何跨过savepoint提交异常子事务。

CASE1

代码语言:javascript复制
CREATE or replace PROCEDURE p_outter()
as
  carry float;
begin
    INSERT INTO t_plpgsql_transaction_20221222_01 (a) VALUES (4);
    savepoint sp4;
    INSERT INTO t_plpgsql_transaction_20221222_01 (a) VALUES (5);
    rollback to sp4;
EXCEPTION WHEN others THEN
    RAISE NOTICE 'in exception %', sqlerrm;
end;
/


truncate t_plpgsql_transaction_20221222_01;
begin;
INSERT INTO t_plpgsql_transaction_20221222_01 (a) VALUES (3);
savepoint sp3;
call p_outter();
select * from t_plpgsql_transaction_20221222_01;
STEP1:创建sp4前事务状态
代码语言:javascript复制
subTransactionId = 3     name = ""      →   异常子事务(内层函数自动创建)
subTransactionId = 2     name = "sp3"
subTransactionId = 1     name = ""          基础事务

econtext两个挂在3、2各一个

代码语言:javascript复制
p *u_sess->plsql_cxt.simple_econtext_stack
$9 = {stack_econtext = 0x7fc1baefe560, xact_subxid = 3,	statckEntryId = 2, next = 0x7fc1bb00d840}
$10 = {stack_econtext = 0x7fc1baefe288,	xact_subxid = 2, statckEntryId = 1, next = 0x0}
STEP2:创建完sp4、rollback to sp4前事务状态
代码语言:javascript复制
subTransactionId = 4.    name = "sp4"
subTransactionId = 3     name = ""      →   异常子事务(内层函数自动创建)
subTransactionId = 2     name = "sp3"
subTransactionId = 1     name = ""          基础事务

econtext三个挂在4、3、2各一个。

代码语言:javascript复制
p *u_sess->plsql_cxt.simple_econtext_stack
$13 = {stack_econtext = 0x7fc1baefe7d0,	xact_subxid = 4, statckEntryId = 3, next = 0x7fc1bb00db08}
$14 = {stack_econtext = 0x7fc1baefe560,	xact_subxid = 3, statckEntryId = 2, next = 0x7fc1bb00d840}
$15 = {stack_econtext = 0x7fc1baefe288,	xact_subxid = 2, statckEntryId = 1, next = 0x0}
STEP3:rollback to sp4

回滚流程

代码语言:javascript复制
SPI_savepoint_rollback
	RollbackToSavepoint
	CommitTransactionCommand(STP_commit == true)
		case TBLOCK_SUBRESTART
			AbortSubTransaction(STP_commit)
				AtSubAbort_XXX
			CleanupSubTransaction(STP_commit)
				PopTransaction
			BeginInternalSubTransaction    // 重启子事务

rollback分两步:

  1. RollbackToSavepoint改事务状态
  2. CommitTransactionCommand完成回滚

回滚完成后事务状态

代码语言:javascript复制
subTransactionId = 4.    name = "sp4"
subTransactionId = 3     name = ""      →   异常子事务(内层函数自动创建)
subTransactionId = 2     name = "sp3"
subTransactionId = 1     name = ""          基础事务
STEP4: 正常执行结束,提交异常子事务,注意如执行过检查点,则异常子事务不做提交!
代码语言:javascript复制
exec_stmt_block
	exec_exception_begin(estate, &excptContext)
	PG_TRY
		exec_stmts
		exec_exception_end

exec_exception_end函数

代码语言:javascript复制
exec_exception_end
// 如果没检查点在block中
	if (context->curExceptionCounter == u_sess->SPI_cxt.portal_stp_exception_counter && GetCurrentTransactionName() == NULL)
		// 可以直接提交
		SPI_savepoint_release
		stp_cleanup_subxact_resource
	else
		// 不提交!

事务堆栈

代码语言:javascript复制
subTransactionId = 4.    name = "sp4"
subTransactionId = 3     name = ""      →   异常子事务(内层函数自动创建)
subTransactionId = 2     name = "sp3"
subTransactionId = 1     name = ""          基础事务

问题二:回滚到最外层的savepoint,把路过的异常子事务回滚了怎么办?

CASE1

代码语言:javascript复制
CREATE or replace PROCEDURE p_outter()
as
  carry float;
begin
    INSERT INTO t_plpgsql_transaction_20221222_01 (a) VALUES (4);
    rollback to sp3;
EXCEPTION WHEN others THEN
    RAISE NOTICE 'in exception %', sqlerrm;
end;
/


truncate t_plpgsql_transaction_20221222_01;
begin;
INSERT INTO t_plpgsql_transaction_20221222_01 (a) VALUES (3);
savepoint sp3;
call p_outter();
select * from t_plpgsql_transaction_20221222_01;
STEP1:rollback to sp3

执行前

代码语言:javascript复制
subTransactionId = 3     name = ""      →   异常子事务(内层函数自动创建)
subTransactionId = 2     name = "sp3"
subTransactionId = 1     name = ""          基础事务

回滚完成

代码语言:javascript复制
subTransactionId = 2     name = "sp3"
subTransactionId = 1     name = ""          基础事务

exec_exception_end函数

代码语言:javascript复制
exec_exception_end
// 如果没检查点在block中
	if (context->curExceptionCounter == u_sess->SPI_cxt.portal_stp_exception_counter && GetCurrentTransactionName() == NULL)
		// 可以直接提交
		SPI_savepoint_release
		stp_cleanup_subxact_resource
	else
		// 不提交!

context->curExceptionCounter = 1 u_sess->SPI_cxt.portal_stp_exception_counter = 0 GetCurrentTransactionName() = “sp3”

走不提交分支!

curExceptionCounter含义:在启动异常子事务的时候记录一下portal_stp_exception_counter的值。 portal_stp_exception_counter含义:记录有几个异常子事务。

exec_exception_end函数的判断逻辑是,启用异常子事务的时候记录下有几个异常子事务,清理异常子事务的时候看下这个数量有没有变化,有变化说明被里面的操作减少了,不可能增加只能减少,所以exec_exception_end不需要清理了。

问题三:回滚到最外层的前一个savepoint,把SPI依赖的子事务回滚了,那SPI资源挂到哪个子事务上了?会调整吗?

CASE1

代码语言:javascript复制
CREATE or replace PROCEDURE p_outter()
as
  carry float;
begin
    INSERT INTO t_plpgsql_transaction_20221222_01 (a) VALUES (4);
    rollback to sp3;
EXCEPTION WHEN others THEN
    RAISE NOTICE 'in exception %', sqlerrm;
end;
/


truncate t_plpgsql_transaction_20221222_01;
begin;
INSERT INTO t_plpgsql_transaction_20221222_01 (a) VALUES (3);
savepoint sp3;
INSERT INTO t_plpgsql_transaction_20221222_01 (a) VALUES (4);
savepoint sp4;
call p_outter();
select * from t_plpgsql_transaction_20221222_01;

结论:不会调整,会出现connectSubid=3

代码语言:javascript复制
*u_sess->SPI_cxt._current = {processed = 1, connectSubid = 3 }

但事务堆栈只有两层的情况

代码语言:javascript复制
subTransactionId  =  2
subTransactionId  =  1

SPI变成了野孩子。

5 复杂场景(有问题,不在测试)

代码语言:javascript复制
drop table if exists t_plpgsql_transaction_20221222_01;
create table t_plpgsql_transaction_20221222_01(a int);


CREATE or replace PROCEDURE p_outter()
as
  carry float;
begin
    INSERT INTO t_plpgsql_transaction_20221222_01 (a) VALUES (4);
    savepoint sp4;

    call p_inner();
EXCEPTION WHEN others THEN
    RAISE NOTICE 'in exception';
end;
/

CREATE or replace PROCEDURE p_inner()
as
  carry float;
begin
    INSERT INTO t_plpgsql_transaction_20221222_01 (a) VALUES (50);
    savepoint sp5;
    INSERT INTO t_plpgsql_transaction_20221222_01 (a) VALUES (60);
    carry = 1 / 0;
EXCEPTION WHEN others THEN
    RAISE NOTICE 'in exception exception';
end;
/


truncate t_plpgsql_transaction_20221222_01;
begin;
INSERT INTO t_plpgsql_transaction_20221222_01 (a) VALUES (3);
savepoint sp3;
call p_outter();
select * from t_plpgsql_transaction_20221222_01;
rollback to sp4;
select * from t_plpgsql_transaction_20221222_01;
commit;

下面调试call p_outter_base()异常子事务回滚流程:

3.1 断点分析:内层函数刚进入PG_TRY

代码语言:javascript复制
exec_stmt_block
	exec_exception_begin(estate, &excptContext)		// 起一层子事务
	PG_TRY
		// 这里的事务状态??

事务堆栈:

代码语言:javascript复制
subTransactionId = 6   name = "sp5"
subTransactionId = 5   name = ""      →   异常子事务(内层函数自动创建)
subTransactionId = 4   name = "sp4"
subTransactionId = 3   name = ""      →   异常子事务(外层函数自动创建)
subTransactionId = 2   name = "sp3"
subTransactionId = 1   name = ""          基础事务

econtext堆栈

代码语言:javascript复制
(gdb) p *u_sess->plsql_cxt.simple_econtext_stack
$17 = {stack_econtext = 0x7f17085dccb0, xact_subxid = 5, statckEntryId = 5, next = 0x7f17056e41f0}
$18 = {stack_econtext = 0x7f17085dca40, xact_subxid = 4, statckEntryId = 4, next = 0x7f17085e3b70}
$19 = {stack_econtext = 0x7f17085dc7d0, xact_subxid = 4, statckEntryId = 3, next = 0x7f17085e3b08}
$20 = {stack_econtext = 0x7f17085dc560, xact_subxid = 3, statckEntryId = 2, next = 0x7f17085e3840}
$21 = {stack_econtext = 0x7f17085dc288, xact_subxid = 2, statckEntryId = 1, next = 0x0}

SPI堆栈

代码语言:javascript复制
p u_sess->SPI_cxt._stack[0]
$25 = {processed = 0, connectSubid = 2}
$26 = {processed = 0, connectSubid = 4}

资源归属关系:s=savepoint subtransaction、e=exception subtransaction

代码语言:javascript复制
subTransaction          1o          2s          3e          4s          5e          6s
econtext                                                                5
econtext                                                    4
econtext                                                    4
econtext                                        3
econtext                            2
SPI_cxt                                                     4
SPI_cxt                             2

3.2 断点分析:内层函数发生异常开始回滚,进入exec_exception_cleanup:第一步XactCleanExceptionSubTransaction释放子事务资源

总结:把顶层的sp5的子事务和异常子事务的资源都释放了。

代码语言:javascript复制
exec_stmt_block
	exec_exception_begin(estate, &excptContext)		// 起一层子事务
	PG_TRY
		plpgsql_create_econtext
		exec_stmts
	PG_CATCH										// 出错,跳转到catch
		...
		exec_exception_cleanup						// 开始回滚
			XactCleanExceptionSubTransaction

进入exec_exception_cleanup时事务堆栈:

代码语言:javascript复制
subTransactionId = 6   name = "sp5"
subTransactionId = 5   name = ""      →   异常子事务(内层函数自动创建)
subTransactionId = 4   name = "sp4"
subTransactionId = 3   name = ""      →   异常子事务(外层函数自动创建)
subTransactionId = 2   name = "sp3"
subTransactionId = 1   name = ""          基础事务

进入XactCleanExceptionSubTransaction函数,函数入参为5。

注意这里只释放子事务资源,不调整事务堆栈!!

代码语言:javascript复制
XactCleanExceptionSubTransaction
	AbortSubTxnRuntimeContext			// 传入5 释放运行时资源(不关注)
	while (s->subTransactionId >= head && s->parent != NULL)
		// 注意第一次循环s是第5层:内层函数的异常子事务
		// 注意第二次循环s是第6层:sp5的子事务
	
		// 开始正常释放子事务资源
		// 注意这里只释放子事务资源,不调整事务堆栈!!
	
	// 释放完 资源状态??

释放完事务状态

代码语言:javascript复制
subTransactionId = 6   name = "sp5"
subTransactionId = 5   name = ""      →   异常子事务(内层函数自动创建)
subTransactionId = 4   name = "sp4"
subTransactionId = 3   name = ""      →   异常子事务(外层函数自动创建)
subTransactionId = 2   name = "sp3"
subTransactionId = 1   name = ""          基础事务

econtext堆栈

代码语言:javascript复制
(gdb) p *u_sess->plsql_cxt.simple_econtext_stack
// xact_subxid = 5 的被释放了,剩下四条
$18 = {stack_econtext = 0x7f17085dca40, xact_subxid = 4, statckEntryId = 4, next = 0x7f17085e3b70}
$19 = {stack_econtext = 0x7f17085dc7d0, xact_subxid = 4, statckEntryId = 3, next = 0x7f17085e3b08}
$20 = {stack_econtext = 0x7f17085dc560, xact_subxid = 3, statckEntryId = 2, next = 0x7f17085e3840}
$21 = {stack_econtext = 0x7f17085dc288, xact_subxid = 2, statckEntryId = 1, next = 0x0}

SPI堆栈

代码语言:javascript复制
p u_sess->SPI_cxt._stack[0]
$25 = {processed = 0, connectSubid = 2}
$26 = {processed = 0, connectSubid = 4}

资源归属关系:s=savepoint subtransaction、e=exception subtransaction

代码语言:javascript复制
subTransaction          1o          2s          3e          4s          5e          6s
econtext                                                    4
econtext                                                    4
econtext                                        3
econtext                            2
SPI_cxt                                                     4
SPI_cxt                             2

3.3 断点分析:进入exec_exception_cleanup:第二步主动回滚顶层用户检查点exec_savepoint_rollback

代码语言:javascript复制
exec_stmt_block
	exec_exception_begin(estate, &excptContext)		// 起一层子事务
	PG_TRY
		plpgsql_create_econtext
		exec_stmts
	PG_CATCH										// 出错,跳转到catch
		...
		exec_exception_cleanup						// 开始回滚
			XactCleanExceptionSubTransaction
			...
			exec_savepoint_rollback(estate, txnName "sp5")

这一步把sp5执行了rollbackto,然后又把sp5重建出来了,事务堆栈不变。

0 人点赞