postgresql snapshot快照源码解读
概述
本文主要介绍数据库事务快照,分别从源码实现角度和从SQL使用角度来剖析,快照的原理,作用,用途,以及在实现过程中存在的一些差异。
简介
数据库快照,可能有很多种理解,如在备份时,有快照备份,也是一种快照;
本文要介绍的快照,是数据库运行过程中,事务并发时,通过快照达到事务隔离,文中叫它事务快照,在postgresql中叫做snapshot。
通过事务快照,可以获得当前所有事务的一组状态,使得,我们在数据库的不同隔离级别下,可以事务内看到的数据可见性不一样,达到数据的隔离性。
快照源码分析
快照结构定义
typedef struct SnapshotData
{
SnapshotType snapshot_type; /* type of snapshot */
TransactionId xmin; /* all XID < xmin are visible to me */
TransactionId xmax; /* all XID >= xmax are invisible to me */
TransactionId *xip;
uint32 xcnt; /* # of xact ids in xip[] */
TransactionId *subxip;
int32 subxcnt; /* # of xact ids in subxip[] */
bool suboverflowed; /* has the subxip array overflowed? */
bool takenDuringRecovery; /* recovery-shaped snapshot? */
bool copied; /* false if it's a static snapshot */
CommandId curcid; /* in my xact, CID < curcid are visible */
uint32 speculativeToken;
struct GlobalVisState *vistest;
uint32 active_count; /* refcount on ActiveSnapshot stack */
uint32 regd_count; /* refcount on RegisteredSnapshots */
pairingheap_node ph_node; /* link in the RegisteredSnapshots heap */
TimestampTz whenTaken; /* timestamp when snapshot was taken */
XLogRecPtr lsn; /* position in the WAL stream when taken */
uint64 snapXactCompletionCount;
} SnapshotData;
快照内容分析
xmin
最小正在运行的事务ID, 初值为ShmemVariableCache->latestCompletedXid + 1;
然后在所有backend中查找对应的xmin(也即每个backend对应快照的xmin),找最小值
xmax
最大已经完成的事务ID,是ShmemVariableCache->latestCompletedXid; + 1;
运行事务ID列表
内容为 xip 和 xcnt
子事务列表
相关内容为 subxip,subxcnt,suboverflowed
快照版本
对应内容为 curXactCompletionCount
来自 ShmemVariableCache->xactCompletionCount
用于是否有旧快照可以快速获取,进行版本比较;
如果版本发生变化,则需要重新获取,否则就直接使用
快照生成流程
那么我们对照快照的内容,看看各成员是如何生成;
信息介绍
在数据库运行过程中,每个backend启动时都会创建一个PGPROC的结构,在共享内存的变量ProcGlobal中有一个数组存储;
但是backend对应的 ProcGlobal数组中的下标,存储在另一个共享内存变量 procArray 中;
在每个PGPROC的结构中保存了当前backend的快照,正在运行的事务ID;
初始化
- xmin,xmax初始化为 初值为ShmemVariableCache->latestCompletedXid + 1
对于xmax来讲,已经完成了,latestCompletedXid 就是最新已经完成的事务ID
- xip, subxip 分配内存;
扫描每个backend
那么知道上面backend事务信息的存储后,我们要想生成一个新的事务快照,就要去扫描每个backend信息;
- 比较xmin, 更新为最小的xmin;
- 遍历每个backend的xid, 与xmax比较,比xmax小的,记录 xid到快照中;
记录每个backend的 subxid到快照中; 如果子事务数据空间不足,则设置 suboverflowed 标志;
子事务总是比父事务ID更新,所以这里丢失也没有关系。
遍历中需要跳过的backend
- 逻辑复制 它的xmin单独管理
- lazy vacuum
- 还有当前backend也不会统计在内
latestCompletedXid更新时机及规则
ShmemVariableCache->latestCompletedXid;
- 变化规则
- 初始化时,赋值为ShmemVariableCache->nextXid;
- 两阶段事务完成时,在MaintainLatestCompletedXid与当前完成事务比较,取最大值 ;
- 子事务abort时,将父子事务中最新xid与latestCompletedXid进行比较,取最大值;
- 事务提交或abort时
oldestXmin更新时机及规则
ShmemVariableCache->oldestXid
数据库集群内的 最小的datfrozenxid
- 更新规则
在vacuum时,会扫描pg_database表中,每个database对应的frozenxid,取最小的作为oldestXid
xactCompletionCount更新时机及规则
ShmemVariableCache->xactCompletionCount
- 更新规则
- 初始化 CreateSharedProcArray 中初始化为 1
- 在backend数量变化时 递增1 ,ProcArrayRemove中
有两种场景会调用:
- 当两阶段事务结束时,它对应额外backend proc需要回收;
- backend退出时,proc需要回收
原理
根据变化规则,它代表了procarray版本,如果有变化就会递增
它是一个64位整型,所以反转也没有问题,当然几乎不可能反转
快照调用
快照生成的函数接口
GetTransactionSnapshot
-> GetSnapshotData
>
常用的调用处
- 事务开始和结束时
```c
/ 事务开始时,生成事务快照 /
StartTransactionCommand();
PushActiveSnapshot(GetTransactionSnapshot());
/* 事务开始时,清理事务快照 */
PopActiveSnapshot();
CommitTransactionCommand();
```
- 在存储过程开始时
在存储过程或者函数中也是一个完整的事务,所以事务开始,结束会生成快照
快照的优化
从生成流程看,每次生成事务快照要扫描所有的backend信息,在扫描过程是需要加ProcArrayLock共享锁的。
ProcArrayLock共享锁会阻止backend信息的变化,也就是事务的提交,代价还是相当大的。
所以在快照中增加了快照版本,当版本没有变化时,直接拿上次的快照即可。
快照的作用
生成snapshot之后,如何使用呢?
事务状态段
在snapshot中将事务状态按事务号区间分成了三部分,
由xmin, xmax组成的半闭闭开区间 [xmin, xmax)事务可见性判断
对于xid < xmin 的部分,肯定是可见的,也就是事务已经完结;因为xmin就是最小运行的xid;
对于 xid <= xmin , 同时 xid < xmax,在此区段的事务,就需要进行检查;
需要检查那些事务号呢? 只检查快照中记录的正在运行的事务号列表xip数组中的事务号,看它们是否完成。
对于,读已经提交,事务完成就是可见了。对于 xid >= xmax的,都是不可见的。因为xmax是已经完成事务的最大事务号+1,所以对于当前快照来说,超过xmax的值都是未来事务,认为它们都在运行中