pg内核之事务管理器(三) 两阶段提交

#pg内核 #事务

概述

两阶段提交主要用于分布式数据库中,分布式数据库本质上是将数据分布到不同的主机上,以实现大数据量的存储。例如greenplum数据库就是基于pg的分布式分析型数据库。分布式数据库主要有两个关注点:分布式执行计划,分布式事务。而分布式事务就是本章要将的两阶段提交。
两阶段提交主要解决两个难点:

  • 读操作需要保证在不同的主机上读到一致的数据。
  • 需要保证事务在不同主机上的原子性,不能出现某个主机上写数据成功,另一个主机上写数据失败的情况。

两阶段提交就是针对写操作,将事务的提交分为两个阶段,当数据写入不同的主机时,要么全部提交,要么全部回滚。
两阶段提交遵循XA协议,XA协议是一种分布式事务协议,由Tuxedo提出。XA中主要可以分为两部分:事务管理器和本地资源管理器。 其中本地资源管理器由本地数据库实现,事务管理器作为全局的调度者,负责各个本地资源的提交和回滚。

  • 2PC: 两阶段提交,是XA协议用于在全局事务中协调多个资源的机制,遵循OSI/DTP 标准。包含两个阶段:准备阶段和提交阶段
  • 3PC: 三阶段提交,是两阶段提交的改进版,包含三个阶段:canCommit阶段、PreCommit阶段和DoCommit阶段。相比于两阶段提交,它的改进点是:
    1. 同时在协调者和参与者中引入超时机制
    2. 引入了一个准备阶段,保证最后提交阶段之前各参与节点的状态是一致的。

pg中是通过max_prepared_transactions参数来控制两阶段提交,当为0时关闭两阶段提交,非0时打开两阶段提交。
正常的事务,在事务commit之前,事务的数据时不会落盘的,一旦这时数据库异常或事务异常,数据将会丢失,但是两阶段提交执行后,即使没有提交,事务相关数据也会刷入磁盘,这时候即使重启了数据库,之前2PC执行的相关事务数据仍然存在。

2PC的使用示例

  • 打开2PC的开关
    配置max_prepared_transactions为非0值,如100
  • 执行2PC SQL命令
CREATE TABLE tb1(id int);
BEGIN;
PREPARE TRANSACTION 'P1';  --创建两阶段提交事务
INSERT INTO tb1 VALUES(1); --插入一行数据
SELECT * FROM tb1;  --查询表的内容
SELECT * FROM pg_prepared_xacts; --查询两阶段提交事务内容 
  • 重启数据库
  • 查看2PC并提交
SELECT * FROM pg_prepared_xacts; --查询两阶段提交事务内容 
COMMIT PREPARED 'P1'; --提交两阶段提交事务
  • 回滚2PC
ROLLBACK PREPARED 'P1';

2PC执行流程

2PC的执行流程主要分为两个阶段: PREPARE阶段,COMMIT/ROLLBACK阶段
引入一个协调者coordinator来协调各个数据库的准备和提交,有了解greenplum数据库的应该就知道,greenplum中的master其实就是扮演者coordinator的角色,各个segment就是对应着各个数据库。
在这里插入图片描述

准备阶段

  • coordinator向所有参与的数据库发出预处理请求prepare,并开始等待各参与者的响应。
  • 各参与者执行本地事务,执行完成后并不会真正提交,也不会释放资源,而是先向coordinator报告结果,是否可以commit。
  • coordinator接收到所有的参与者的反馈后,决定是否继续完成commit。
    整个准备阶段只要有任一参与者没有正确响应,就会导致事务abort,而coordinator需要记录相关的日志信息。

提交/回滚阶段

  • coordinator向所有参与者发出commit/rollback请求
  • 各参与者收到commit/rollback请求之后,正式的执行本地事务的commit操作并记录日志,然后释放相关的资源。最后回复coordinator执行结果
  • coordinator收到所有参与者的回复之后,记录日志并释放资源。

两阶段提交相关结构

GlobalTransactionData

全局事务数据结构体,保存每个事务相关的数据,数据库启动后,会初始化max_prepared_transactions个GlobalTransactionData结构,并以链表的形式存储到TwoPhaseStateData结构体的freeGxacts中
该结构体主要记录了三部分内容:

  • 两阶段提交相关的信息,比如指向的下一个GlobalTransaction地址,进程信息,事务开始时间等
  • XLOG信息,事务ID信息及用户信息
  • 标志位,比如是否记录到PGPROC中,是否落盘,数据是否是从WAL日志中获得等
