赞
踩
事务
事务是为用户提供的最核心、最具吸引力的数据库功能之一。简单地说,事务是用户定义的一系列数据库操作(如查询、插入、修改或删除等)的集合,从数据库内部保证了该操作集合作为一个整体的原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),这些特性统称事务的 ACID 特性。
DBMS 中的并发控制
并发控制旨在针对数据库中对事务并行的场景,保证 ACID 中的一致性(Consistency)与隔离性(Isolation)。数据库技术中主流的三种并发控制技术分别是: Multi-version Concurrency Control (MVCC), Strict Two-Phase Locking (S2PL), 以及 Optimistic Concurrency Control (OCC),每种技术也都有很多的变种。
MVCC
MVCC 的基本机制是:写事务不会原地修改元组内容,每次写操作都会在旧的版本之上创建新的版本,并且会保留旧的版本。当某个事务需要读取数据时,数据库系统会从所有的版本中选取出符合该事务隔离级别要求的版本。
MVCC 的主要优点是读数据的锁请求与写数据的锁请求不冲突,以此来实现读不阻塞写,写也不阻塞读。
openGauss 事务整体架构
<img src='./figures/20211017-210839-v2-58a3a0df18e1a92b9cc209036fb149ab_b.jpg'>
在 openGauss 中,事务的实现与存储引擎的实现有很强关联,代码主要集中在 src/gausskernel/storage/access/transam 及 src/gausskernel/storage/lmgr 下,关键文件如图所示。
我们需要关注:
多版本元组存储结构
src/include/access/htup.h
为了定义 MVCC 中不同版本的数据,Opengauss 在每个元组的头部信息 HeapTupleHeaderData 中引入了一些字段如下:
- typedef struct HeapTupleHeaderData {
- union {
- HeapTupleFields t_heap; /* 存储该元组的一些描述信息 */
- DatumTupleFields t_datum;
- } t_choice;
-
- ItemPointerData t_ctid; /* (块号,块内偏移) 存储用来记录当前元组或新元组的物理位置 */
-
- /* Fields below here must match MinimalTupleData! */
-
- uint16 t_infomask2;
-
- uint16 t_infomask; /* various flag bits, see below */
-
- uint8 t_hoff;
-
- /* ^ - 23 bytes - ^ */
-
- bits8 t_bits[FLEXIBLE_ARRAY_MEMBER];
-
- /* MORE DATA FOLLOWS AT END OF STRUCT */
- } HeapTupleHeaderData;
- typedef HeapTupleHeaderData* HeapTupleHeader
HeapTupleFields
- typedef struct HeapTupleFields {
- ShortTransactionId t_xmin; /* 存放插入该 Tuple 时的 txid */
- ShortTransactionId t_xmax; /* 存放删除或者更新该 Tuple 时的 txid,如果还没更新或者删除,那么置 0,表示无效 */
-
- union {
- CommandId t_cid; /* 创建或更新/删除该 Tuple 的命令在该事务内执行的所有 SQL 命令中的编号 */
- ShortTransactionId t_xvac; /* old-style VACUUM FULL xact ID */
- } t_field3;
- } HeapTupleFields;
t_infomask
- #define HEAP_HASNULL 0x0001 /* has null attribute(s) */
- #define HEAP_HASVARWIDTH 0x0002 /* has variable-width attribute(s) */
- #define HEAP_HASEXTERNAL 0x0004 /* has external stored attribute(s) */
- #define HEAP_HASOID 0x0008 /* has an object-id field */
- #define HEAP_COMPRESSED 0x0010 /* has compressed data */
- #define HEAP_COMBOCID 0x0020 /* t_cid is a combo cid */
- #define HEAP_XMAX_EXCL_LOCK 0x0040 /* xmax is exclusive locker */
- #define HEAP_XMAX_SHARED_LOCK 0x0080 /* xmax is shared locker */
- /* if either LOCK bit is set, xmax hasn't deleted the tuple, only locked it */
- #define HEAP_IS_LOCKED (HEAP_XMAX_EXCL_LOCK | HEAP_XMAX_SHARED_LOCK)
- #define HEAP_XMIN_COMMITTED 0x0100 /* t_xmin committed */
- #define HEAP_XMIN_INVALID 0x0200 /* t_xmin invalid/aborted */
- #define HEAP_XMIN_FROZEN (HEAP_XMIN_INVALID | HEAP_XMIN_COMMITTED)
- #define HEAP_XMAX_COMMITTED 0x0400 /* t_xmax committed */
- #define HEAP_XMAX_INVALID 0x0800 /* t_xmax invalid/aborted */
- ...
插入、删除、更新元组
元组在页中是如何存放的
插入
假设一个 txid 为 99 的事务插入一个元组
删除
假设一个 txid 为 111 的事务删除一个元组
更新
假设 99 号事务插入的元组被 100 号事务更新了两次
openGauss 通过 HeapTupleHeaderData 的几个特殊的字段,给元组设置了不同的版本号,元组的每次更新操作都会产生一条新版本的元组,版本之间从旧到新形成了一条版本链(旧的 ctid 指向新的元组)。
事务快照的实现
为了实现元组对事务的可见性判断,openGauss 引入了事务快照 SnapshotData
在 openGauss 中,有两种方式来实现快照。
(1)活跃事务数组方法
在数据库进程中,维护一个全局的数组,其中的成员为正在执行的事务信息,包括事务的事务号,该数组即活跃事务数组。
在每个事务开始的时候,复制一份该数组内容。
当事务执行过程中扫描到某个元组时,需要通过判断元组 xmin 和 xmax 这两个事务对于查询事务的可见性,来决定该元组是否对查询事务可见。
(2)时间戳方法
在 openGauss 内部,使用一个全局自增的长整数作为逻辑的时间戳,模拟数据库内部的时序,该逻辑时间戳被称为提交顺序号(Commit Sequence Number,简称 CSN)。
每当一个事务提交的时候,在 CSN 日志中会记录该事务号 XID 对应的逻辑时间戳 CSN 值。
<img src='./figures/20211015-225513-64eaedd1d1501b104652b104bd3152b2.png'>
- #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) // 事务正在提交中
事务快照数据结构 SnapshotData
src/include/utils/snapshot.h
获取快照时会记录当前活跃的最小的 xid,记为 snapshot.xmin。当前最新提交的“事务 id(latestCompleteXid) + 1”,记为 snapshot.xmax。当前最新提交的“CSN 号 + 1”(NextCommitSeqNo),记为 snapshot.csn。
- typedef struct SnapshotData {
- SnapshotSatisfiesFunc satisfies; /* 判断可见性的函数;通常使用MVCC,即HeapTupleSatisfiesMVCC */
- TransactionId xmin; /*当前活跃事务最小值,小于该值的事务说明已结束 */
- TransactionId xmax; /*最新提交事务id(latestCompeleteXid)+1,大于等于改值说明事务还未开始,该事务id不可见 */
- TransactionId* xip; /*记录当前活跃事务链表,在CSN版本中该值无用 */
- TransactionId* subxip; /* 记录缓存子事务活跃链表,在CSN版本中该值无用 */
- uint32 xcnt; /* 记录活跃事务的个数(xip中元组数)在CSN版本中该值无用 */
- ...
-
- CommitSeqNo snapshotcsn; /* 快照的CSN号,一般为最新提交事务的CSN号+1(NextCommitSeqNo),CSN号严格小于该值的事务可见。 */
- ...
-
- CommandId curcid; /*事务块中的命令序列号,即同一事务中,前面插入的数据,后面可见。 */
- uint32 active_count; /* ActiveSnapshot stack的refcount */
- uint32 regd_count; /* RegisteredSnapshotList 的refcount*/
- void* user_data; /* 本地多版本快照使用,标记该快照还有线程使用,不能直接释放 */
- SnapshotType snapshot_type; /* openGauss单机无用 */
- } SnapshotData;
satisfies 是 openGauss 提供的对于事务可见性判断的统一操作接口。
src/gausskernel/storage/access/heap/heapam_visibility.c
MVCC 可见性判断机制
XidVisibleInSnapshot
src/gausskernel/storage/access/heap/heapam_visibility.c
- bool XidVisibleInSnapshot(TransactionId xid, Snapshot snapshot, TransactionIdStatus* hintstatus, Buffer buffer, bool* sync)
- {
- bool looped = false;
- *hintstatus = XID_INPROGRESS;
- if (GTM_MODE && TransactionIdFollowsOrEquals(xid, snapshot->xmax)) {
- return false;
- }
- loop:
- csn = TransactionIdGetCommitSeqNo(xid, false, true, false);
- if (COMMITSEQNO_IS_COMMITTED(csn)) {
- *hintstatus = XID_COMMITTED;
- if (csn < snapshot->snapshotcsn)
- return true;
- else
- return false;
- } else if (COMMITSEQNO_IS_COMMITTING(csn)) {
- ...
- } else {
- if (csn == COMMITSEQNO_ABORTED)
- *hintstatus = XID_ABORTED;
- return false;
- }
- }
如果 xid 事务正在执行:
- if (looped) {
- ereport(DEBUG1, (errmsg("transaction id %lu's csn %ld is changed to ABORT after lockwait.", xid, csn)));
- RecheckXidFinish(xid, csn);
- CSNLogSetCommitSeqNo(xid, 0, NULL, COMMITSEQNO_ABORTED);
- SetLatestFetchState(xid, COMMITSEQNO_ABORTED);
- *hintstatus = XID_ABORTED;
- return false;
- } else {
- if (!COMMITSEQNO_IS_SUBTRANS(csn)) {
- ...
- CommitSeqNo latestCSN = GET_COMMITSEQNO(csn);
- if (latestCSN >= snapshot->snapshotcsn) {
- ...
- return false;
- }
- } else {
- parentXid = (TransactionId)GET_PARENTXID(csn);
- }
- ...
- if (TransactionIdIsValid(parentXid))
- SyncWaitXidEnd(parentXid, buffer);
- else
- SyncWaitXidEnd(xid, buffer);
- looped = true;
- parentXid = InvalidTransactionId;
- goto loop;
- }
HeapTupleSatisfiesMVCC
- static bool HeapTupleSatisfiesMVCC(HeapTuple htup, Snapshot snapshot, Buffer buffer)
- {
- // 取元组头
- HeapTupleHeader tuple = htup->t_data;
- ...
- // 根据hint bit,若xmin没有被标记为已提交:可能被标记为回滚,或者还未标记
- if (!HeapTupleHeaderXminCommitted(tuple)) {
- // 如果xmin已经被标记为invalid,说明插入该元组的事务已经回滚,直接返回不可见
- if (HeapTupleHeaderXminInvalid(tuple))
- return false;
- // xmin还未标记,并且xmin为当前事务,说明是在同一个事务内的插入命令和扫描命令,则需要去判断CID
- // 同一个事务内,后面的查询可以查到当前事务之前命令插入的并且未删除的结果
- if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(page, tuple))) {
- if ((tuple->t_infomask & HEAP_COMBOCID) && CheckStreamCombocid(tuple, snapshot->curcid, page))
- return true; /* delete after stream producer thread scan started */
-
- // 当前扫描命令之后的某条命令才插入
- if (HeapTupleHeaderGetCmin(tuple, page) >= snapshot->curcid)
- return false; /* inserted after scan started */
- // 到这里说明当前扫描命令之前已经插入
- // 根据hint bit,xmax被标记为invalid
- if (tuple->t_infomask & HEAP_XMAX_INVALID)
- return true;
-
- ...
-
- // 当前扫描命令之后的某条命令删除了该元组
- if (HeapTupleHeaderGetCmax(tuple, page) >= snapshot->curcid)
- return true; /* deleted after scan started */
- else
- return false; /* deleted before scan started */
- }
- // xmin还没打标记,并且不是当前事务
- else {
- // 通过csnlog判断事务是否可见,并且返回该事务的最终提交状态
- visible = XidVisibleInSnapshot(HeapTupleHeaderGetXmin(page, tuple), snapshot, &hintstatus, buffer, NULL);
- // 如果该事务提交,则打上提交的hint bit用于加速判断
- if (hintstatus == XID_COMMITTED)
- SetHintBits(tuple, buffer, HEAP_XMIN_COMMITTED, HeapTupleHeaderGetXmin(page, tuple));
- // 如果事务回滚,则打上回滚标记
- if (hintstatus == XID_ABORTED) {
- ...
- SetHintBits(tuple, buffer, HEAP_XMIN_INVALID, InvalidTransactionId);
- }
- // 如果xmin不可见,则该元组不可见
- if (!visible) {
- ...
- return false;
- }
- }
- }
- // 根据hint bit,若xmin已经被标记为已提交,则通过函数接口CommittedXidVisibleInSnapshot判断是否对本次快照可见
- else {
- /* xmin is committed, but maybe not according to our snapshot */
- if (!HeapTupleHeaderXminFrozen(tuple) &&
- !CommittedXidVisibleInSnapshot(HeapTupleHeaderGetXmin(page, tuple), snapshot, buffer)) {
- if (...) {
- return false; /* treat as still in progress */
- }
- }
- }
- // 到此为止认为xmin visible,继续判断xmax的可见性
-
- recheck_xmax:
- // 根据hint bit,xmax已经被标记为invalid,即已经回滚
- if (tuple->t_infomask & HEAP_XMAX_INVALID) /* xid invalid or aborted */
- return true;
-
- ... // 还有一些其他状态判断
-
- // 根据hint bit,xmax没有被标记为commited
- if (!(tuple->t_infomask & HEAP_XMAX_COMMITTED)) {
- bool sync = false;
- TransactionId xmax = HeapTupleHeaderGetXmax(page, tuple);
-
- // 如果xmax为当前事务
- if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmax(page, tuple))) {
- // 如果删除该元组的命令后发生于快照扫描时刻
- if (HeapTupleHeaderGetCmax(tuple, page) >= snapshot->curcid)
- return true; /* deleted after scan started */
- else
- return false; /* deleted before scan started */
- }
-
- visible = XidVisibleInSnapshot(HeapTupleHeaderGetXmax(page, tuple), snapshot, &hintstatus, buffer, &sync);
- /*
- * If sync wait, xmax may be modified by others. So we need to check xmax again after acquiring the page lock.
- */
- if (sync && (xmax != HeapTupleHeaderGetXmax(page, tuple))) {
- goto recheck_xmax;
- }
- // 根据hintstatus在元组头部打标记 hint bit
- if (hintstatus == XID_COMMITTED) {
- SetHintBits(tuple, buffer, HEAP_XMAX_COMMITTED, HeapTupleHeaderGetXmax(page, tuple));
- }
- if (hintstatus == XID_ABORTED) {
- ...
- SetHintBits(tuple, buffer, HEAP_XMAX_INVALID, InvalidTransactionId);
- }
- if (!visible) {
- if (...) {
- if (sync && (xmax != HeapTupleHeaderGetXmax(page, tuple))) {
- goto recheck_xmax;
- }
- return true; /* treat as still in progress */
- }
- }
- }
- // 根据hint bit,xmax被标记为commited
- else {
- /* xmax is committed, but maybe not according to our snapshot */
- if (!CommittedXidVisibleInSnapshot(HeapTupleHeaderGetXmax(page, tuple), snapshot, buffer)) {
- if (...) {
- return true; /* treat as still in progress */
- }
- }
- }
- return false;
- }
隔离级别的实现
<img src='./figures/zh-cn_image_0000001197508006.png'>
(7)写偏斜(write skew):假设数据 x,y 有隐式的约束 x+y<=100;事务一读取 x=50,并写入 y=50;事务二读取 y=30 并写入 x=70,并提交;事务一再提交;最终导致 x=70,y=50 不满足 x+y<=100 的约束。
隔离级别越高,在一个事务执行过程中,它能“感知”到的并发事务的影响越小。在最高的可串行化隔离级别下,任意一个事务的执行,均“感知”不到有任何其他并发事务执行的影响,并且所有事务执行的效果就和一个个顺序执行的效果完全相同。
在 openGauss 中,隔离级别的实现基于 MVCC 和快照机制,因此这种隔离方式被称为快照隔离(Snapshot Isolation,SI)。目前,openGauss 支持读已提交和可重复读这两种隔离级别。两者实现上的差别在于在一个事务中获取快照的次数。(在实现上可重复读隔离级别无幻读问题,有 A5B 写偏斜问题)
如果采用读已提交的隔离级别,那么在一个事务块中每条语句的执行开始阶段,都会去获取一次最新的快照,从而可以看到那些在本事务块开始以后、在前面语句执行过程中提交的并发事务的效果。
如果采用可重复读的隔离级别,那么在一个事务块中,只会在第一条语句的执行开始阶段,获取一次快照,后面执行的所有语句都会采用这个快照,整个事务块中的所有语句均不会看到该快照之后提交的并发事务的效果。
<img src='./figures/20211017-204222-dc83a9cc72803e849caa49dae027369f.png'>
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。