引言
在上一篇博客MVCC(1)–概述中,对MVCC机制的特点进行了回顾,并简要叙述了实现MVCC需要关注的重点。本篇博客就是对实现重点之一的快照进行更进一步的学习分析,以期更好地理解MVCC。
一、快照基本信息
如下源码展示,文件路径:src\include\utils\snapmgr.h 快照基本类型
extern THR_LOCAL PGDLLIMPORT SnapshotData SnapshotNowData;
extern THR_LOCAL PGDLLIMPORT SnapshotData SnapshotSelfData;
extern THR_LOCAL PGDLLIMPORT SnapshotData SnapshotAnyData;
extern THR_LOCAL PGDLLIMPORT SnapshotData SnapshotToastData;
#ifdef ENABLE_MULTIPLE_NODES
extern THR_LOCAL PGDLLIMPORT SnapshotData SnapshotNowNoSyncData;
#endif
由上面的声明可知,所有快照类型都是由一种结构体定义而来的。 在文件src\include\utils\snapshot.h中我们可以找到快照数据结构的定义如下:
typedef struct SnapshotData {
SnapshotSatisfiesMethod satisfies; //可见性判断函数
TransactionId xmin; //当前活跃事务的最小值
TransactionId xmax; //最新提交事务,大于等于该值的事务ID不可见
SubTransactionId subxid;
TransactionId* xip;//当前活跃事务链表
TransactionId* subxip;//活跃事务个数
uint32 xcnt; //xip最大个数
GTM_Timeline timeline; //时间线
#ifdef PGXC /* PGXC_COORD */
uint32 max_xcnt; /* Max # of xact in xip[] */
#endif
/* note: all ids in xip[] satisfy xmin <= xip[i] < xmax */
int32 subxcnt; //子事务活跃链条
int32 maxsubxcnt; //最大子事务活跃链条
bool suboverflowed; //子事务超共享内存判断
CommitSeqNo snapshotcsn; //快照的CSN号码
int prepared_array_capacity;//准备数组容量
int prepared_count;//准备数量
TransactionId* prepared_array; //准备数组
bool takenDuringRecovery; //恢复过程中创建的快照判断
bool copied; //快照是静态级别,还是内存复制判断
CommandId curcid; //快照序列号
uint32 active_count; //活跃事务返回数目
uint32 regd_count; /* refcount on RegisteredSnapshotList */
void* user_data; //标记当前快照是否还有线程使用,以免释放
GTM_SnapshotType gtm_snapshot_type;
} SnapshotData;
二、快照创建一般流程
在数据库中,创建快照的过程通常涉及以下步骤:
1.开始事务:快照通常在事务开始时创建。这是因为快照需要捕获事务开始时数据库的状态,以便在事务执行期间提供一致的数据视图。
2.记录活动事务:快照需要记录所有在创建快照时正在进行的事务。这是因为这些事务可能会在快照创建后更改数据,而这些更改不应反映在快照中。
3.复制数据:根据数据库的实现和配置,可能需要复制一部分或全部数据以创建快照。有些数据库使用称为写时复制(Copy-on-Write)的技术,只有当数据被修改时才复制数据。
4.保存快照:一旦快照创建完毕,就可以保存下来供以后使用。保存的方式取决于数据库的实现,可能包括将快照写入磁盘,或者在内存中保留快照。
5.使用和维护快照:在事务执行期间,可以使用快照来查询数据,以获取一致的数据视图。如果在事务执行期间修改了数据,可能需要更新快照以反映这些更改。
6.结束事务和清理快照:当事务结束时,通常会清理与该事务相关的所有快照。清理过程可能包括释放内存、删除临时文件等。
以上是创建数据库快照的一般过程,具体实现可能会根据不同的数据库系统和配置有所不同。
接下来我们以一个线程使用快照为例: 位于src\gausskernel\process\threadpool\knl_thread.cpp文件首先对线程进行初始化。
void knl_thread_init(knl_thread_role role)
{
...
knl_t_snapshot_init(&t_thrd.snapshot_cxt);
...
}
该线程向快照初始化函数传递了一个实参,查找该实参的定义,位于文件src\include\knl\knl_thread.h 我们找到其定义如下,恰好为本文第一部分所讲的快照的四种基本类型
typedef struct knl_t_snapshot_context {
struct SnapshotData* SnapshotNowData;
struct SnapshotData* SnapshotSelfData;
struct SnapshotData* SnapshotAnyData;
struct SnapshotData* SnapshotToastData;
} knl_t_snapshot_context;
初始化快照
static void knl_t_snapshot_init(knl_t_snapshot_context* snapshot_cxt)
{
snapshot_cxt->SnapshotNowData = (SnapshotData*)palloc0(sizeof(SnapshotData));
snapshot_cxt->SnapshotNowData->satisfies = SNAPSHOT_NOW;
snapshot_cxt->SnapshotSelfData = (SnapshotData*)palloc0(sizeof(SnapshotData));
snapshot_cxt->SnapshotSelfData->satisfies = SNAPSHOT_SELF;
snapshot_cxt->SnapshotAnyData = (SnapshotData*)palloc0(sizeof(SnapshotData));
snapshot_cxt->SnapshotAnyData->satisfies = SNAPSHOT_ANY;
snapshot_cxt->SnapshotToastData = (SnapshotData*)palloc0(sizeof(SnapshotData));
snapshot_cxt->SnapshotToastData->satisfies = SNAPSHOT_TOAST;
}
三、活跃事务数组方法
活跃事务数组
在数据库进程中,维护一个全局的数组,其中的成员为正在执行的事务信息,包括事务的事务号,该数组即活跃事务数组。 数据结构如下,成员分别记录快照,等级,指向下一个活跃快照
typedef struct ActiveSnapshotElt {
Snapshot as_snap;//快照部分
int as_level;
struct ActiveSnapshotElt* as_next;
} ActiveSnapshotElt;
在每个事务开始的时候,复制一份该数组内容。
当事务执行过程中扫描到某个元组时,需要通过判断元组 xmin 和 xmax 这两个事务对于查询事务的可见性,来决定该元组是否对查询事务可见。
简单分析活跃事务数组的获取函数
源码以及注释如下
//Return the topmost snapshot in the Active stack.
Snapshot GetActiveSnapshot(void)
{
#ifdef PGXC
/*
* Check if topmost snapshot is null or not,
* if it is, a new one will be taken from GTM.
*/
if (!u_sess->utils_cxt.ActiveSnapshot && IS_PGXC_COORDINATOR && !IsConnFromCoord())
return NULL;
#endif
//Check if ActiveSnpshot is null or nut.
if (u_sess->utils_cxt.ActiveSnapshot == NULL) {
ereport(ERROR,
(errmodule(MOD_TRANS_SNAPSHOT),
errcode(ERRCODE_INVALID_STATUS),
errmsg("snapshot is not active")));
}
return u_sess->utils_cxt.ActiveSnapshot->as_snap;
}
四、时间戳方法
在openGauss内部,使用一个全局自增的长整数作为逻辑的时间戳,模拟数据库内部的时序,该逻辑时间戳被称为提交顺序号(Commit Sequence Number,简称CSN)。每当一个事务提交的时候,在提交序列号日志中(CommitSequenceNumberLog,CSN日志)会记录该事务号 XID(事务的全局唯一标识)对应的逻辑时间戳 CSN 值。CSN日志中记录的 XID值与 CSN 值的对应关系,即决定了所有事务的状态函数f(t)。 时间戳:通俗的讲, 时间戳是一份能够表示一份数据在一个特定时间点已经存在的完整的可验证的数据.
由此,某一个事务 T的快照内容,即是其他所有事务Tother 的事务状态函数fother(t) 在该事务开始时刻点tstart 的取值状态。根据fother的定义,可知,若tstart <= ,则该事务Tother在T的快照中为未提交状态,其对数据库的写操作对事务T不可见;若tstart > ,则该事务Tother在T的快照中为提交状态,其对数据库的写操作对事务T可见。
在一个事务的实际执行过程中,并不会在一开始就加载全部的CSN 日志,而是在扫描到某条记录以后,才会去 CSN 日志中查询该条记录头部 xmin和xmax这两个事务号对应的 CSN 值,并基于此进行可见性判断。(可见性判断在下一篇博客当中将会详细提到介绍MVCC可见性判断)
每当一个事务提交的时候,在 CSN 日志中会记录该事务号 XID 对应的逻辑时间戳 CSN 值。其记录规则如下:
#define COMMITSEQNO_INPROGRESS UINT64CONST(0x0) // 表示该事务还未提交或回滚
#define COMMITSEQNO_ABORTED UINT64CONST(0x1) // 表示该事务已经回滚
#define COMMITSEQNO_FROZEN UINT64CONST(0x2) // 表示该事务已提交,且对任何快照可见
#define COMMITSEQNO_FIRST_NORMAL UINT64CONST(0x3) // 事务正常的CSN号起始值
#define COMMITSEQNO_COMMIT_INPROGRESS (UINT64CONST(1) << 62) // 事务正在提交中
五、小结
本次对MVCC技术实现的快照部分做了较为详细的分析,更加细致的分析将会在后面展开,下一篇博客将要进行的是对MVCC可见性判断的分析。