typedef struct GlobalTransactionData
{
	GlobalTransaction next;		/* TwophaseStateData的freeGxacts链表 */
	int			pgprocno;		/* PGPROC,在initPRoc时会初始化 */
	BackendId	dummyBackendId; /* dummy进程,用来代表一个backend */
	TimestampTz prepared_at;	/* prepared事务开始的时间 */

	XLogRecPtr	prepare_start_lsn;	/* prepared记录的XLOG日志的起始位置 */
	XLogRecPtr	prepare_end_lsn;	/* prepared记录的XLOG日志的结束位置 */
	TransactionId xid;			/* 全局事务ID */

	Oid			owner;			/* 启动这个事务的用户 */
	BackendId	locking_backend;	/* 当前事务事务中运行的事务ID */
	bool		valid;			/* 当前进程是在PGPROC中记录 */
	bool		ondisk;			/* Prepare状态数据是否落盘 */
	bool		inredo;			/* 数据内容是否是通过回放WAL日志获得 */
	char		gid[GIDSIZE];	/* prepared transaction指定的gid */
}			GlobalTransactionData;

TwoPhaseStateData

数据库在启动时初始化的一个全局变量,记录了两阶段提交事务需要用到的GlobalTransactionData结构的使用情况。比如空闲链表,已使用的个数,已使用的GlobalTransactionData的地址。

typedef struct TwoPhaseStateData
{
	/* Head of linked list of free GlobalTransactionData structs */
	GlobalTransaction freeGXacts;  //空闲链表,存储的是可用的GlobalTransactionData结构

	/* Number of valid prepXacts entries. */
	int			numPrepXacts; //当前的有效的2PC事务的个数

	/* There are max_prepared_xacts items in this array */
	GlobalTransaction prepXacts[FLEXIBLE_ARRAY_MEMBER]; //当时使用的2PC的GlobalTransactionData的地址
} TwoPhaseStateData;

共享内存中存储格式

数据库启动时会初始化一个全局共享变量TwoPhaseState和max_prepared_transactions个GlobalTransactionData结构体,所有的GlobalTransactionData是以链表的形式保存,TwoPhaseState的成员freeGXacts指向链表的头部。当启动一个两阶段提交事务时,就会从freeGxacts的链表中取下一个空闲的GlobalTransactionData用来保存当前的2PC的事务信息,同时TwoPhaseState中的numprepXacts计数加1,并会将GlobalTransactionData地址保存到prepXacts数组中。当两阶段提交事务提交后,会重置其使用的GlobalTransactionData结构体,并将其重新插入到freeGxacts链表中,同时numpreXacts计数减1,并清除prepXacts数组中对应的元素。
在这里插入图片描述

2PC的代码执行流程

参照上面的使用示例,接下来分析一下执行PREPARE命令,COMMIT命令和ROLLBACK命令后代码执行流程。

prepare阶段

