海山数据库(He3DB)源码详解:AbortTransaction函数
本文介绍了事务提交过程中,具体执行提交任务的AbortTransaction函数详细执行流程。
1. 执行条件
存在三种调用此函数的场景:
- SQL语法出错,由sigsetjmp函数跳回PostgresMain函数,从AbortCurrentTransaction函数中调用AbortTransaction函数。
- 事务执行过程中出错,跳出执行AbortTransaction函数。
- 主动放弃事务,执行AbortTransaction函数。
2. 执行过程
2.1 获取当前状态:
申请事务状态变量TransactionState s和并创建两个临时变量latestXid和is_parallel_worker。
将CurrentTransactionState变量赋值给TransactionState s。
TransactionState s = CurrentTransactionState;
TransactionId latestXid;
bool is_parallel_worker;
2.2 开始事务资源清理:
关闭中断开始清理工作。
/* Prevent cancel/die interrupt while cleaning up */
HOLD_INTERRUPTS();
切换事务上下文和资源拥有者。
/* Make sure we have a valid memory context and resource owner */
AtAbort_Memory();
AtAbort_ResourceOwner();
- AtAbort_Memory()负责切换事务上下文到TransactionAbortContext,如果这个上下文没有足够的空间就切换到TopMemoryContext。
- 因为Abort的操作肯定是从top开始,AtAbort_ResourceOwner负责将TopTransactionResourceOwner切换到CurrentResourceOwner 。
释放所有LW锁,清除等待信息和命令进度指示器。
LWLockReleaseAll();
/* Clear wait information and command progress indicator */
pgstat_report_wait_end();
pgstat_progress_end_command();
事务在Abort之前可能会遇到各种需要等待的情况,这些等待可能是由于资源争用、锁、系统限制或其他原因造成的,因此:
- 调用LWLockReleaseAll()函数释放所有LW锁,需要释放LW锁;
- 调用pgstat_report_wait_end()函数,需要pgstat_report_wait_end()通知数据库后台事务等待结束;
- 调用pgstat_progress_end_command()函数,需要pgstat_progress_end_command()通知数据库后台结束对当前事务进度的报告。
清除I/O和缓存相关的资源。
/* Clean up buffer I/O and buffer context locks, too */
AbortBufferIO();
UnlockBuffers();
AbortBufferIO()的作用主要是在事务中止(Abort)时进行相关的清理工作。
- 中断 I/O 操作:如果事务在中止时有正在进行的 I/O 操作,AbortBufferIO 会尝试中断这些操作,以确保不会有数据被错误地写入磁盘。
- 清除缓冲区标记:事务可能会标记缓冲区为“脏”(即已修改),表示这些缓冲区的数据已经被更改,需要被刷新到磁盘。AbortBufferIO 会清除这些标记,因为中止的事务不会将更改永久化。
- UnlockBuffers()主要作用是在事务结束时释放事务过程中所持有的缓冲区锁,以及清理固定计数(PIN COUNT)
- 固定(Pinning)一个缓冲区意味着告诉系统在某些操作完成之前不要释放该缓冲区,因此,即使锁已经被释放,缓冲区仍然处于被固定状态,不能被操作。
重置WAL记录的构造状态。
/* Reset WAL record construction state */
XLogResetInsertion();
XLogResetInsertion()的作用是重置WAL记录的构造状态。重置插入状态意味着清除当前事务对WAL日志记录的所有影响,撤销任何未完成的 WAL 记录构造,确保 WAL 日志的整洁和正确性。具体的来说:
- 清除当前事务可能已经开始构建但未完成的WAL记录;
- 撤销对WAL缓冲区的任何修改,这些修改可能是由于事务中的操作而进行的;
- 确保在事务失败或需要回滚时,不会有部分完成的WAL记录留在系统中。
清理任何未完成的条件等待睡眠。
/* Cancel condition variable sleep */
ConditionVariableCancelSleep();
ConditionVariableCancelSleep()的作用是取消睡眠状态,将当前线程的等待状态从条件变量的等待队列中移除,如果线程没有处于睡眠状态(等待条件变量),则什么都不做。
取消所有挂起的锁等待要求并撤销进行中的强锁计数获取。
LockErrorCleanup();
- LockErrorCleanup()主要作用:在事务中止时取消所有挂起的锁等待请求。挂起的锁等待指的是,某些操作因为等待锁的释放而被阻塞。比如,查询操作对数据的读取,创建一个索引的操作。
- LockErrorCleanup()还会撤销(revert)任何正在进行中的强锁计数获取。在 PostgreSQL 中,锁有一个计数机制,用于跟踪同一锁被请求的次数。如果事务在获取锁的过程中被中止,需要确保相关的锁计数被恢复到正确的状态。
重新安排超时事件。
reschedule_timeouts();
reschedule_timeouts()的作用是重新安排超时事件。
- 如果存在任何仍然处于活动状态的超时事件,该函数确保已经安排了超时中断。在清理锁错误之后,系统会检查是否有任何超时事件仍然需要处理,并确保相应的超时中断被重新安排。
- 这样做可以防止由于异常退出信号处理器而导致的超时中断丢失,并避免重新安排锁或死锁的超时检查。
重新启用(解除屏蔽)信号。
PG_SETMASK(&UnBlockSig);
- PG_SETMASK 宏或函数的作用是重新启用(解除屏蔽)信号。这是为了处理一种特定情况:程序可能通过 longjmp 函数从信号处理器(signal handler)中跳出。
- 在 C 语言中,longjmp 可以跳出最近的 setjmp 调用,通常用于异常处理或从信号处理器中恢复执行。如果程序通过这种方式跳出信号处理器,可能需要重新启用之前被屏蔽的信号。
设置用户ID和安全上下文。
SetUserIdAndSecContext(s->prevUser, s->prevSecContext);
为了确保用户ID可能因为某些操作的临时改变能够被正确处理,调用SecurityRestrictionContext函数保存prior的ID和安全上下文,在恢复时能够恢复到一个正确的状态。
重置REINDEX状态、逻辑流复制状态和快照状态。
/* Forget about any active REINDEX. */
ResetReindexState(s->nestingLevel);
/* Reset logical streaming state. */
ResetLogicalStreamingState();
/* Reset snapshot export state. */
SnapBuildResetExportedSnapshotState();
- ResetReindexState用于重置当前会话或事务的REINDEX状态,取消或撤销任何当前处于活动状态的REINDEX操作。REINDEX是PostgreSQL中用来重建索引的命令,它可能会改变数据库的索引结构和统计信息。
- ResetLogicalStreamingState函数的作用是重置逻辑流复制的状态。CheckXidAlive = InvalidTransactionId; bsysscan = false;
- SnapBuildResetExportedSnapshotState函数的作用是重置由快照构建器(snapshot builder)导出的快照状态。在 PostgreSQL 中,快照是数据库在某一时刻的状态的视图,可以用于复制、备份或其他数据库维护任务。
执行事务放弃过程。
/*
* do abort processing
*/
AfterTriggerEndXact(false); /* 'false' means it's abort */
AtAbort_Portals();
smgrDoPendingSyncs(false, is_parallel_worker);
AtEOXact_LargeObject(false);
AtAbort_Notify();
AtEOXact_RelationMap(false, is_parallel_worker);
AtAbort_Twophase();
-
AfterTriggerEndXact(false)表示处理事务结束时的后触发器的函数,它接受一个布尔参数isCommit,该参数指示事务是提交 (true) 还是回滚 (false)。
如果事务中有未触发的触发器,它们将被取消,并且与这些触发器相关的任何信息或状态都将被丢弃。在事务结束时,不需要执行这些未触发的触发器,也不需要保留它们的任何状态信息。
-
AtAbort_Portals 函数的作用是在事务中止时对门户(Portals)进行清理中止处理。门户是存储的游标,它们可以包含复杂的查询和状态信息。当事务中止时,需要清理门户以释放资源并确保不会留下任何未完成的操作。
-
smgrDoPendingSyncs函数会在当前事务期间,对那些被创建且其更改没有记录在 WAL(Write-Ahead Logging)日志中的文件进行同步。这是为了确保如果发生崩溃,这些文件的状态是一致的,并且不会留下已提交但损坏的文件。
-
AtEOXact_LargeObject函数主要是清理一些超出常规字段所能存储限制的“大对象”,关闭与大型对象关联的文件描述符(LO fds),并清空 cookie 数组。
这样做的结果是,这些文件描述符将不再有效,无法再被用来访问或操作大型对象。无论事务是提交还是中止,持有这些 LO fds 的内存上下文和资源所有者在事务结束时都将被销毁。这意味着这些资源在事务结束时无论如何都会被清理。
在事务提交的情况下,需要关闭这些 LO fds,以避免在提交时出现关于资源泄露的警告。这是因为在提交时,系统会检查所有资源是否已经被正确管理,包括打开的文件描述符。
在事务中止的情况下,可以跳过关闭 LO fds 的步骤。这是因为在中止事务时,系统会回滚所有更改,并且通常不需要执行与提交相同的清理操作。
-
AtAbort_Notify 函数的作用是清除所有挂起的操作和出站通知(outbound notifies)。这里的“挂起的操作”指的是在事务中计划执行但尚未执行的动作。“出站通知”可能指的是使用NOTIFY命令发送的信号,这些信号原本计划在事务提交时发送,以通知其他进程或应用程序有关事件的发生,但现在事务放弃了,就不发送NOTIFY命令通知其他程序,直接放弃。
-
AtAbort_Twophase 函数的作用是在事务中止时解锁我们正在工作的全局事务条目。非分布式的事务中,全局变量MyLockedGxact为NULL,函数不做任何操作直接返回。如果存在2PC,根据事务中止时的不同情况,如何处理全局事务条目。这包括在某些情况下从共享内存中移除条目,以及在其他情况下保留并解锁条目,以允许事务稍后完成或正确结束。
清理GUC、SPI、HashTables等资源的占用。
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);
具体可以参考CommitTransaction函数的清理过程。
2.3 完成事务资源清理
完成事务的放弃操作,恢复中断。
/*
* State remains TRANS_ABORT until CleanupTransaction().
*/
RESUME_INTERRUPTS();
这里只是完成了放弃事务需要的资源清理工作,对事务的放弃完成需要执行CleanupTransaction()函数。
作者介绍
李超,移动云数据库工程师,负责云原生数据库He3DB的研发。