--POSTGRESQL 事务控制(二) 事务开启 (写着费力,看着费劲系列)

2021-08-30 00:00:00 事务 记录 状态 页面 相关

今天接着上回书,事务如果在处理中没有子事务,则实现和控制是十分容易的,但如果有子事务的情况下, 子事务通过transactionState结构体来实现,(上次已经提到了),每一个transactionState都指向父事务的结构体的指针.

在继续往下说之前,我们的提到clog, 这是理解后面要提到的一些事情的前提, 在POSTGRESQL 中clog 是记录每一个事务相关的xid, 以及事务的提交状态, 状态包含了 执行中, 已提交, 中断,子事务已提交. 那么clog 承担了对整体事务状态的记录和查询的任务. 

下面是一段clog 代码和 xidstatus的状态结构

从上面两个图中可以得到的信息


CLOG_BITS_PER_XACT  2  一个事务使用2BIT

CLOG_XACTS_PER_BYTE 4  一个字节记录4个事务

CLOG_XACTS_PER_PAGE (BLCKSZ*CLOG_XACTS_PER_BYTE)  一个页面记录(8196*4=32784) 个事务

((xid)   (TransactionId)  CLOG_XACTS_PER_PAGE  

如果要寻找xid的页面位置,通过事务号除以一个页面的记录事务数,就得到了具体这个事务所在的页面数.

((xid) % (TransactionId) CLOG_XACTS_PER_PAGE)

下面的计算可以获得相关信息存储在页面中的第多少个字节

(TransactionIdToPgIndex(xid) / CLOG_XACTS_PER_BYTE)

后的公式是计算事务信息存在在字节内的偏移量

 ((xid) % (TransactionId) CLOG_XACTS_PER_BYTE)


如果要知道xid事务的状态,则直接通过取余的方式直接获得事务的状态页面内的偏移位置

从上面的东西中可以获得

1  事务的状态是通过2个BITS来存储的, 每个字节可以存储4个事务的状态

2  确认当前事务的状态通过事务ID ,事务XID 是32BIT

3  通过简单的除法和商运算,可以获得事务的状态信息


我们验证一下, 我们开通4个 SESSION ,然后将SESSION中的事务ID打印出来.

分别事务id 为 623  - 626



下面四个计算的公式 

1 得到事务存储的页面 0号页面  

2 得到事务存储在页面总的偏移量

3 数据存储在页面内的多少字节

4 字节内的偏移量


通过事务ID 就可以直接获得事务的状态在CLOG中的存储位置,并且提取相关的信息.



那么我们来计算一下,一个8K的页面可以存储多少事务,以及事务的状态

8K页面* 8b/2b (事务的4个状态) = 32KB = 16384 个事务的状态


假设我们有5MB的内存空间,可以存储  10485760 个事务的状态,这里需要注意我们的事务动态的信息都是存在在内存中,所以用更小的内存来控制更多的事务是有利于整体运行效率的.


现在我们回归主题,子事务的与父事务的关系的存储, 在我们的pg的数据库目录里面有一个目录是pg_subtrans 目录,其中通过pg_subtrans 这个目录中的文件来存储子事务与父事务之间的关系. 子事务保存自己上一次的事务,相关事务的追述是通过自下而上的方式. 


下面是这段和pg_subtrans有关的代码, 这段代码在 xact.c 中, 这段代码的主要的作用是在给子事务上的父事务ID 进行记录, 好进行自下而上的寻根


static void
AssignTransactionId(TransactionState s)
{

bool isSubXact = (s->parent != NULL);

//是否是子事务,如果有父事务,则父事务不能为空

ResourceOwner currentOwner;
bool log_unknown_top = false;
// 确认当前事务号有效并且事务是在inprogress 的状态
Assert(!TransactionIdIsValid(s->transactionId));
Assert(s->state == TRANS_INPROGRESS);

//如果当前事务在并行则不运行分配事务号

if (IsInParallelMode() || IsParallelWorker())
elog(ERROR, "cannot assign XIDs during a parallel operation");

在 subtrans.c中包含关于在PG 重启后处理pg_subtrans中的数据的方法是直接清理掉这些数据,一下这段代码进行相关工作的代码.


主要的原由是,子事务是包含在事务内的,在事务本身失效后,这些子事务也没有必要进行记录,所以在pg_wal中也不会有相关子事务的日志记录.

上期说到事务的ID 只有在执行 INSERT ,UPDATE ,DELETE的时候才进行事务号的分配,那么不分配事务号的情况下,事务到底有没有事务号, 实际上是有的在事务开始时是分配一个虚拟的事务ID


在开始一个事务的时候,会先开始分配内存和事务所需要的,buffer, 锁等信息,每个事务都需要有相关的resourceOwner信息, 虚拟的事务主要通过两个方式来生成自己的虚拟事务ID ,  benchend process  ID + 本地的计数器, 这样就可以产生一个自己的临时的虚拟的事务ID 

在获取了ID后,我们直接就开始进行相关事务的开启,参加下面的语句

TRACE_POSTGRESQL_TRANSACTION_START(vxid.localTransactionId);




此时在设置完相关的变量的初始值后, 如事务就可以开始启动了.


总结,在一个事务开启时


1  事务初始并没有实际的事务ID ,而是本地通过backend 和计数器临时分配的虚拟事务ID , 只有在事务中出现IUD 的操作才会分配实际的事务ID 

2  服务器在重启或者CRASH 后会清理与子事务与父事务相关的关联的信息

3  在事务中,存在越多的子事务,则整体的系统的性能会越来越低,所以不建议使用大量的save point 产生大量的子事务. 并且子事务与父事务之间的关系是自下而上的搜索, 只有通过子事务才能查找到自己的父事务, 并在设计的时候, 通过简单的事务ID与页面数的余数,商可以直接快速定位事务的状态.



相关文章