执行PrePare transaction命令后,实际执行的就是执行PrepareTransaction函数

  • PrepareTransaction
    执行PrepareTransaction函数,进行Prepare相关操作。该函数执行完之后,相关数据就已经写入WAL日志且刷入磁盘中了,即使重启数据库数据也不会丢失,只不过事务状态是未提交的,对其他事务来时还是不可见的。需要注意的是,在Prepare后,这个2PC事务添加的锁也不会释放,即使重启数据库,重启后锁依然存在。
  1. 事务启动前的准备工作,跟正常的事务启动差不多
  	TransactionState s = CurrentTransactionState; //获取当前的事务
	TransactionId xid = GetCurrentTransactionId(); //获取当前的事务ID,如果不存在就分配一个,也就是两阶段提交必然会分配事务ID
    ...
	s->state = TRANS_PREPARE; //事务状态更新为prepare
	prepared_at = GetCurrentTimestamp(); //获取当前的时间作为两阶段提交创建时间戳
	BufmgrCommit();//啥也没干
  1. 从共享内存链表TwoPhaseState中获取一个空闲的GlobalTransactionData并初始化得到一个全局事务状态变量gxact
   	gxact = MarkAsPreparing(xid, prepareGID, prepared_at,
							GetUserId(), MyDatabaseId); //获取到一个全局的GXact
  1. 启动两阶段提交事务,并进行相应处理,如将当前事务的record写入XLOG中,并由checkpointer控制是否刷盘(手动创建checkpoint会写pg_twophase目录)
   StartPrepare(gxact);//初始化2PC文件头数据
	AtPrepare_Notify();//已经执行LISTEN、UNLISTEN、NOTIFY的话不允许2PC
	AtPrepare_Locks();//记录当前所有的持有的锁信息
	AtPrepare_PredicateLocks();//记录所有的predicate lock信息
	AtPrepare_PgStat();//记录2PC相关的统计信息
	AtPrepare_MultiXact();//记录MultiXact信息
	AtPrepare_RelationMap();//2PC 不允许任何relation Map修改
	EndPrepare(gxact);//2PC真正执行的东西

  1. 2PC 事务执行结束后的清理,
    标记当前的gxact为prepared,并标记事务状态为in_progress,添加到ProcGlobal共享内存中,该事务所在的proc被添加到全局的ProcGlobal中,该事务会被认为是in progress的,也就是说这个事务的修改并不会被其他事务看到。
	PostPrepare_Locks(xid);//将我们持有的锁保存到PGPROC中
	ProcArrayClearTransaction(MyProc);//清理事务
    ...
	PostPrepare_Twophase();//彻底与进程解耦
	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();
  • MarkAsPreparing
    • 检查是否开启2PC,检查gid是否有冲突
    • 检查是否还有空闲2PC空间
    • 从freeGxacts链表上取下一个空闲的gxact并进行初始化
 	/* fail immediately if feature is disabled */
	//只有GUC参数max_prepared_transactions参数不为0才能使用两阶段提交
	if (max_prepared_xacts == 0)
		ereport(ERROR,
				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
				 errmsg("prepared transactions are disabled"),
				 errhint("Set max_prepared_transactions to a nonzero value.")));



	LWLockAcquire(TwoPhaseStateLock, LW_EXCLUSIVE);//获取LWlock,因为要访问全局变量TwoPhaseState

	//检查是否有GUD冲突
	for (i = 0; i < TwoPhaseState->numPrepXacts; i++)
	{
		gxact = TwoPhaseState->prepXacts[i];
		if (strcmp(gxact->gid, gid) == 0) //判断名字是否有冲突
		{
			ereport(ERROR,
					(errcode(ERRCODE_DUPLICATE_OBJECT),
					 errmsg("transaction identifier \"%s\" is already in use",
							gid)));
		}
	}

	/* 从全局的空闲链表中获取一个空闲的GXACT */
	if (TwoPhaseState->freeGXacts == NULL)
		ereport(ERROR,
				(errcode(ERRCODE_OUT_OF_MEMORY),
				 errmsg("maximum number of prepared transactions reached"),
				 errhint("Increase max_prepared_transactions (currently %d).",
						 max_prepared_xacts)));
	gxact = TwoPhaseState->freeGXacts;//从freelist上取一个GXact
	TwoPhaseState->freeGXacts = gxact->next; //全局的指向下一个

	MarkAsPreparingGuts(gxact, xid, gid, prepared_at, owner, databaseid);//初始化gxact以及对应的proc

	gxact->ondisk = false; //是否落盘,初始化为false
	TwoPhaseState->prepXacts[TwoPhaseState->numPrepXacts++] = gxact;//将选择的GXact地址保存到数据中,方便遍历

	LWLockRelease(TwoPhaseStateLock);//释放锁

	return gxact;
  • EndPrepare
    该函数将完成两阶段提交的所有准备并写入到WAL日志。
  • 注册数据到record中
  • 确保WAL的registerbuffer有足够的空间
  • 填充数据到record中
  • 插入到WAL buffer中
  • 将WAL buffer中数据刷入磁盘
  • 标记该两阶段提交事务为有效,将当前进程信息加入到ProcGlobal中
	RegisterTwoPhaseRecord(TWOPHASE_RM_END_ID, 0,
						   NULL, 0); //注册到record中,用于写入到2PL文件

	XLogEnsureRecordSpace(0, records.num_chunks);//保证register_buffers有足够的空间
	for (record = records.head; record != NULL; record = record->next)
		XLogRegisterData(record->data, record->len);//填充数据
	gxact->prepare_end_lsn = XLogInsert(RM_XACT_ID, XLOG_XACT_PREPARE);//插入XLOG数据
	XLogFlush(gxact->prepare_end_lsn);//WAL日志刷盘
	MarkAsPrepared(gxact, false);//标记为可用,并将进程加到ProcGlobal中

commit/rollback阶段

