pg内核之事务管理器(一) 事务块

#pg内核

一 概述

事务管理系统

事务管理系统一般是指数据库中事务处理相关的几个模块的统称。事务管理系统主要包括三个大模块: 事务管理器、锁管理器和日志管理器。

在这里插入图片描述

ACID

  • 原子性(Atomic)
    事务必须是原子操作,要么全部成功,要么全部失败。
  • 一致性(Consistent)
    事务在完成时,所有的数据必须保持一致状态。
  • 隔离性(Isolation)
    由并发事务所作的修改必须与其他并发事务所作的修改隔离,互不影响。
  • 持久性(Duration)
    事务完成后,对数据库中数据的变更时永久的,不会被回滚。

二 事务管理器

事务块

pg是通过事务块实现数据库内部底层事务与终端命令的交互的。postmaster进程在接收到一个用户的连接请求后,会fork出一个子进程postgres来专门处理对应的用户请求。每一个postgres进程就可以看作是一个事务块,进程内自始至终只会有一个事务块。而显式的BEGIN、END等命令只是修改事务块的状态。
事务块与事务的区别:

  • 事务块:一个进程只有一个,SQL命令只会修改其状态。
  • 事务: 默认情况下,单个SQL就是一个事务,当然,也可以使用BEGIN,END显式的将多个SQL放到一个事务中。

事务块状态

在一个事务块中,SQL命令只会修改事务块的状态,其主要的状态定义在TBlockState
一个事务块默认的状态是TBLOCK_DEFAULT, 当事务开始时,会修改为不同的状态,当事务结束后会将事务块状态重置为TBLOCK_DEFAULT。

  • TBlockState
typedef enum TBlockState
{
	/* not-in-transaction-block states */
	TBLOCK_DEFAULT,				/* 空闲状态 */
	TBLOCK_STARTED,				/* 开始状态*/

	/* transaction block states */
	TBLOCK_BEGIN,				/* BENGIN状态,执行BEGIN命令会进入此状态 */
	TBLOCK_INPROGRESS,			/* 运行中 */
	TBLOCK_IMPLICIT_INPROGRESS, /* 隐式事务块运行中(已废弃) */
	TBLOCK_PARALLEL_INPROGRESS, /* 并行事务运行中 */
	TBLOCK_END,					/* 已经提交,执行COMMIT命令会进入此状态 */
	TBLOCK_ABORT,				/* 事务块终止,等待回滚 */
	TBLOCK_ABORT_END,			/* 事务块终止,执行ROLLBACK后进入此状态 */
	TBLOCK_ABORT_PENDING,		/* 事务块终止中,执行ROLLBACK后可能会进入此状态 */
	TBLOCK_PREPARE,				/* 两阶段提交状态,执行PREPARE进入此状态 */

	/* subtransaction states */
	TBLOCK_SUBBEGIN,			/* 子事务启动 */
	TBLOCK_SUBINPROGRESS,		/* 子进程运行中 */
	TBLOCK_SUBRELEASE,			/* 子进程释放 */
	TBLOCK_SUBCOMMIT,			/* 子进程提交 */
	TBLOCK_SUBABORT,			/* 子事务终止,等待回滚*/
	TBLOCK_SUBABORT_END,		/* 子事务终止,等待回滚*/
	TBLOCK_SUBABORT_PENDING,	/* 子事务终止,但是已经收到ROLLBACK*/
	TBLOCK_SUBRESTART,			/* 子事务提交,已经收到COMMIT*/
	TBLOCK_SUBABORT_RESTART		/* 子事务终止,已经收到ROLLBACK */
} TBlockState;
  • 状态图

在这里插入图片描述

  • 状态流转时序图
    在这里插入图片描述

隐式事务与隐式事务块

  • 隐式事务
    隐式事务是指在执行SQL语句时,如果没有显示的开启事务,则系统会为该语句自动开启一个事务。每条SQL语句都是一个事务。
  • 隐式事务块
    隐式事务块是指在执行多个SQL语句时,如果没有显式的开启事务,则这些语句会自动组成一个事务块,也就是说系统会将这些语句看做一个整体 ,在执行过程中如果某个语句执行失败,则整个事务块都会被回滚。旧版本可以通过SET IMPLICIT_TRANSACTION ON;语句显式开启,但是当前的版本已经没有隐式事务块的概念了,只不过事务块状态中仍保留而已。

事务块基本操作函数

事务块系统一般分为三层:上层、中层、底层
在数据库中,任何语句的执行都是通过事务块的入口函数进入并开始执行的,执行完毕后通过事务块的出口函数退出,出错时通过事务块的出错函数处理,事务块的状态时通过上层函数和中层函数共同控制的,而底层函数则是主要处理事务的函数并且控制事务的状态。

