- 源码文件:src/backend/access/transam/subtrans.c
- 源码版本:PG 13.3
1. 子事务的使用
PG 提供了一组 savepoint 相关的命令用于子事务操作,子事务相关例子如下:
定义一个保存点:
savepoint savepoint_name;
销毁一个保存点:
release [savepoint] savepoint_name;
回滚到一个保存点:
rollback to [savepoint] savepoint_name;
2. 子事务对事务号 xid 的消耗
PG 对于只包含 select 语句的事务并不会分配 xid,但是在子事务场景下,则可能为一个只包含 select 语句的子事务分配 xid。举个例子,如下:
begin;
select * from t;
savepoint s1;
select * from t;
savepoint s2;
select * from t;
savepoint s3;
insert into t values(1);
commit;
当执行到 insert 语句时,将会给 s1,s2,s3 三个子事务分配 xid。在 PG 数据库中,事务号 xid 是一种宝贵的资源,但如果使用了子事务,将可能导致事务号 xid 消耗加快。
3. 子事务相关的共享内存
子事务信息会占用共享内存,因此在 PG 主进程启动时会对子事务相关的共享内存进行初始化,如下:
- SUBTRANSShmemSize() 函数计算子事务共享内存大小,主要包含 默认的 32 个 page 页面以及相关的控制结构,该函数在 postmaster 主进程启动时调用。
- SUBTRANSShmemInit() 函数进行子事务共享内存初始化,将子事务共享内存通过 LRU Buffer 进行管理,LRU Buffer 可以落盘持久化到目录 pg_subtrans 里面。该函数在 postmaster 主进程启动时调用。
4. 子事务数据持久化
子事务 LRU Buffer 会持久化到 pg_subtrans 目录,以文件形式存储,文件名通常为 0000,0001等,单个文件 256KB,以 PAGE 为基本单位,默认一个 PAGE 8KB,这里类似于 CLOG 的持久化存储。
pg_subtrans 目录中的文件实际上存储的是 xid 与其父事务 xid 之间的映射关系,xid 作为索引,父事务 xid 为值,举个例子:
假设 xid=3,那么 0000 文件的偏移 12 个字节的位置存储其父事务 xid,占用 4 字节。
一个 8KB 的 PAGE 页面可以存储 8192/4=2048 个父事务 xid,源码中的宏定义如下:
#define SUBTRANS_XACTS_PER_PAGE (BLCKSZ / sizeof(TransactionId))
一个事务 xid 对应的 PAGE 号,源码中使用宏定义如下:
#define TransactionIdToPage(xid) ((xid) / (TransactionId) SUBTRANS_XACTS_PER_PAGE)
pg_subtrans 里面的文件在事务提交后有些是可以删掉的,见函数 TruncateSUBTRANS(),该函数在执行 checkpoint 时调用,检查如果有不再使用的子事务持久化文件,即进行删除。
5. 子事务缓存
backend 进程对应的数据结构 PGPROC 会对子事务进行缓存,默认缓存大小为 64,子事务数量小于 64 时,会将子事务 xid 放入 MyProc->subxids.xids 数组中,该数组位于共享内存。子事务缓存能够提升子事务处理效率,当子事务数量大于 64 时,子事务性能下降明显。
子事务缓存大小的宏定义如下:
#define PGPROC_MAX_CACHED_SUBXIDS 64
在 GetNewTransactionId()函数中分配事务号时,如果是子事务并且子事务数量小于 64,则将子事务 xid 放入 MyProc->subxids.xids 数组。如果子事务数量超过 64,则标记 MyPgXact->overflowed = true。
在获取快照时,会遍历所有 backend 进程的 PGXACT 结构,PGXACT->overflowed 会影响 snapshot->suboverflowed,在 MVCC 可见性判断时,如果 snapshot->suboverflowed=true,则会去子事务共享内存 LRU Buffer 获取子事务信息,由于 LRU buffer 在子事务数量巨大的情况下会有 IO 消耗(页换入换出),因此会出现性能问题。
6. 子事务模块相关的函数
- SubTransSetParent(),设置事务 xid 的父事务,根据 xid 找到其对应的 PAGE 号和偏移量 ptr,将 ptr 对应的内存值设置为父事务号 parent,同时将该 PAGE 设置为 dirty。
- SubTransGetParent(),获取事务 xid 的父事务号。根据 xid 找到其对应的 PAGE 号和偏移量 ptr,将 ptr 对应的内存存储的值返回。
- SubTransGetTopmostTransaction(),获取子事务的顶层事务号。包含多个子事务时,事务号之间有关联关系,比如 xid->subxid1->subxid2->subxid3,获取顶层事务号即返回最上层的 xid。