一、 事务状态与事务栈
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章