zl程序教程

您现在的位置是:首页 >  数据库

当前栏目

Postgresql源码(73)两阶段事务PrepareTransaction事务如何与会话解绑(上)

postgresql源码事务 如何 阶段 73 与会
2023-06-13 09:11:01 时间

相关 《Postgresql源码(69)常规锁简单分析》 《Postgresql源码(73)两阶段事务PrepareTransaction事务如何与会话解绑(上)》 《Postgresql源码(74)两阶段事务PrepareTransaction事务如何与会话解绑(下)》

总结速查:

  • PrepareTransaction类似于事务提交过程,因为事务提交也会将事务状态与会话解绑、做清理工作。
  • 不同的是PrepareTransaction后面还要恢复信息以便二次提交,所以PrepareTransaction会保存提交所需的信息,并且将与会话关联的锁解绑,最后清理事务相关资源,达到事务与会话解绑的效果。
  • 注意虽然解绑了,锁还在,只是锁与任何会话都没关系了。

1 背景

两阶段事务提供的核心能力:一阶段提交的事务保证在二阶段提交时,可以正常提交。即使一阶段提交后,数据库宕机重启,都不会影响二阶段提交。这是普通事务不具备的能力,也是分布式事务全局提交依赖的功能。

两阶段语法例子:

create table tbl01(i int primary key);
insert into tbl01 values (1),(2),(3);

begin;
update tbl01 set i = 4 where i = 3;
prepare transaction 'x1';

select locktype,database,relation,transactionid,virtualtransaction,pid,mode,granted,fastpath,waitstart from pg_locks where pid is null;
   locktype    | database | relation | transactionid | virtualtransaction | pid |       mode       | granted | fastpath | waitstart 
---------------+----------+----------+---------------+--------------------+-----+------------------+---------+----------+-----------
 transactionid |          |          |       4003120 | 4/7843             |     | ExclusiveLock    | t       | f        | 
 relation      |    19525 |    19598 |               | 4/7843             |     | RowExclusiveLock | t       | f        | 
 relation      |    19525 |    19601 |               | 4/7843             |     | RowExclusiveLock | t       | f        | 

commit prepared 'x1';

select locktype,database,relation,transactionid,virtualtransaction,pid,mode,granted,fastpath,waitstart from pg_locks where pid is null;
(0 rows)
  • 执行完PREPARE后,事务会和当前会话“解绑”,当前会话结束事务状态,可以再起其他事务。
  • 事务信息会持久化到磁盘上,如果服务器发生宕机,在启动后,也可以正常提交两阶段事务。
  • 注意:锁还在(两把常规锁分别加在表和索引上,一把事务ID锁)。在上面锁表中看到PID is null的就是两阶段留下的锁。
  • 注意:锁虽然还在,但是已经和会话无关联关系,变成了”野锁“。

2 prepare transaction

prepare transaction执行完成后,预期内要完成的事情:

  1. 恢复事务块状态到default初始模式。
  2. 保存所有使用过的、事务提交时需要的资源。
  3. 将保存的资源持久化:写两阶段文件pg_twophase/xxx 和wal日志 pg_wal/xxx

prepare transaction命令和其他事务控制语句类似:在DDL执行中调整状态,在最后finish_xact_command->CommitTransactionCommand时调用功能函数干活:PrepareTransaction

2.1 数据结构

  • 整体结构:TwoPhaseStateData头 + max_prepared_xacts个指针 + max_prepared_xacts个两阶段状态结构
  • 头部freeGXacts连接所有空闲GlobalTransactionData
  • 头部prepXacts柔性数组便于找到GlobalTransactionData指针
  • 每开启一个两阶段事务就占用一个GlobalTransactionData指针、一个GlobalTransactionData结构

2.2 PrepareTransaction分段分析