上层函数

就是可通过SQL命令控制事务块的状态的变更的函数,但是不处理具体的事务。如BEGIN、END、ROLLBACK、ABORT、PREPARE、SAVEPOINT、RELEASE等命令。主要包含以下函数:

BeginTransactionBlock
EndTransactionBlock
UserAbortTransactionBlock
DefineSavepoint
ReleaseSavepoint
RollbackToSavepoint
PrepareTransactionBlock

中层函数

任何一个SQL命令的执行,都会执行中层函数。中层函数只会修改事务块的状态,不进行实际操作,主要包含:

StartTransactionCommand
CommitTransactionCommand
AbortCurrentTransaction

底层函数

实际执行事务的函数

StartTransaction
CommitTransaction
AbortTransaction
CleanupTransaction
StartSubTransaction
CommitSubTransaction
AbortSubTransaction
CleanupSubTransaction
PushTransaction
PopTransaction

SQL示例

下面通过一个SQL,分析事务块各函数的调用情况

  • SQL语句
		BEGIN;
		SELECT * FROM foo;
		INSERT INTO foo VALUES (...);
		COMMIT;
  • 函数调用情况
    可以看到每个SQL语句都是包含在中层函数中的(StartTransactionCommand/CommitTransactionCommand)
    实际的SQL对应的执行时通过Process*函数调用相应的底层函数的

     /  StartTransactionCommand;
    /       StartTransaction;
1) <    ProcessUtility;                 << BEGIN
    \       BeginTransactionBlock;
     \  CommitTransactionCommand;

    /   StartTransactionCommand;
2) /    PortalRunSelect;                << SELECT ...
   \    CommitTransactionCommand;
    \       CommandCounterIncrement;

    /   StartTransactionCommand;
3) /    ProcessQuery;                   << INSERT ...
   \    CommitTransactionCommand;
    \       CommandCounterIncrement;

     /  StartTransactionCommand;
    /   ProcessUtility;                 << COMMIT
4) <        EndTransactionBlock;
    \   CommitTransactionCommand;
     \      CommitTransaction;

在这里插入图片描述

事务状态结构体

每个事务都会由一个事务状态结构体来表示,具体的结构如下

typedef struct TransactionStateData
{
	FullTransactionId fullTransactionId;	/* 事务ID 64位 */
	SubTransactionId subTransactionId;	/* 子事务ID */
	char	   *name;			/* 保存点名称 */
	int			savepointLevel; /* 保存点嵌套层次 */
	TransState	state;			/* 事务状态 */
	TBlockState blockState;		/* 事务块状态 */
	int			nestingLevel;	/* 事务嵌套深度 */
	int			gucNestLevel;	/* GUC嵌套深度 */
	MemoryContext curTransactionContext;	/* 当前事务的内存上下文 */
	ResourceOwner curTransactionOwner;	/* 当前事务的resourceowner信息 */
	TransactionId *childXids;	/* 子事务ID信息r */
	int			nChildXids;		/* 已经提交的子事务数量 */
	int			maxChildXids;	/* childXid分配的最大size */
	Oid			prevUser;		/* 先前的用户OID */
	int			prevSecContext; /* 先前的用户上下文设置标记 */
	bool		prevXactReadOnly;	/* 先前的只读事务标记 */
	bool		startedInRecovery;	/* 是否是恢复模式标记 */
	bool		didLogXid;		/* XID是否被包含在XLOG日志中*/
	int			parallelModeLevel;	/* 并行模式开关 */
	bool		chain;			/* 当前的block结束后,是否启动一个新的block */
	bool		assigned;		/* 父事务是否分配事务ID */
	struct TransactionStateData *parent;	/* 指向父事务t */
} TransactionStateData;
  • fullTransactionId
    每个事务都会由一个全局事务ID来表示,由fullTransactionId记录,fullTransactionId是一个64位整数,但是目前只用了32位来保存事务ID,事务ID的范围为1~2^32,是数据库非常宝贵的资源,所以不是每一个事务都会分配事务ID的,只有在进行IUD操作时,才会分配事务ID,普通事务只会分配一个虚拟事务ID。事务ID达到一定数量后,回触发数据库的冻结,以避免数据库的回卷。以后会讲到事务ID的分配和回卷。
  • subTransactionId
    子事务ID,当存在子事务时,从父事务开始计数,父事务为1,子事务按次序递增
  • name
    如果是子事务,保存子事务的名字,就是SAVEPOINT P1中的P1
  • savepointlevel
    子事务的嵌套深度,
  • nestinglevel
    事务的嵌套深度,父事务为1,子事务依次递增
  • gucNestLevel
    GUC嵌套深度,因为每个子事务中可能会修改GUC参数,所以每个子事务都需要保存对应的GUC信息
  • curTransactionContext
    当前的事务上下文
  • curTransactionOwner
    当前事务的resourceOwner信息
  • state
    事务状态,主要的状态有
