结束事务分为两类:
由于比较长,我们分开两篇记录。
会话1,隐式事务会自动提交
会话2
跟之前一样,我们先只是过一遍这个函数流程,里面具体调用函数的步骤这里都会先跳过,避免深陷泥潭。Lets Go!
调用栈如下
/*
* CommitTransaction
*
* 注意:如果想修改此函数逻辑,最好同时也修改 PrepareTransaction 函数
*/
static void
CommitTransaction(void)
{
TransactionState s = CurrentTransactionState; //事务栈
TransactionId latestXid;
bool is_parallel_worker;
is_parallel_worker = (s->blockState == TBLOCK_PARALLEL_INPROGRESS);
我看到这有点疑惑,之前的文章不是说隐式事务不会有INPROGRESS的状态吗?于是又回去翻了一下,噢,原来是隐式事务没有TBLOCK_INPROGRESS状态(事务块状态而不是事务状态)。这俩蛮容易弄混的,需要注意。postgresql源码学习(二)—— 事务块状态转换_Hehuyi_In的博客-CSDN博客
/* 如果是并行worker提交,强制进入并行模式. */
if (is_parallel_worker)
EnterParallelMode();
ShowTransactionState("CommitTransaction");
/*
* 检查当前事务状态
*/
if (s->state != TRANS_INPROGRESS)
elog(WARNING, "CommitTransaction while in %s state",
TransStateAsString(s->state));
Assert(s->parent == NULL);
/*
* Do pre-commit processing that involves calling user-defined code, such
* as triggers. SECURITY_RESTRICTED_OPERATION contexts must not queue an
* action that would run here, because that would bypass the sandbox.
* Since closing cursors could queue trigger actions, triggers could open
* cursors, etc, we have to keep looping until there's nothing left to do.
* 执行涉及调用用户定义代码(如触发器)的预提交处理。
* 因为关闭游标可能会执行触发器,触发器可能打开游标等等,所以我们必须一直循环,直到没有什么可做的。
*/
for (;;)
{
/*
* 触发所有after触发器
*/
AfterTriggerFireDeferred();
/*
* Close open portals (converting holdable ones into static portals).
* If there weren't any, we are done ... otherwise loop back to check
* if they queued deferred triggers. Lather, rinse, repeat.
* 关闭打开的portals(将holdable portals转换为static portals),portals好像是一种内存资源?
* 循环检查触发器队列直至全部关闭完
*/
if (!PreCommit_Portals(false))
break;
}
/*
* The remaining actions cannot call any user-defined code, so it's safe
* to start shutting down within-transaction services. But note that most
* of this stuff could still throw an error, which would switch us into
* the transaction-abort path.
* 剩余的操作不能调用用户自定义的代码,因此可以安全地开始关闭事务内的服务。
* 但是请注意,大多数这些动作仍然会抛出错误,这会导致流程切换到事务中止的路径上。
*/
CallXactCallbacks(is_parallel_worker ? XACT_EVENT_PARALLEL_PRE_COMMIT
: XACT_EVENT_PRE_COMMIT);
/* 如果有并行workers, 需要清理 */
if (IsInParallelMode())
AtEOXact_Parallel(true);
/* Shut down the deferred-trigger manager,关闭延迟触发器管理器 */
AfterTriggerEndXact(true);
/*
* Let ON COMMIT management do its thing (must happen after closing
* cursors, to avoid dangling-reference problems)
* 由ON COMMIT管理器执行(必须在关闭游标后执行,避免挂起引用问题)
*/
PreCommit_on_commit_actions();
/*
* Synchronize files that are created and not WAL-logged during this
* transaction. This must happen before AtEOXact_RelationMap(), so that we
* don't see committed-but-broken files after a crash.
* 同步该事务创建的没有WAL日志记录的文件。这步必须在AtEOXact_RelationMap()前执行,避免在crash后我们看不到已提交但是broken了的文件
*/
smgrDoPendingSyncs(true, is_parallel_worker);
/* close large objects before lower-level cleanup ,在低级别清理前关闭大对象 */
AtEOXact_LargeObject(true);
/*
* Insert notifications sent by NOTIFY commands into the queue. This
* should be late in the pre-commit sequence to minimize time spent
* holding the notify-insertion lock. However, this could result in
* creating a snapshot, so we must do it before serializable cleanup.
* 将NOTIFY命令发送的通知插入到队列中。这应该在预提交序列的末尾,以最小化持有notify-insertion锁的时间。然而这可能导致创建一个快照,因此必须在序列化清理前执行这步。
*/
PreCommit_Notify();
/*
* Mark serializable transaction as complete for predicate locking
* purposes. This should be done as late as we can put it and still allow
* errors to be raised for failure patterns found at commit. This is not
* appropriate in a parallel worker however, because we aren't committing
* the leader's transaction and its serializable state will live on.
*/
if (!is_parallel_worker)
PreCommit_CheckForSerializationFailure();
/* Prevent cancel/die interrupt while cleaning up,清理期间禁用中断,避免被打断 */
HOLD_INTERRUPTS();
/* Commit updates to the relation map --- do this as late as possible,提交更新到relation map -- 尽量晚地执行该动作 */
AtEOXact_RelationMap(true, is_parallel_worker);
注意这里有特别重要的两步:
- 设置事务状态为已提交 s->state = TRANS_COMMIT;
- 将事务日志写回磁盘 RecordTransactionCommit(),关于这步,以后还要继续深入学习
/*
* 设置事务状态
*/
s->state = TRANS_COMMIT;
s->parallelModeLevel = 0;
if (!is_parallel_worker)
{
/*
* We need to mark our XIDs as committed in pg_xact. This is where we
* durably commit.将已提交的xid保存在pg_xact中(事务日志写回磁盘),这是我们持久化提交的位置
*/
latestXid = RecordTransactionCommit();
}
else
{
/*
* We must not mark our XID committed; the parallel leader is
* responsible for that.并行worker则不需要标记,由并行leader处理
*/
latestXid = InvalidTransactionId;
/*
* Make sure the leader will know about any WAL we wrote before it
* commits.确保leader在提交之前知道worker写入的WAL
*/
ParallelWorkerReportLastRecEnd(XactLastRecEnd);
}
TRACE_POSTGRESQL_TRANSACTION_COMMIT(MyProc->lxid);
/*
* Let others know about no transaction in progress by me. Note that this
* must be done _before_ releasing locks we hold and _after_
* RecordTransactionCommit.
* 通知其他进程,本进程中已没有进行中的事务。
* 注意,这必须在释放持有的锁之前、RecordTransactionCommit之后执行。
*/
ProcArrayEndTransaction(MyProc, latestXid);
/*
* This is all post-commit cleanup. Note that if an error is raised here,
* it's too late to abort the transaction. This should be just
* noncritical resource releasing.
* 这些都是提交后清理。
* 请注意,如果这里才出现错误终止事务就太迟了,这应该是非关键的资源释放。
* The ordering of operations is not entirely random. The idea is:
* release resources visible to other backends (eg, files, buffer pins);
* then release locks; then release backend-local resources. We want to
* release locks at the point where any backend waiting for us will see
* our transaction as being fully cleaned up.
* 操作的顺序并不是完全随机的。
* 其思想是:先释放对其他后台进程可见的资源(如文件、buffer pins),然后释放锁,最后释放后端本地资源。
* 我们希望在所有等待本后台进程看到本事务被完全清理时才释放锁。
* Resources that can be associated with individual queries are handled by
* the ResourceOwner mechanism. The other calls here are for backend-wide
* state.
* 与单个查询关联的资源由ResourceOwner机制处理。
* 这里的其他调用是针对后台进程范围状态的。
*/
CallXactCallbacks(is_parallel_worker ? XACT_EVENT_PARALLEL_COMMIT
: XACT_EVENT_COMMIT);
ResourceOwnerRelease(TopTransactionResourceOwner,
RESOURCE_RELEASE_BEFORE_LOCKS,
true, true);
/* Check we've released all buffer pins,检查所有已释放的buffer pins */
AtEOXact_Buffers(true);
/* Clean up the relation cache,清理关系缓存 */
AtEOXact_RelationCache(true);
/*
* Make catalog changes visible to all backends. This has to happen after
* relcache references are dropped (see comments for
* AtEOXact_RelationCache), but before locks are released (if anyone is
* waiting for lock on a relation we've modified, we want them to know
* about the catalog change before they start using the relation).
* 使目录更改对所有后台进程可见。
* 这必须发生在relcache引用被删除之后(参见AtEOXact_RelationCache注释),
* 但在锁被释放之前(如果有人在等待我们修改了的表的锁,我们希望他们在开始使用该表前知道目录的更改)。
*/
AtEOXact_Inval(true);
AtEOXact_MultiXact();
ResourceOwnerRelease(TopTransactionResourceOwner,
RESOURCE_RELEASE_LOCKS,
true, true);
ResourceOwnerRelease(TopTransactionResourceOwner,
RESOURCE_RELEASE_AFTER_LOCKS,
true, true);
/*
* Likewise, dropping of files deleted during the transaction is best done
* after releasing relcache and buffer pins. (This is not strictly
* necessary during commit, since such pins should have been released
* already, but this ordering is definitely critical during abort.) Since
* this may take many seconds, also delay until after releasing locks.
* Other backends will observe the attendant catalog changes and not
* attempt to access affected files.
* 同样,在事务期间删除的文件的清理最好在释放relcache和buffer pin之后进行。(这在提交过程中并不是必须的,因为这样的pins应该已经被释放了,但是该顺序在中止过程中绝对是至关重要的。)
* 因为这可能需要较长的时间,所以也要延迟到释放锁之后。
* 其他后台进程将监控相关的catalog更改,不尝试访问受影响的文件。
*/
smgrDoPendingDeletes(true);
AtCommit_Notify();
//一大波资源清理
AtEOXact_GUC(true, 1);
AtEOXact_SPI(true);
AtEOXact_Enum();
AtEOXact_on_commit_actions(true);
AtEOXact_Namespace(true, is_parallel_worker);
AtEOXact_SMgr();
AtEOXact_Files(true);
AtEOXact_ComboCid();
AtEOXact_HashTables(true);
AtEOXact_PgStat(true, is_parallel_worker);
AtEOXact_Snapshot(true, false);
AtEOXact_ApplyLauncher(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;
/*
* done with commit processing, set current transaction state back to default,完成事务提交后,将当前事务状态改回TRANS_DEFAULT
*/
s->state = TRANS_DEFAULT;
RESUME_INTERRUPTS(); //恢复中断
}
上面流程图里最重要的是事务日志写回磁盘部分:RecordTransactionCommit(),保证已提交数据不会丢失。
if ((wrote_xlog && markXidCommitted &&
synchronous_commit > SYNCHRONOUS_COMMIT_OFF) ||
forceSyncCommit || nrels > 0) //判断是否要求同步提交,nrels > 0暂时没懂
{
XLogFlush(XactLastRecEnd); //如果是,则必须先写日志
/*
* Now we may update the CLOG, if we wrote a COMMIT record above。更新CLOG,更新事务状态
*/
if (markXidCommitted)
TransactionIdCommitTree(xid, nchildren, children);
}
else //如果是异步提交,不要求先写WAL,但崩溃时可能有数据丢失
{
/*设置异步提交最新的LSN */
XLogSetAsyncXactLSN(XactLastRecEnd);
/*
* 将最新的LSN保存到异步提交的事务组中,不要求先写WAL即可提交事务,同时将事务状态保存到CLOG中
*/
if (markXidCommitted)
TransactionIdAsyncCommitTree(xid, nchildren, children, XactLastRecEnd);
}
RecordTransactionCommit函数内的主要调用流程如下,本篇我们暂时不详细分析了
参考:
《PostgreSQL技术内幕:事务处理深度探索》第1章
《PostgreSQL数据库内核分析》第7章
PostgreSQL 源码解读(122)- MVCC#7(提交事务-整体流程)_ITPUB博客
PostgreSQL 源码解读(123)- MVCC#8(提交事务-实际提交过程)_ITPUB博客