static void
PrepareTransaction(void)
{
	...

事务提交准备:

	CallXactCallbacks(XACT_EVENT_PRE_PREPARE);
	AfterTriggerEndXact(true);
	PreCommit_on_commit_actions();
	smgrDoPendingSyncs(true, false);
	AtEOXact_LargeObject(true);
	PreCommit_CheckForSerializationFailure();

停止相应中断

	HOLD_INTERRUPTS();

事务状态转换,记录时间点

	s->state = TRANS_PREPARE;
	prepared_at = GetCurrentTimestamp();

构造GlobalTransactionData:MarkAsPreparing

  1. 加锁:TwoPhaseStateLock
  2. freelist中取一个Data:gxact = TwoPhaseState->prepXacts[i];
  3. 构造Data、补全MYPROC、MyLockedGxact指向当前Data:MarkAsPreparingGuts
  4. 现在肯定还没落盘:gxact->ondisk = false;
  5. 放锁:TwoPhaseStateLock
	gxact = MarkAsPreparing(xid, prepareGID, prepared_at,
							GetUserId(), MyDatabaseId);
	prepareGID = NULL;

收集需要保存的信息:

  • StartPrepare
  • AtPrepare_Notify
  • AtPrepare_Locks:收集锁信息
  • AtPrepare_PredicateLocks
  • AtPrepare_PgStat
  • AtPrepare_MultiXact
  • AtPrepare_RelationMap
	StartPrepare(gxact);

	AtPrepare_Notify();
	AtPrepare_Locks();
	AtPrepare_PredicateLocks();
	AtPrepare_PgStat();
	AtPrepare_MultiXact();
	AtPrepare_RelationMap();

上面所有收集到的信息写入XLOG。

	EndPrepare(gxact);

注意PostPrepare_Locks这一步非常重要:一阶段提交后,事务锁状态信息和会话也会解耦,就是这个函数做的。

PostPrepare_Locks将锁信息和PGPROC proc关联,清理LOCALLOCK锁表上的关联关系。

效果就是一阶段提交后,锁和会话的关联关系没了,锁变成了”野锁“。

	PostPrepare_Locks(xid);

开始事务结束的标准清理操作:

	ProcArrayClearTransaction(MyProc);
	CallXactCallbacks(XACT_EVENT_PREPARE);
	ResourceOwnerRelease(TopTransactionResourceOwner,
						 RESOURCE_RELEASE_BEFORE_LOCKS,
						 true, true);
	AtEOXact_Buffers(true);
	AtEOXact_RelationCache(true);
	PostPrepare_PgStat();
	PostPrepare_Inval();
	PostPrepare_smgr();
	PostPrepare_MultiXact(xid);
	PostPrepare_PredicateLocks(xid);
	ResourceOwnerRelease(TopTransactionResourceOwner,
						 RESOURCE_RELEASE_LOCKS,
						 true, true);
	ResourceOwnerRelease(TopTransactionResourceOwner,
						 RESOURCE_RELEASE_AFTER_LOCKS,
						 true, true);
	PostPrepare_Twophase();
	AtEOXact_GUC(true, 1);
	AtEOXact_SPI(true);
	AtEOXact_Enum();
	AtEOXact_on_commit_actions(true);
	AtEOXact_Namespace(true, false);
	AtEOXact_SMgr();
	AtEOXact_Files(true);
	AtEOXact_ComboCid();
	AtEOXact_HashTables(true);
	/* don't call AtEOXact_PgStat here; we fixed pgstat state above */
	AtEOXact_Snapshot(true, true);
	pgstat_report_xact_timestamp(0);
	CurrentResourceOwner = NULL;
	ResourceOwnerDelete(TopTransactionResourceOwner);
	s->curTransactionOwner = NULL;
	CurTransactionResourceOwner = NULL;
	TopTransactionResourceOwner = NULL;

	AtCommit_Memory();

	s->fullTransactionId = InvalidFullTransactionId;
	s->subTransactionId = InvalidSubTransactionId;
	s->nestingLevel = 0;
	s->gucNestLevel = 0;
	s->childXids = NULL;
	s->nChildXids = 0;
	s->maxChildXids = 0;

	XactTopFullTransactionId = InvalidFullTransactionId;
	nParallelCurrentXids = 0;

事务状态切回原始状态,会话与事务完成解绑

	s->state = TRANS_DEFAULT;

	RESUME_INTERRUPTS();
}