typedef enum TransState
{
	TRANS_DEFAULT,				/* 默认状态 */
	TRANS_START,				/* 启动事务 */
	TRANS_INPROGRESS,			/* 事务在运行中 */
	TRANS_COMMIT,				/* 事务提交 */
	TRANS_ABORT,				/* 事务终止 */
	TRANS_PREPARE				/* 两阶段提交事务执行 */
} TransState;
  • blockState
    事务块状态,状态流转情况前面已经描述,不再赘述

事务块基本函数分析

我们就依据上面的示例所涉及的事务相关函数,深入分析一下具体事务执行的流程

上面说过任何一个SQL语句执行都会在语句开始执行前先执行StartTranscationCommand函数,在SQL执行之后会执行CommitTransactionCommand

BEGIN

  • StartTranscationCommand
    事务块默认的状态是TBLOCK_DEFAULT,这里要开启一个事务,调用StartTransaction启动一个事务,并将事务块状态修改为TBLOCK_STARTED
StartTransactionCommand(void)
{
	TransactionState s = CurrentTransactionState;

	switch (s->blockState) //跟据当前事务块的状态,修改为不同的状态
	{
		case TBLOCK_DEFAULT:
			StartTransaction();
			s->blockState = TBLOCK_STARTED;
			break;
  • StartTransaction
    启动一个事务,并进行相关的初始化工作,主要就是初始化事务结构体TransactionState,并将事务状态设置为TRANS_INPROGRESS。每个进程或者说事务块中默认存在一个事务结构体,存放在全局变量TopTransactionStateData中,只用将CurrentTransactionState指向这个事务,就将该事务切换为当前的事务了,就可以进行事务操作了。
/
 启动事务
 */
static void
StartTransaction(void)
{
	TransactionState s;
	VirtualTransactionId vxid;

	/*
	 * 获取当前的事务状态结构体
	 */
	s = &TopTransactionStateData;
	CurrentTransactionState = s;

	Assert(!FullTransactionIdIsValid(XactTopFullTransactionId)); //判断事务ID是否有效(是否为0)

	/* 启动一个事务时,事务的状态必须是TRANS_DEFAULT状态 */
	Assert(s->state == TRANS_DEFAULT);


	s->state = TRANS_START; //事务状态设置为start
	s->fullTransactionId = InvalidFullTransactionId; //事务ID设置未无效,后面有IUP操作时会再分配

	/* Determine if statements are logged in this transaction, 判断事务日志是否需要记录 */
	xact_is_sampled = log_xact_sample_rate != 0 &&
		(log_xact_sample_rate == 1 ||
		 random() <= log_xact_sample_rate * MAX_RANDOM_VALUE);

	/*
	 * initialize current transaction state fields
	 * 初始化事务结构体中子事务相关的值,这里不是子事务,所以初始化为初始值
	 */
	s->nestingLevel = 1;
	s->gucNestLevel = 1;
	s->childXids = NULL;
	s->nChildXids = 0;
	s->maxChildXids = 0;


	GetUserIdAndSecContext(&s->prevUser, &s->prevSecContext); //设置当前事务的用户及安全上下文标志

	/* SecurityRestrictionContext should never be set outside a transaction */
	Assert(s->prevSecContext == 0);

	if (RecoveryInProgress()) //如果是在恢复中,设置对应变量,并且事务设置为只读,恢复模式有些事务操作不能执行。
	{
		s->startedInRecovery = true;
		XactReadOnly = true;
	}
	else
	{
		s->startedInRecovery = false;
		XactReadOnly = DefaultXactReadOnly;
	}
	XactDeferrable = DefaultXactDeferrable;
	XactIsoLevel = DefaultXactIsoLevel;
	forceSyncCommit = false;
	MyXactFlags = 0;

	/*
	 * reinitialize within-transaction counters
	 初始化事务ID计数,command计数等
	 */
	s->subTransactionId = TopSubTransactionId;
	currentSubTransactionId = TopSubTransactionId;
	currentCommandId = FirstCommandId;
	currentCommandIdUsed = false;

	/*
	 * initialize reported xid accounting
	 初始化xid报告计数,XLOG是否记录xid设置为false
	 */
	nUnreportedXids = 0;
	s->didLogXid = false;

	/*
	 * must initialize resource-management stuff first
	 初始化资源管理器
	 */
	AtStart_Memory(); //初始化内存上下文
	AtStart_ResourceOwner(); //初始化资源管理器

	/*
	 分配本地事务ID,这里的事务ID是指虚拟事务ID,而真正的事务ID分配会在执行IUD操作时才会分配
	 */
	vxid.backendId = MyBackendId; //进程ID 
	vxid.localTransactionId = GetNextLocalTransactionId(); //本地事务ID

	/*
	 * Lock the virtual transaction id before we announce it in the proc array
	 虚拟事务ID加锁,并添加到进程中,后面可通过快速路径访问
	 */
	VirtualXactLockTableInsert(vxid);
	Assert(MyProc->backendId == vxid.backendId);
	MyProc->lxid = vxid.localTransactionId;//添加到proc中,这样就相当于广告了,可以被其他人访问

	TRACE_POSTGRESQL_TRANSACTION_START(vxid.localTransactionId);

	/*
	 在一个事务里,时间戳是事务开始时获取后就不会再变化的,但是对于并行worker,其父事务肯定已经被设置过了,这里就不需要再设置了
	 */
	if (!IsParallelWorker())
	{
		if (!SPI_inside_nonatomic_context())
			xactStartTimestamp = stmtStartTimestamp;
		else
			xactStartTimestamp = GetCurrentTimestamp();
	}
	else
		Assert(xactStartTimestamp != 0);
	pgstat_report_xact_timestamp(xactStartTimestamp); //报告时间戳
	/* Mark xactStopTimestamp as unset. */
	xactStopTimestamp = 0;

	/*
	 * initialize other subsystems for new transaction
	 初始化新事务的其他子系统,如GUC,cache,trigger
	 */
	AtStart_GUC(); //设置GUC嵌套深度为1,后面若是有子事务,嵌套深度会增加的
	AtStart_Cache(); //初始化IM,消息一致性相关
	AfterTriggerBeginXact(); //aftertrigger:事务块外的BEGIN或者implicit 

	/*
	 * done with start processing, set current transaction state to "in
	 * progress"
	 */
	s->state = TRANS_INPROGRESS; //事务状态修改为运行中

	ShowTransactionState("StartTransaction");
  • BeginTransactionBlock
    执行SQL语句“BEGIN”,调用的是ProcessUtility->standard_ProcessUtility->BeginTransactionBlock,实际调用的是BeginTransactionBlock函数,该函数只修改了一下事务块的状态。 StartTransactionCommand函数已经将事务块状态设置为TBBLOCK_STARTED, 这里会将其设置为TBLOCK_BEGIN。
BeginTransactionBlock(void)
{
	TransactionState s = CurrentTransactionState;

	switch (s->blockState)
	{
		case TBLOCK_STARTED:
			s->blockState = TBLOCK_BEGIN;
			break;
  • CommitTransactionCommand
    因为BEGIN执行之后将事务块状态修改为了TBLOCK_BEGIN,这里则将事务块状态修改为TBLOCK_INPROCESS,表示一个事务从现在开始就在运行中了。
void
CommitTransactionCommand(void)
{
	TransactionState s = CurrentTransactionState; //获取到当前事务

	if (s->chain)//如果有新块,设置保存标记
		SaveTransactionCharacteristics(); 

	switch (s->blockState)
	{
			case TBLOCK_BEGIN:
			s->blockState = TBLOCK_INPROGRESS;
			break;

SELECT/INSERT

执行SELECT和INSERT的SQL时,事务块状态不会发生变化,都还是处于TBLOCK_INPROGRESS,在执行INSERT的操作时,会调用GetCurrentTransactionId为该事务分配一个全局事务ID

 插入一条数据
 */
void
heap_insert(Relation relation, HeapTuple tup, CommandId cid,
			int options, BulkInsertState bistate)
{
	TransactionId xid = GetCurrentTransactionId(); //获取当前的事务ID,如果没有则分配一个新的事务ID

COMMIT

执行COMMIT的SQL时,会提交事务,并修改事务块状态

  • StartTransactionCommand
    没有修改事务块状态,还是处于TBLOCK_INPROGRESS状态
void
StartTransactionCommand(void)
{
	TransactionState s = CurrentTransactionState;

	switch (s->blockState) //跟据当前事务块的状态,修改喂不同的状态
	{
		case TBLOCK_INPROGRESS:
		case TBLOCK_IMPLICIT_INPROGRESS:
		case TBLOCK_SUBINPROGRESS:
			break;
  • EndTransactionBlock
    执行COMMIT 的SQL命令时,实际调用的是ProcessUtility->standard_ProcessUtility->EndTransactionBlock, EndTransactionBlock函数也只会修改事务块的状态,不会做其他任何操作,这里将事务块状态修改为TBLOCK_END。
bool
EndTransactionBlock(bool chain)
{
	TransactionState s = CurrentTransactionState;
	bool		result = false;

	switch (s->blockState)
	{
		case TBLOCK_INPROGRESS:
			s->blockState = TBLOCK_END;
			result = true;
			break;
  • CommitTransactionCommand
    因为现在的事务块状态是TBLOCK_END,在CommitTransactionCommand对应的逻辑如下,它会调用CommitTransaction函数提交事务,事务提交后会将事务块的状态重置为
    TBLOCK_DEFAULT。
void
CommitTransactionCommand(void)
{
	TransactionState s = CurrentTransactionState; //获取到当前事务

	if (s->chain)//如果有新块,设置保存标记
		SaveTransactionCharacteristics(); 

	switch (s->blockState)
	{ 
			case TBLOCK_END:
			CommitTransaction(); //提交事务
			s->blockState = TBLOCK_DEFAULT; //提交后事务块状态重置为DEFAULT
			if (s->chain) //如果存在新申请的块,也要保存
			{
				StartTransaction();
				s->blockState = TBLOCK_INPROGRESS;
				s->chain = false;
				RestoreTransactionCharacteristics();
			}
			break;
  • CommitTransaction
    事务提交真正的执行函数。主要内容就是提交事务,然后清理期间事务申请的各种资源。
  1. 事务提交前操作
    事务提交前,需要先触发一下延迟触发的触发器,关闭大对象,关闭中断等操作
   for (;;) //触发所有延迟触发器
   {
   	AfterTriggerFireDeferred();
   	if (!PreCommit_Portals(false))
   		break;
   }

   CallXactCallbacks(is_parallel_worker ? XACT_EVENT_PARALLEL_PRE_COMMIT
   				  : XACT_EVENT_PRE_COMMIT);  //并行模式的回调函数
   if (IsInParallelMode()) //如果是并行模式,现在就处理并行的一些上下文信息
   	AtEOXact_Parallel(true);
   AfterTriggerEndXact(true); //关闭所有的defferred-trigger
   PreCommit_on_commit_actions(); //在真正commit之前需要执行的一些动作
   smgrDoPendingSyncs(true, is_parallel_worker); //同步还没有被WAL记录的文件
   AtEOXact_LargeObject(true); //关闭大对象
   PreCommit_Notify(); //提交前如果有待处理的LISTEN的操作,提前加入到共享内存的监听队列中
   if (!is_parallel_worker)
   	PreCommit_CheckForSerializationFailure(); //如果是非并行模式,且事务隔离级别是串行化,需要检查是否有危险的结构 
   HOLD_INTERRUPTS();//关中断
   AtEOXact_RelationMap(true, is_parallel_worker);  //更新relation map
  1. 事务提交
    事务提交主要是调用RecordTransactionCommit函数进行
  2. 事务提交后
    事务提交后,主要就是释放事务期间使用的资源,重置使用的一些全局变量或共享内存中记录的信息,最后将事务状态改为DEFAULT
 */
	ProcArrayEndTransaction(MyProc, latestXid); //通知其他进程,当前事务已经结束,主要是清理ProCArray上记录的事务信息
//下面就是事务提交后的清理工作,即使这里出现错误,也已经来不及回滚了
	CallXactCallbacks(is_parallel_worker ? XACT_EVENT_PARALLEL_COMMIT
					  : XACT_EVENT_COMMIT);

	ResourceOwnerRelease(TopTransactionResourceOwner,
						 RESOURCE_RELEASE_BEFORE_LOCKS,
						 true, true);//释放共享缓冲区和RelCache
	AtEOXact_Buffers(true); //释放共享缓冲区的buffer pin
	AtEOXact_RelationCache(true); //清理relation cache
	AtEOXact_Inval(true); //IM消息通知,其他进程此时就可以看待该事务做出的修改了

	AtEOXact_MultiXact();//清理MultiXact相关的cache

	ResourceOwnerRelease(TopTransactionResourceOwner,
						 RESOURCE_RELEASE_LOCKS,
						 true, true);//释放锁
	ResourceOwnerRelease(TopTransactionResourceOwner,
						 RESOURCE_RELEASE_AFTER_LOCKS,
						 true, true);//释放其他资源

	smgrDoPendingDeletes(true);//事务期间有drop操作时,删除对应的文件
	AtCommit_Notify();//事务提交完成后且事务状态也已经更新到CLOG后,通知其他进程
	AtEOXact_GUC(true, 1); //处理GUC配置更新
	AtEOXact_SPI(true); //清理SPI状态
	AtEOXact_Enum(); //重置uncommit tables
	AtEOXact_on_commit_actions(true);//清理OnCommitItem信息
	AtEOXact_Namespace(true, is_parallel_worker); //清理namespace
	AtEOXact_SMgr();//关闭通过smgr打开的文件
	AtEOXact_Files(true);//关闭打开的临时文件VFD
	AtEOXact_ComboCid();//重置comboCid相关全局变量
	AtEOXact_HashTables(true);//关闭所有打开的scan
	AtEOXact_PgStat(true, is_parallel_worker); //pg_stat统计更新
	AtEOXact_Snapshot(true, false);//清理相关的快照信息
	AtEOXact_ApplyLauncher(true);//唤醒提交后登录
	pgstat_report_xact_timestamp(0);//重置pgstat的事务开始时间记录

	CurrentResourceOwner = NULL;
	ResourceOwnerDelete(TopTransactionResourceOwner);//删除该事务的resourceOwner
	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; //事务状态重置为DEFAULT
	RESUME_INTERRUPTS(); //释放中断
  • RecordTransactionCommit
    该函数主要是提交事务时,记录相应的信息并写入WAL日志中。
    如果事务执行过程中,没有IUD操作,也就没有分配事务ID,这就不会将事务记录到WAL日志中的。但是如果是主备模式,即使没有事务ID,那么就需要将IM消息记录写入到WAL日志中。
	TransactionId xid = GetTopTransactionIdIfAny(); //获取到事务ID
	bool		markXidCommitted = TransactionIdIsValid(xid);
	TransactionId latestXid = InvalidTransactionId;
	int			nrels;
	RelFileNode *rels;
	int			nchildren;
	TransactionId *children;
	int			nmsgs = 0;
	SharedInvalidationMessage *invalMessages = NULL;
	bool		RelcacheInitFileInval = false;
	bool		wrote_xlog;

	if (XLogLogicalInfoActive())
		LogLogicalInvalidations(); //将invalidations记录到WAL日志中

	/* Get data needed for commit record */
	nrels = smgrGetPendingDeletes(true, &rels); //获取要删除的非临时文件列表
	nchildren = xactGetCommittedChildren(&children);//获取当前事务的已经提交的子事务列表
	if (XLogStandbyInfoActive())//如果主备模式,还需要将IM消息记录到事务提交记录中
		nmsgs = xactGetCommittedInvalidationMessages(&invalMessages,
													 &RelcacheInitFileInval);
	wrote_xlog = (XactLastRecEnd != 0); //是否写XLOG

	if (!markXidCommitted)//如果没有分配事务ID,就不需要记录事务提交记录了
	{
		if (nrels != 0)
			elog(ERROR, "cannot commit a transaction that deleted files but has no xid");
		if (nmsgs != 0)
		{
			LogStandbyInvalidations(nmsgs, invalMessages,
									RelcacheInitFileInval); //有要写入的IM消息,这时候还是要写XLOG的
			wrote_xlog = true;	/* not strictly necessary */
		}
		if (!wrote_xlog) //如果不需要写XLOG,直接调到执行清理的步骤即可
			goto cleanup;
	}

如果已经分配了事务ID,那么就需要将事务记录写入到WAL日志,事务提交信息写入到CommitTs日志中。

	bool		replorigin;

		replorigin = (replorigin_session_origin != InvalidRepOriginId &&
					  replorigin_session_origin != DoNotReplicateId);
		BufmgrCommit(); //啥也没干
		START_CRIT_SECTION(); //开启临界区,也就是说下面的操作不允许有错误,出错就是CRITICAL
		MyProc->delayChkpt = true; //推迟当前进程的checkpoint
		SetCurrentTransactionStopTimestamp();//更新事务结束时间戳
        //事务提交记录写XLOG日志
		XactLogCommitRecord(xactStopTimestamp,
							nchildren, children, nrels, rels,
							nmsgs, invalMessages,
							RelcacheInitFileInval,
							MyXactFlags,
							InvalidTransactionId, NULL /* plain commit */ );

		if (replorigin) //如果配置了流复制功能,需要移动LSN
			/* Move LSNs forward for this replication origin */
			replorigin_session_advance(replorigin_session_origin_lsn,
									   XactLastRecEnd);
		if (!replorigin || replorigin_session_origin_timestamp == 0)
			replorigin_session_origin_timestamp = xactStopTimestamp;
         //将事务提交结果写入到CommitTS中
		TransactionTreeSetCommitTsData(xid, nchildren, children,
									   replorigin_session_origin_timestamp,
									   replorigin_session_origin);

如果是同步模式的话,就会直接将WAL日志刷入磁盘,并将结果写入到CLOG中
如果是异步模式的话,则记录最新的异步提交的LSN,等待WAL writer刷该事务的COMMIT信息。
离开临界区后,更新最新的事务的ID,然后清理相关信息后,返回最新的事务ID。

	if ((wrote_xlog && markXidCommitted &&
		 synchronous_commit > SYNCHRONOUS_COMMIT_OFF) ||
		forceSyncCommit || nrels > 0) //如果允许写XLOG,且同步提交关闭后,可以先刷XLOG到磁盘
	{
		XLogFlush(XactLastRecEnd); //将XLOG日志刷入到磁盘
		if (markXidCommitted)
			TransactionIdCommitTree(xid, nchildren, children); //将事务提交结果写入到CLOG中
	}
	else //异步提交
	{
		XLogSetAsyncXactLSN(XactLastRecEnd);//报告最新的异步提交LSN,这样WAL writer就知道刷新该commit信息了
		if (markXidCommitted) //保存LSN直到CLOG被更新后
			TransactionIdAsyncCommitTree(xid, nchildren, children, XactLastRecEnd);
	}
	if (markXidCommitted)
	{
		MyProc->delayChkpt = false; //取消进程checkpoint推迟
		END_CRIT_SECTION(); //离开临界区
	}
	latestXid = TransactionIdLatest(xid, nchildren, children); //如果有子事务,计算出最新的事务ID

	if (wrote_xlog && markXidCommitted)
		SyncRepWaitForLSN(XactLastRecEnd, true);//如果有需求,等待同步复制
	XactLastCommitEnd = XactLastRecEnd;
	XactLastRecEnd = 0;
cleanup:
	/* Clean up local data */
	if (rels)
		pfree(rels);//释放本地数据

其他函数

当事务出现错误或执行ROLLBACK时,会回滚事务,主要就是调用AbortTransaction函数

  • AbortTransaction
    重置事务执行期间申请的资源或配置的变量,事务状态修改为ABORT
TransactionState s = CurrentTransactionState;
   TransactionId latestXid;
   bool		is_parallel_worker;

   /* Prevent cancel/die interrupt while cleaning up */
   HOLD_INTERRUPTS(); //屏蔽中断

   /* Make sure we have a valid memory context and resource owner */
   AtAbort_Memory(); //确认内存上下文是否可用
   AtAbort_ResourceOwner(); //确认resourceOwner是否可用
   LWLockReleaseAll(); //释放所有轻量锁
   pgstat_report_wait_end(); //pgstat报告等待结束
   pgstat_progress_end_command(); //pgstat通知进程结束
   AbortBufferIO(); //清理所有的buffer I/O
   UnlockBuffers();//释放所有buffer上的content 锁
   XLogResetInsertion(); // 重置WAL 插入相关变量
   ConditionVariableCancelSleep(); //取消条件变量睡眠
   LockErrorCleanup(); //取消锁等待
   reschedule_timeouts();//如果有超时时间仍然存活的话,执行超时中断
   PG_SETMASK(&UnBlockSig); //重置全局变量
   is_parallel_worker = (s->blockState == TBLOCK_PARALLEL_INPROGRESS);
   if (s->state != TRANS_INPROGRESS && s->state != TRANS_PREPARE)
   	elog(WARNING, "AbortTransaction while in %s state",
   		 TransStateAsString(s->state));
   Assert(s->parent == NULL);
   s->state = TRANS_ABORT; //事务状态修改为abort
   SetUserIdAndSecContext(s->prevUser, s->prevSecContext); //重置上下文
   ResetReindexState(s->nestingLevel); //重置索引
   ResetLogicalStreamingState(); //重置逻辑复制
   SnapBuildResetExportedSnapshotState();//重置快照
   if (IsInParallelMode()) //并行模式下,清理works
   {
   	AtEOXact_Parallel(false);
   	s->parallelModeLevel = 0;
   }
   AfterTriggerEndXact(false); /* 'false' means it's abort 取消所有还未触发的的触发器*/
   AtAbort_Portals(); //处理portals
   smgrDoPendingSyncs(false, is_parallel_worker);//处理文件同步,删除相应的文件
   AtEOXact_LargeObject(false); //处理大文件对象
   AtAbort_Notify();//处理notify信息
   AtEOXact_RelationMap(false, is_parallel_worker);//处理relation map
   AtAbort_Twophase();//解锁占用的全局事务结构变量

事务终止时调用的RecordTransactionAbort函数,该函数记录该事务终止信息,并写入到WAL日志中

	if (!is_parallel_worker)
		latestXid = RecordTransactionAbort(false); //事务退出
	else
	{
		latestXid = InvalidTransactionId;
		XLogSetAsyncXactLSN(XactLastRecEnd); //并行模式,我们只更新LSN即可
	}

之后就是一些常规的清理了,这里如果resourceOwner没有创建成功就失败了,那么下面许多清理就不需要再做了

ProcArrayEndTransaction(MyProc, latestXid);//从ProcArray中删除当前事务相关的信息
	if (TopTransactionResourceOwner != NULL) //如果是resourceOwner就没创建就事务终止了,下面的清理就不需要做了
	{
		if (is_parallel_worker)
			CallXactCallbacks(XACT_EVENT_PARALLEL_ABORT);
		else
			CallXactCallbacks(XACT_EVENT_ABORT);

		ResourceOwnerRelease(TopTransactionResourceOwner,
							 RESOURCE_RELEASE_BEFORE_LOCKS,
							 false, true);
		AtEOXact_Buffers(false);
		AtEOXact_RelationCache(false);
		AtEOXact_Inval(false);
		AtEOXact_MultiXact();
		ResourceOwnerRelease(TopTransactionResourceOwner,
							 RESOURCE_RELEASE_LOCKS,
							 false, true);
		ResourceOwnerRelease(TopTransactionResourceOwner,
							 RESOURCE_RELEASE_AFTER_LOCKS,
							 false, true);
		smgrDoPendingDeletes(false);

		AtEOXact_GUC(false, 1);
		AtEOXact_SPI(false);
		AtEOXact_Enum();
		AtEOXact_on_commit_actions(false);
		AtEOXact_Namespace(false, is_parallel_worker);
		AtEOXact_SMgr();
		AtEOXact_Files(false);
		AtEOXact_ComboCid();
		AtEOXact_HashTables(false);
		AtEOXact_PgStat(false, is_parallel_worker);
		AtEOXact_ApplyLauncher(false);
		pgstat_report_xact_timestamp(0);
	}
	RESUME_INTERRUPTS(); //释放中断
  • RecordTransactionAbort
    跟recordTransactionCommit差不多流程,就不多赘述了,主要就是记录事务结束日志到WAL日志中。
	TransactionId xid = GetCurrentTransactionIdIfAny();//获取当前的事务ID
	TransactionId latestXid;
	int			nrels;
	RelFileNode *rels;
	int			nchildren;
	TransactionId *children;
	TimestampTz xact_time;
	if (!TransactionIdIsValid(xid)) //如果事务ID没有分配,直接返回即可
	{
		/* Reset XactLastRecEnd until the next transaction writes something */
		if (!isSubXact)
			XactLastRecEnd = 0;
		return InvalidTransactionId;
	}

	if (TransactionIdDidCommit(xid)) //如果事务已经提交了,则不能再abort了,即过了RecordTransactionCommit阶段就不能abort或回滚了
		elog(PANIC, "cannot abort transaction %u, it was already committed",
			 xid);
	nrels = smgrGetPendingDeletes(false, &rels); //获取记录的数据
	nchildren = xactGetCommittedChildren(&children);
	START_CRIT_SECTION();
	if (isSubXact)
		xact_time = GetCurrentTimestamp();
	else
	{
		SetCurrentTransactionStopTimestamp();
		xact_time = xactStopTimestamp;
	}

	XactLogAbortRecord(xact_time,
					   nchildren, children,
					   nrels, rels,
					   MyXactFlags, InvalidTransactionId,
					   NULL);//记录abort的记录数据,两阶段提交可能会用
	if (!isSubXact)
		XLogSetAsyncXactLSN(XactLastRecEnd); //获取最新的LSN
	TransactionIdAbortTree(xid, nchildren, children);//CLOG中更新事务状态

	END_CRIT_SECTION(); //关闭临界区

	/* Compute latestXid while we have the child XIDs handy */
	latestXid = TransactionIdLatest(xid, nchildren, children);//获取最新的事务ID,主要是判断当前的事务ID和子事务ID的大小,取最大值
	if (isSubXact)
		XidCacheRemoveRunningXids(xid, nchildren, children, latestXid);//从proc中清除失败的事务ID
	if (!isSubXact)
		XactLastRecEnd = 0;
	if (rels)
		pfree(rels);

	return latestXid;

【参考】

  1. 《PostgreSQL数据库内核分析》
  2. 《Postgresql技术内幕-事务处理深度探索》
  3. pg14源码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值