引言
在上一篇博客MVCC(2)—快照中,我们了解了MVCC实现的关键点——快照。本篇我们将着力于——分析判断数据有效性、可见性、可更新性的算法的实现
在进行可见性判断时,需要获取事务的状态,即元组中 t_xmin 和 t_xmax 的状态,需要clog来记录事务的状态,从而判断其可见性,内存里的访问远远快于磁盘读写,因此opengauss的很多机制都是运行时候在内存,然后定期持久化到磁盘。因此clog也有一块内存区域便于高效访问,即clog buffers,它也属于共享内存的这部分,平时更新clog是内存中进行的,然后满足条件后会调用pg_fsync刷数据到磁盘上的clog文件,或者等待checkpoint刷数据。数据库启动时会从磁盘的pg_xact目录下读取事务状态加载到clog buffers,并且运行过程中,vacuum会定时将不再使用的clog文件清理
从可见性判断的过程,我们能够大致了解到需要实现哪些功能,对于笔者来说,就是可以了解到需要分析哪些函数,按照什么顺序分析这些函数。
本篇博客将会先学习相关概念过后,再对实现分析判断数据有效性、可见性、可更新性的算法的实现的主要函数中的一个—XidVisibleInSnapshot做详尽分析
一、事务隔离级别
我们对事务隔离级别做一个简单说明:SQL标准定义了脏写,脏读,不可重复读,幻读 在open gaus数据库的实现过程中,对隔离级别做了一些扩展。
表格如下
隔离级别 | P0:脏写 | P1:脏读 | P4:更新丢失 | P2:不可重复读 | 3:幻读 | A5A:读偏斜 | A5B:写偏斜 |
---|---|---|---|---|---|---|---|
读未提交 | 不可能 | 可能 | 可能 | 可能 | 可能 | 可能 | 可能 |
读已提交 | 不可能 | 不可能 | 可能 | 可能 | 可能 | 可能 | 可能 |
可重复读 | 不可能 | 不可能 | 不可能 | 不可能 | 可能 不可能 | 不可能 | |
快照一致性读 | 不可能 | 不可能 | 不可能 | 不可能 | 偶尔 | 不可能 | 可能 |
可串行化 | 不可能 | 不可能 | 不可能 | 不可能 | 不可能 | 不可能 | 不可能 |
我们对新增的三种隔离级别:更新丢失,读偏斜,写偏斜做简单说明如下
更新丢失
一个事务在读取元组并更新该元组的过程中,有另一个事务修改了该元组的值,最终导致这次修改丢失。
读偏斜
-
假设数据x,y有隐式约束,x-y<=50。
-
事务一读取x=50
-
事务二写x=0,y=-50保证了约束成立并提交
-
事务一读y=-50,不满足约束
写偏斜
-
假设数据x,y有隐式约束,x-y<=50。
-
事务一读取x=50,写入y=0
-
事务二读y=10,写x=60保证了约束成立并提交
-
事务一提交,x=60,y=0,不满足约束
二、satisfies
-
satisfies 是 openGauss 提供的对于事务可见性判断的统一操作接口。*
src/gausskernel/storage/access/heap/heapam_visibility.c
-
HeapTupleSatisfiesMVCC:判断元组对某一快照版本是否有效
-
HeapTupleSatisfiesUpdate:判断元组是否可更新
-
HeapTupleSatisfiesDirty:判断当前元组是否已脏
-
HeapTupleSatisfiesSelf:判断 tuple 对自身信息是否有效
-
HeapTupleSatisfiesToast:用于 TOAST 表(参考文档)的判断
-
HeapTupleSatisfiesVacuum:用在 VACUUM,判断某个元组是否对任何正在运行的事务可见,如果是,则该元组不能被 VACUUM 删除
-
HeapTupleSatisfiesAny:所有元组都可见
-
HeapTupleSatisfiesHistoricMVCC:用于 CATALOG 表 上述几个函数的参数都是 (HeapTuple htup, Snapshot snapshot, Buffer buffer),其具体逻辑和判断条件,本文不展开具体讨论。 确定在给定快照的上下文中,是否可以看到特定的堆元组
-
可见性判断的主调函数源码如下:*
bool HeapTupleSatisfiesVisibility(HeapTuple tup, Snapshot snapshot, Buffer buffer)
{
switch (snapshot->satisfies) {
case SNAPSHOT_MVCC:
return HeapTupleSatisfiesMVCC(tup, snapshot, buffer);
break;
case SNAPSHOT_VERSION_MVCC:
return HeapTupleSatisfiesVersionMVCC(tup, snapshot, buffer);
break;
case SNAPSHOT_DELTA:
return HeapTupleSatisfiesDelta(tup, snapshot, buffer);
break;
case SNAPSHOT_LOST:
return HeapTupleSatisfiesLost(tup, snapshot, buffer);
break;
case SNAPSHOT_NOW:
return HeapTupleSatisfiesNow(tup, snapshot, buffer);
break;
#ifdef ENABLE_MULTIPLE_NODES
case SNAPSHOT_NOW_NO_SYNC:
return HeapTupleSatisfiesNow(tup, snapshot, buffer);
break;
#endif
case SNAPSHOT_SELF:
return HeapTupleSatisfiesSelf(tup, snapshot, buffer);
break;
case SNAPSHOT_ANY:
return HeapTupleSatisfiesAny(tup, snapshot, buffer);
break;
case SNAPSHOT_TOAST:
return HeapTupleSatisfiesToast(tup, snapshot, buffer);
break;
case SNAPSHOT_DIRTY:
return HeapTupleSatisfiesDirty(tup, snapshot, buffer);
break;
case SNAPSHOT_HISTORIC_MVCC:
return HeapTupleSatisfiesHistoricMVCC(tup, snapshot, buffer);
break;
case SNAPSHOT_DECODE_MVCC:
return HeapTupleSatisfiesDecodeMVCC(tup, snapshot, buffer);
break;
}
return false;
}
-
这段代码定义了一个函数HeapTupleSatisfiesVisibility,它接收三个参数:一个堆元组tup,一个快照snapshot,和一个缓冲区buffer。这个函数的目的是检查给定的堆元组是否满足时间限定符。
-
函数的主要逻辑是一个switch语句,它根据快照的satisfies属性的值来调用不同的函数。每个函数都接收相同的参数(即tup,snapshot和buffer),并返回一个布尔值,表示堆元组是否满足特定类型的快照。
-
例如,如果satisfies属性的值为SNAPSHOT_MVCC,那么就会调用函数HeapTupleSatisfiesMVCC。如果satisfies属性的值为SNAPSHOT_VERSION_MVCC,那么就会调用函数HeapTupleSatisfiesVersionMVCC。以此类推。
-
如果satisfies属性的值不匹配任何预定义的值,那么函数将返回false。
-
这个函数用于在数据库系统中检查某个堆元组是否满足给定快照的时间限定符,以便进行相应的处理。
-
下面我们分析可见性判断的主要实现函数*
三、XidVisibleInSnapshot函数分析
Function name:XidVisibleInSnapshot
完整源码以及注释如下
/*
* XidVisibleInSnapshot
* This code is a function named XidVisibleInSnapshot, which accepts five parameters: TransactionId xid, Snapshot snapshot,
* TransactionIdStatus* hintstatus, Buffer buffer, and bool* sync
* The main purpose of this function is to check whether a transaction ID (xid) is visible in a given snapshot (snapshot).
* It does this by obtaining the commit sequence number (csn) of the transaction ID and comparing it with the commit sequence number of the snapshot.
*
*/
bool XidVisibleInSnapshot(TransactionId xid, Snapshot snapshot, TransactionIdStatus* hintstatus, Buffer buffer, bool* sync)
{
volatile CommitSeqNo csn;
bool looped = false;
TransactionId parentXid = InvalidTransactionId;
*hintstatus = XID_INPROGRESS;
#ifdef XIDVIS_DEBUG
ereport(DEBUG1,
(errmsg("XidVisibleInSnapshot xid %ld cur_xid %ld snapshot csn %lu xmax %ld",
xid,
GetCurrentTransactionIdIfAny(),
snapshot->snapshotcsn,
snapshot->xmax)));
#endif
loop:
csn = TransactionIdGetCommitSeqNo(xid, false, true, false, snapshot);
//fetch CSN of specified transaction id
#ifdef XIDVIS_DEBUG
ereport(DEBUG1,
(errmsg("XidVisibleInSnapshot xid %ld cur_xid %ld csn %ld snapshot"
"csn %ld xmax %ld",
xid,
GetCurrentTransactionIdIfAny(),
csn,
snapshot->snapshotcsn,
snapshot->xmax)));
#endif
if (COMMITSEQNO_IS_COMMITTED(csn)) {
*hintstatus = XID_COMMITTED;
if (csn < snapshot->snapshotcsn)
return true;
else
return false;
//if the commit sequence number has been committed,
//and is less than the commit sequence number of the sanpshot,
//then the function return true ,else return false
} else if (COMMITSEQNO_IS_COMMITTING(csn)) {
//if the commit sequece number is committing,
//the function performs some additonal checks and operations
//including :synchronously waiting for the transaction to end
if (looped) {
ereport(DEBUG1, (errmsg("transaction id %lu's csn %ld is changed to ABORT after lockwait.", xid, csn)));
/*
* If a loop has already been performed (looped is true),
* then the function reports that the csn of the transaction ID has been changed to ABORT
* after waiting for a lock. Then, it rechecks whether the transaction ID has finished,
* sets the csn of the transaction ID to ABORTED, updates the latest fetch state of the transaction ID to ABORTED,
* sets hintstatus to XID_ABORTED, and returns false (false).
*/
RecheckXidFinish(xid, csn);
CSNLogSetCommitSeqNo(xid, 0, NULL, COMMITSEQNO_ABORTED);
SetLatestFetchState(xid, COMMITSEQNO_ABORTED);
*hintstatus = XID_ABORTED;
return false;
} else {
if (!COMMITSEQNO_IS_SUBTRANS(csn)) {
/* If snapshotcsn lower than csn stored in csn log, don't need to wait. */
CommitSeqNo latestCSN = GET_COMMITSEQNO(csn);
if (latestCSN >= snapshot->snapshotcsn) {
ereport(DEBUG1,
(errmsg(
"snapshotcsn %lu lower than csn %lu stored in csn log, don't need to sync wait, trx id %lu",
snapshot->snapshotcsn,
csn,
xid)));
return false;
}
} else {
parentXid = (TransactionId)GET_PARENTXID(csn);
}
if (u_sess->attr.attr_common.xc_maintenance_mode || t_thrd.xact_cxt.bInAbortTransaction) {
return false;
}
/* Then, if it is currently in maintenance mode or in the process of aborting a transaction, then the function returns false */
/* Wait for txn end and check again. */
if (sync != NULL) {
*sync = true;
}
if (TransactionIdIsValid(parentXid))
SyncWaitXidEnd(parentXid, buffer);
else
SyncWaitXidEnd(xid, buffer);
looped = true;
parentXid = InvalidTransactionId;
goto loop;
}
} else {
if (csn == COMMITSEQNO_ABORTED)
*hintstatus = XID_ABORTED;
return false;
}
}
这段代码用于检查一个事务 ID 是否在一个快照中可见。
代码的主要逻辑如下:
- 首先,使用 TransactionIdGetCommitSeqNo 函数获取事务 ID 的提交序号(csn),这是一个表示事务状态的值。
- 如果 csn 是 COMMITSEQNO_IS_COMMITTED,表示事务已经提交,那么根据 csn 和快照的 csn 比较,返回 true 或 false。
- 如果 csn 是 COMMITSEQNO_IS_COMMITTING,表示事务正在提交,那么需要同步等待事务结束。
- 如果 csn 是 COMMITSEQNO_IS_SUBTRANS,表示事务是一个子事务,那么需要获取其父事务 ID(parentXid)。
- 如果已经循环过一次,表示发生了异常,那么需要重新检查事务状态,并将 csn 设置为 COMMITSEQNO_ABORTED。
- 否则,使用 SyncWaitXidEnd 函数等待事务或其父事务结束,并重新获取 csn。
- 如果 csn 是其他值,表示事务未提交或已中止,那么返回 false。
四、小结
本次博客学习了open gauss数据库建立过程中相对于SQL标准新增的事务隔离级别,对于可见性判断的接口做了简单学习,了解其分别对哪些情况进行了函数调用。分析了XidVisibleInSnapshot 函数的结构,这个函数对于快照的可见性有着至关重要的作用。HeapTupleSatisfiesMVCC函数的详细分析,我们留在下一篇博客展开叙述。 下一篇博客MVCC(4)-可见性判断