执行commit prepared ‘p1’,提交prepare事务,或执行rollback prepared 'p1’都是执行FinishPreparedTransaction函数实现

  1. 通过传入的gid从共享内存中的TwoPhaseState中找到gxact
  2. 从XLOG或pg_twophase中读取gxact对应的数据到buffer中
  3. 通过RecordTransactionCommitPrepared函数写commit标记到XLOG中,并标记当前事务状态为committed。
  4. 通过ProcArrayRemove从共享内存变量ProcGlobal中移除当前proc,这样对其他事务就可见了
  5. 发送IM消息到其他的cache,标记当前tuple已经被更新,需要重新读取填充。
  6. 提交成功,清理锁信息以及从TwoPhaseState中移除gxact
	if (gxact->ondisk) //如果已经落盘到2PL文件中,则从pg_twophase中读取数据
		buf = ReadTwoPhaseFile(xid, false);
	else //从XLOG中读取数据
		XlogReadTwoPhaseData(gxact->prepare_start_lsn, &buf, NULL);

	latestXid = TransactionIdLatest(xid, hdr->nsubxacts, children); //获取最新的事务ID
	if (isCommit)
		RecordTransactionCommitPrepared(xid,hdr->nsubxacts, children,
			hdr->ncommitrels, commitrels,hdr->ninvalmsgs, invalmsgs,
			hdr->initfileinval, gid); //记录事务提交信息,会写XLOG
	else
		RecordTransactionAbortPrepared(xid,
			hdr->nsubxacts, children,hdr->nabortrels, abortrels,
			gid);//记录事务abort信息

	ProcArrayRemove(proc, latestXid); //将proc信息从ProcArray中删除
	DropRelationFiles(delrels, ndelrels, false); //删除标记为droped的文件
	SendSharedInvalidMessages(invalmsgs, hdr->ninvalmsgs); //IM消息相关
	PredicateLockTwoPhaseFinish(xid, isCommit); //释放2PC的锁
	RemoveGXact(gxact); //从TwoPhase全局变量中删除
	if (gxact->ondisk)
		RemoveTwoPhaseFile(xid, true); //删除pg_twophase下的对应文件

2PC的checkpoint

checkpoint定时进行checkpoint定时将buffer中的内容刷入到磁盘,其中也会将2两阶段提交的WAL日志信息刷入到磁盘中。这里只会将WAL写入pg_twophase目录下。
正常情况下,两阶段提交事务的数据会在Prepare 命令执行完调用的Endprepare函数中奖事务相关数据写入WAL日志中并刷入磁盘。但是如果在Prepare transaction执行过程中,在执行Endprepare前,checkpointer就触发了,那么就会触发刷WAL日志数据数据到磁盘的。
如果PrePare transaction执行时没有跨过检查点,那么只需要将两阶段数据保存到WAL日志中即可,但是如果跨过了检查点,由于checkpointer会触发WAL日志的刷盘和清理,所以有可能删除2PC产生的WAL日志。因此需要将检查点之前的相关WAL日志保存到数据目录的pg_twophase目录下,文件名以事务ID的8位16进制值命名。

  • CheckPointTwoPhase
    • 申请2PC的锁
    • 遍历每一个2PC事务
    • 如果2PC中的end_lsn小于redo点,则将这部分WAL日志保存到pg_twophase目录下下
    • 释放2PC的锁
    • pg_twophase目录下文件sync

	 */
	LWLockAcquire(TwoPhaseStateLock, LW_SHARED);
	//将所有的2PL相关WAL数据刷入磁盘
	for (i = 0; i < TwoPhaseState->numPrepXacts; i++)
	{
		GlobalTransaction gxact = TwoPhaseState->prepXacts[i];

		if ((gxact->valid || gxact->inredo) &&
			!gxact->ondisk &&
			gxact->prepare_end_lsn <= redo_horizon) //只会将LSN小于checkpoint的redo point时才会写入2PL文件,否则不用写,会刷入WAL日志中
		{
			char	   *buf;
			int			len;

			XlogReadTwoPhaseData(gxact->prepare_start_lsn, &buf, &len); //从XLOG中读取2PL数据
			RecreateTwoPhaseFile(gxact->xid, buf, len); //创建2PL文件,文件名以事务ID的16进制值命名
			gxact->ondisk = true;
			gxact->prepare_start_lsn = InvalidXLogRecPtr;
			gxact->prepare_end_lsn = InvalidXLogRecPtr;
			pfree(buf); //从XLOG读数据的时候分配的空间,这里要释放掉
			serialized_xacts++;
		}
	}
	LWLockRelease(TwoPhaseStateLock);

	fsync_fname(TWOPHASE_DIR, true); //刷入磁盘

【参考】

  1. 《PostgreSQL数据库内核分析》
  2. 《Postgresql技术内幕-事务处理深度探索》
  3. pg14源码
  • 26
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值