postgresql源码学习(三)—— 事务ID分配

67 篇文章 55 订阅
35 篇文章 3 订阅

一、 事务状态与事务栈

1. 事务状态

注意区分pg中事务块和事务的概念

  • pg中事务块:DB理论中的事务
  • pg中事务:事务块中sql语句

因此这里说的事务状态是指,底层事务(事务块中sql语句)真正的状态

typedef enum TransState
{
    TRANS_DEFAULT,               /* idle */
    TRANS_START,             /* transaction starting */
    TRANS_INPROGRESS,            /* inside a valid transaction */
    TRANS_COMMIT,                /* commit in progress */
    TRANS_ABORT,             /* abort in progress */
    TRANS_PREPARE                /* prepare in progress */
} TransState;

2. 事务栈    

       由于子事务的引入,一个事务中可能会有多个层级的子事务。pg使用一个事务栈来保存每个层级子事务的状态,这个事务栈的结构体是 TransactionStateData (其实上一篇它就出现了)

typedef struct TransactionStateData
{
    FullTransactionId fullTransactionId;  /* 事务Id */
    SubTransactionId subTransactionId;    /* 子事务ID */
    char    *name;            /* savepoint名字 */
    int          savepointLevel; /* savepoint层级,因为可以有多层子事务 */
    TransState   state;           /* 事务状态 */
    TBlockState blockState;      /* 事务块状态 */
    int          nestingLevel;    /* 事务嵌套深度 */
    int          gucNestLevel;    /* GUC(Grand Unified Configuration,全局统一配置) 上下文嵌套深度,与子事务出入栈相关 */
    MemoryContext curTransactionContext;  /* 事务当前上下文 */
    ResourceOwner curTransactionOwner;    /* 当前事务占有的资源 */
    TransactionId *childXids; /* 提交的子事务链表 */
    int          nChildXids;      /* 提交的子事务个数 */
    int          maxChildXids;    /* 已分配的子事务 childXids[] 存储空间 */
    Oid          prevUser;        /* 记录前一个 CurrentUserId(用户名) 设置 */
    int          prevSecContext; /* previous SecurityRestrictionContext */
    bool    prevXactReadOnly;    /* 只读事务 */
    bool    startedInRecovery;   /* did we start in recovery? */
    bool    didLogXid;       /* has xid been included in WAL record? */
    int          parallelModeLevel;   /* Enter/ExitParallelMode counter */
    bool    chain;           /* 是否执行了 commit and chain */
    bool    assigned;        /* assigned to top-level XID */
    struct TransactionStateData *parent;  /* 指向上层事务的指针 */
} TransactionStateData;

typedef TransactionStateData *TransactionState;

二、 事务ID

  • 进入StartTransaction函数标志着一个事务的开始,会将事务状态由 TRANS_DEFAULT 改为 TRANS_START

  • 通常只读事务不会申请事务ID,只有涉及写操作时才会分配事务ID。事务会在执行第一个含有写操作的语句时分配事务ID
select txid_current_if_assigned();
  • 如果有子事务,要给顶层事务和子事务都分配事务ID,并且顶层事务ID一定小于子事务ID(层数越深id号越大)
  • 分配事务ID函数 AssignTransactionId() -> GetNewTransactionId()

1. 案例构造

我们构造一个包含子事务和dml操作的小案例

会话1

Create table t1(a int);

Savepoint p1;

Savepoint p2;

会话2

b GetNewTransactionId

b GetNewTransactionId

会话1

Insert into t1 values(1);

进入会话2进行调试

set print pretty on 格式化显示

可以看到这是最底层事务,savepoint name=p2

上层事务

顶层事务,name为空,parent也指向空

       可以看到,最先进入AssignTransactionId()函数的参数是最底层事务(这里我们按照savepoint名字叫它p2),下面逐步来看。

2. AssignTransactionId()函数

  • 这个函数最主要的部分是构造一个parents数组,按照子事务->父事务的顺序填充该数组,再按照父事务->子事务的顺序递归调用AssignTransactionId()函数
  • AssignTransactionId()函数会再继续调用GetNewTransactionId()函数分配事务ID
