当前位置:   article > 正文

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

pgsql 事务id

一、 事务状态与事务栈

1. 事务状态

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

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

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

  1. typedef enum TransState
  2. {
  3.     TRANS_DEFAULT,               /* idle */
  4.     TRANS_START,             /* transaction starting */
  5.     TRANS_INPROGRESS,            /* inside a valid transaction */
  6.     TRANS_COMMIT,                /* commit in progress */
  7.     TRANS_ABORT,             /* abort in progress */
  8.     TRANS_PREPARE                /* prepare in progress */
  9. } TransState;

2. 事务栈    

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

  1. typedef struct TransactionStateData
  2. {
  3.     FullTransactionId fullTransactionId;  /* 事务Id */
  4.     SubTransactionId subTransactionId;    /* 子事务ID */
  5.     char    *name;            /* savepoint名字 */
  6.     int          savepointLevel; /* savepoint层级,因为可以有多层子事务 */
  7.     TransState   state;           /* 事务状态 */
  8.     TBlockState blockState;      /* 事务块状态 */
  9.     int          nestingLevel;    /* 事务嵌套深度 */
  10.     int          gucNestLevel;    /* GUC(Grand Unified Configuration,全局统一配置) 上下文嵌套深度,与子事务出入栈相关 */
  11.     MemoryContext curTransactionContext;  /* 事务当前上下文 */
  12.     ResourceOwner curTransactionOwner;    /* 当前事务占有的资源 */
  13.     TransactionId *childXids; /* 提交的子事务链表 */
  14.     int          nChildXids;      /* 提交的子事务个数 */
  15.     int          maxChildXids;    /* 已分配的子事务 childXids[] 存储空间 */
  16.     Oid          prevUser;        /* 记录前一个 CurrentUserId(用户名) 设置 */
  17.     int          prevSecContext; /* previous SecurityRestrictionContext */
  18.     bool    prevXactReadOnly;    /* 只读事务 */
  19.     bool    startedInRecovery;   /* did we start in recovery? */
  20.     bool    didLogXid;       /* has xid been included in WAL record? */
  21.     int          parallelModeLevel;   /* Enter/ExitParallelMode counter */
  22.     bool    chain;           /* 是否执行了 commit and chain */
  23.     bool    assigned;        /* assigned to top-level XID */
  24.     struct TransactionStateData *parent;  /* 指向上层事务的指针 */
  25. } TransactionStateData;
  26. 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
  1. static void
  2. AssignTransactionId(TransactionState s)
  3. {
  4. bool isSubXact = (s->parent != NULL);
  5. if (isSubXact && !FullTransactionIdIsValid(s->parent->fullTransactionId))
  6. {
  7. TransactionState p = s->parent;
  8. TransactionState *parents;
  9. size_t parentOffset = 0;
  10. parents = palloc(sizeof(TransactionState) * s->nestingLevel);
  11. while (p != NULL && !FullTransactionIdIsValid(p->fullTransactionId))
  12. {
  13. parents[parentOffset++] = p;
  14. p = p->parent;
  15. }
  16. /*
  17. * This is technically a recursive call, but the recursion will never
  18. * be more than one layer deep.
  19. */
  20. while (parentOffset != 0)
  21. AssignTransactionId(parents[--parentOffset]);
  22. pfree(parents);
  23. }
  24. s->fullTransactionId = GetNewTransactionId(isSubXact);

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

  1. if (isSubXact && !FullTransactionIdIsValid(s->parent->fullTransactionId))
  2. TransactionState p = s->parent;
  3. TransactionState *parents;
  4. size_t parentOffset = 0;
  5. parents = palloc(sizeof(TransactionState) * s->nestingLevel);

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

继续走到第一个while循环

  1. while (p != NULL && !FullTransactionIdIsValid(p->fullTransactionId))
  2. {
  3. parents[parentOffset++] = p;
  4. p = p->parent;
  5. }

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

继续走到第二个while循环

  1. while (parentOffset != 0)
  2. AssignTransactionId(parents[--parentOffset]);
  3. 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源码中没搜到)

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

  1. if (!isSubXact) //如果非子事务,需要记录到当前会话(PGROC)的事务结构体中
  2. {
  3. /* LWLockRelease acts as barrier */
  4. // pg 14将事务id保存在PGROC,和ProcGlobal的镜像数组中
  5. MyProc->xid = xid;
  6. ProcGlobal->xids[MyProc->pgxactoff] = xid; //
  7. }
  8. else //如果是子事务,需要记录到ProcGlobal的子事务id数组中
  9. {
  10. XidCacheStatus *substat = &ProcGlobal->subxidStates[MyProc->pgxactoff];
  11. int nxids = MyProc->subxidStatus.count;
  12. Assert(substat->count == MyProc->subxidStatus.count);
  13. Assert(substat->overflowed == MyProc->subxidStatus.overflowed);
  14. if (nxids < PGPROC_MAX_CACHED_SUBXIDS) //子事务id数组最大长度
  15. {
  16. MyProc->subxids.xids[nxids] = xid;
  17. pg_write_barrier();
  18. MyProc->subxidStatus.count = substat->count = nxids + 1;
  19. }
  20. else //如果超出最大长度,要标记overflowed
  21. MyProc->subxidStatus.overflowed = substat->overflowed = true;
  22. }

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

分配事务id后

三、 pg_subtrans日志

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

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

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

参考:

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

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

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/135175
推荐阅读
相关标签
  

闽ICP备14008679号