static void
AssignTransactionId(TransactionState s)
{
bool		isSubXact = (s->parent != NULL);
	…
	if (isSubXact && !FullTransactionIdIsValid(s->parent->fullTransactionId))
	{
		TransactionState p = s->parent;
		TransactionState *parents;
		size_t		parentOffset = 0;

		parents = palloc(sizeof(TransactionState) * s->nestingLevel);
		while (p != NULL && !FullTransactionIdIsValid(p->fullTransactionId))
		{
			parents[parentOffset++] = p;
			p = p->parent;
		}

		/*
		 * This is technically a recursive call, but the recursion will never
		 * be more than one layer deep.
		 */
		while (parentOffset != 0)
			AssignTransactionId(parents[--parentOffset]);

		pfree(parents);
	}
…
	s->fullTransactionId = GetNewTransactionId(isSubXact);

这里我们只截取一些重要的调试过程

	if (isSubXact && !FullTransactionIdIsValid(s->parent->fullTransactionId))
		TransactionState p = s->parent;
		TransactionState *parents;
		size_t		parentOffset = 0;

		parents = palloc(sizeof(TransactionState) * s->nestingLevel);

       如果是子事务(函数最开始部分对isSubXact的定义是 isSubXact = (s->parent != NULL);),并且其父事务未分配事务id:做一些数据初始化,其中比较重要的是将p指向s的父事务,并根据s->nestingLevel(子事务深度)构造parents数组。

继续走到第一个while循环

		while (p != NULL && !FullTransactionIdIsValid(p->fullTransactionId))
		{
			parents[parentOffset++] = p;
			p = p->parent;
		}

       如果没到顶层事务,并且当前事务未分配事务id:按照子事务->父事务的顺序填充该数组,填充结果如下:

继续走到第二个while循环

		while (parentOffset != 0)
			AssignTransactionId(parents[--parentOffset]);

		pfree(parents);

      按照父事务->子事务的顺序递归调用AssignTransactionId()函数。通过这种方式,保证了父事务id一定先于子事务id分配(父事务id一定比子事务id小)。各层都递归执行完后,通过pfree释放parents数组占用资源

       开始递归执行,从顶层事务开始

注意这次它不符合    if (isSubXact && !FullTransactionIdIsValid(s->parent->fullTransactionId))

它是顶层的父事务,因此跳过了while循环,直接到了s->fullTransactionId = GetNewTransactionId(isSubXact); 分配实际的事务id

3. GetNewTransactionId()函数

       有时按函数名搜索源文件会遇到比较尴尬的情况——出来太多,不知道在哪里找

一个办法是通过gdb打断点

   

首先可以看到Xid来源

full_xid = ShmemVariableCache->nextXid;

xid = XidFromFullTransactionId(full_xid);

查看ShmemVariableCache,可以看到这是个VariableCache类型的指针

VariableCache的定义在VariableCacheData结构体(以后我们会详细研究它),其中包括事务id的计数器,每次获取事务id后会对计数器+1。

参考 PostgreSQL 源码解读(117)- MVCC#2(获取快照#2)_cuichao1900的博客-CSDN博客

(pg 14源码中没搜到)

继续往下,这个函数主要的部分在

	if (!isSubXact) //如果非子事务,需要记录到当前会话(PGROC)的事务结构体中
	{
         …
		/* LWLockRelease acts as barrier */
         // pg 14将事务id保存在PGROC,和ProcGlobal的镜像数组中
		MyProc->xid = xid;
		ProcGlobal->xids[MyProc->pgxactoff] = xid; //
	}
	else //如果是子事务,需要记录到ProcGlobal的子事务id数组中
	{
		XidCacheStatus *substat = &ProcGlobal->subxidStates[MyProc->pgxactoff];
		int			nxids = MyProc->subxidStatus.count;

		Assert(substat->count == MyProc->subxidStatus.count);
		Assert(substat->overflowed == MyProc->subxidStatus.overflowed);

		if (nxids < PGPROC_MAX_CACHED_SUBXIDS) //子事务id数组最大长度
		{
			MyProc->subxids.xids[nxids] = xid;
			pg_write_barrier();
			MyProc->subxidStatus.count = substat->count = nxids + 1;
		}
		else //如果超出最大长度,要标记overflowed
			MyProc->subxidStatus.overflowed = substat->overflowed = true;
	}

       这步过后,顶层事务就获取到了自己的事务id,通过递归调用AssignTransactionId(),依次给各子事务分配事务id。分配后结果如下:

分配事务id后

三、 pg_subtrans日志

在GetNewTransactionId()后面是将事务ID的父子关系记入pg_subtrans目录

可以看到,如果是子事务,会为其记录父事务的事务id(单向记录),方便追溯

有一个疑惑:为什么TransactionState结构体中已经有记录了,这里还要记一下?是效率比较高?待研究。

参考:

https://blog.csdn.net/asmartkiller/article/details/121490543

《PostgreSQL技术内幕:事务处理深度探索》第1章

《PostgreSQL数据库内核分析》第7章

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hehuyi_In

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值