一、PostgreSQL中的MVCC
先举一个简单的例子来理解它的机制,在同一个session中执行以下两条:
insert into table1(id,name) values (1,'小刘'); update table1 set name = '小攀' where id =1;
假设执行前,当前事务ID为:100,则执行这两条语句时在page中数据的存放为:
注1:上图中执行update后,tuple1中xmax被设置为当前事务ID:101,ctid被设置为新版本tuple2的位置从上可以看出,在PostgreSQL中,当一行记录被update时,该行数据(称为tuple)的新版本将被创建并插入表中(如上图的tuple2),之前版本(tuple1)提供一个指针(ctid)指向新版本,之前版本被标记为”expired”过期(即xmax值由0 -> 101),但是还保留在数据库直到垃圾收集器回收掉。
PostgreSQL中的MVCC实现原理可简单概括如下:
每个事务都会得到一个XID(称为事务ID),当一个新事务开始,递增XID,然后把它赋予这个新事务(源码里面由是ShmemVariableCache->nextXid这个值作为当前新事物的ID,然后在自增,见GetNewTransactionId函数)。
把一个元组(Tuple)称作同一逻辑行的一个行版本,数据文件中存放同一逻辑行的多个行版本
每个行版本的头部,记录该行版本的创建和删除的事务ID(分别称为xmin和xmax)
每个事务的状态(running, abort 或 commit)记录在pg_clog文件中
运用一定的规则,使每个事务只会看到一个特定的行版本(快照)
Tuple对事务T1可见的条件
T1的隔离级别为可重复读或可串行化时:
创建该元组的事务(xmin)在T1开始前已提交
更新(如果有)该元组的事务(xmax)在T1开始时不处于已提交状态
T1的隔离级别为读已提交时
创建该元组的事务(xmin)在T1的查询执行前已提交
更新(如果有)该元组的事务(xmax)在T1的查询执行时不处于已提交状态
某一时刻t,事务已提交的判断条件
pg_clog中该事物状态为已提交。且
该事务ID小于t时刻的事务计数。且
t时刻采取的SnapshotData中不包含该事务ID
举个例子,当insert一行记录时,只有那些已提交的、并且xmin比当前事务XID小(xmin<XID) 的行记录 对当前事务才是可见的。
这意味着你可以创建一个新事务然后插入记录,直到commit之前,这些记录对其他事务永远都是不可见的;commit之后,其他后创建的新事务就可看到这行新记录了(xmin<XID)。
对于delete和update,机制也是类似的,不同的是要用xmax值来判断数据的可见性。
不过上面介绍的只是个简单的场景,实际源码中需要结合xid、快照以及tuple中的xmin、xmax和事务状态标记(t_infomask)等综合来判断TUPLE的可见性,后续将详细叙述。
1.1.1 事务ID
在PostgreSQL中,每个事务都有一个唯一的事务ID,被称为XID,在源码中是由GetNewTransactionId函数获取。全局保存在ShmemVariableCache->nextXid中。注意:
除了被BEGIN - COMMIT/ROLLBACK/END包裹的一组语句会被当作一个事务对待外,不显示指定BEGIN - COMMIT/ROLLBACK/END的单条语句也是一个事务。
在BEGIN - COMMIT/ROLLBACK/END 事务中,若没有子事务,那么事务id是不变的,即使有多次插入/更新/删除操作(不同的操作,可能会修改一个或多个元组tuple中xmin和xmax的值,但是修改的值都是同一个事务id,后面详述)。
不管有没有BEGIN - COMMIT/ROLLBACK/END,只要没有INSERT、UPDATE、DELETE操作,在当前session中,事务ID都不会递增。
数据库中的事务ID递增。可通过txid_current()函数获取当前事务的ID,不过每次获取时XID都会自增一。
1.1.2 PostgreSQL如何存储数据
PostgreSQL中,一个表对应一个逻辑文件,一个表被分割成若干个物理段文件(relation segment),每个段又由若干个文件页组成。在数据表创建的过程中创建相应的数据文件,而这些数据文件就是我们通常所说的表中数据所存放的位置。而这些数据文件是按照页的形式组织,称之为文件页。文件页(磁盘块)是物理段文件的基本储存单位,也是内存和磁盘交换的单位。文件页大小限制了表元组的大小并影响磁盘操作效率,缺省大小8192字节,最大可设置为2^15字节(这是由磁盘块索引是15位决定的)。
一个文件页空间被逻辑分割为三个部分:
PageHeader:页描述区 ◦记载页的使用情况, 如页分布格式版本,元组数据空间和特殊空间的起始位置以及文件页相关的事务日志记载点等信息
Tuple Item space:元组数据空间,实际记录元组数据的地方
Special space:特殊空间
1.1.3 元组(tuple)
PostgreSQL中,对于每一行数据(称为一个tuple),包含有5个隐藏字段。这五个字段是隐藏的,但可直接访问。
xmin 创建该tuple时的事务ID(后续都称为CreationID),由INSERT或UPDATE操作时设置
xmax 过期的事务ID(后续都称为ExpiredID),在删除该tuple时,记录此时的事务ID,或者更新这个tuple时在旧的tuple上记录更新操作时的事务ID;如果该值不为0,则说明该行数据进行过删除或更新或回滚注:该标记也用于显式行锁
cmin和cmax 标识在同一个事务中增删改元组时存储其序列值,从0开始,只有INSERT/UPDATE/DELETE操作会递增,用于同一个事务中实现版本可见性判断注:该标记也用于显式行锁
ctid 是用来记录当前元组或新元组的物理位置,有块号和快内偏移组成,可以用来查找元组的下一个版本在磁盘上的位置,如果这个元组被更新,则该字段指向更新后的新元组,所以一个元组是最新版本,当且仅当它的xmax为空或者它的ctid指向它自己。元组的结构图如下所示:
元组的代码结构为:
struct HeapTupleHeaderData { union { HeapTupleFields t_heap; DatumTupleFields t_datum; }t_choice; ItemPointerData t_ctid; *current TID of this or newer tuple(or a speculative insertion token) */ /* Fields below here must match MinimalTupleData! */ uint16 t_infomask2; /* number of attributes + various flags */ uint16 t_infomask; /* various flag bits, see below */ uint8 t_hoff; /* sizeof header incl. bitmap, padding */ /* ^ - 23 bytes - ^ */ bits8 t_bits[FLEXIBLE_ARRAY_MEMBER]; /* bitmap of NULLs */ /* MORE DATA FOLLOWS AT END OF STRUCT */ };
在上面的元组头部结构中t_heap即为元组结构,而t_infomask表明了xmin和xmax的状态(committed/invalid/aborted)等信息,而t_infomask2包含了该元组属性的个数(即表的列数)和其他一些标志信息,具体含义见下:
t_infomask的各位的含义如下:
/* * information stored in 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_XMAX_KEYSHR_LOCK 0x0010 /* xmax is a key-shared locker */ #define HEAP_COMBOCID 0x0020 /* t_cid is a combo cid */ #define HEAP_XMAX_EXCL_LOCK 0x0040 /* xmax is exclusive locker */ #define HEAP_XMAX_LOCK_ONLY 0x0080 /* xmax, if valid, is only a locker */ * xmax is a shared locker */ #define HEAP_XMAX_SHR_LOCK (HEAP_XMAX_EXCL_LOCK | HEAP_XMAX_KEYSHR_LOCK) #define HEAP_LOCK_MASK (HEAP_XMAX_SHR_LOCK | HEAP_XMAX_EXCL_LOCK | \ HEAP_XMAX_KEYSHR_LOCK) #define HEAP_XMIN_COMMITTED 0x0100 /* t_xmin committed 即xmin已经提交*/ #define HEAP_XMIN_INVALID 0x0200 /* t_xmin invalid/aborted */ #define HEAP_XMIN_FROZEN (HEAP_XMIN_COMMITTED|HEAP_XMIN_INVALID) #define HEAP_XMAX_COMMITTED 0x0400 /* t_xmax committed即xmax已经提交*/ #define HEAP_XMAX_INVALID 0x0800 /* t_xmax invalid/aborted */ #define HEAP_XMAX_IS_MULTI 0x1000 /* t_xmax is a MultiXactId */ #define HEAP_UPDATED 0x2000 /* this is UPDATEd version of row */ #define HEAP_MOVED_OFF 0x4000 /* moved to another place by pre-9.0 * VACUUM FULL; kept for binary * upgrade support */ #define HEAP_MOVED_IN 0x8000 /* moved from another place by pre-9.0 * VACUUM FULL; kept for binary * upgrade support */ #define HEAP_MOVED (HEAP_MOVED_OFF | HEAP_MOVED_IN) #define HEAP_XACT_MASK 0xFFF0 /* visibility-related bits */
t_infomask2的各位的含义如下:
/* * information stored in t_infomask2: */ #define HEAP_NATTS_MASK 0x07FF /* 11 bits for number of attributes */ /* bits 0x1800 are available */ #define HEAP_KEYS_UPDATED 0x2000 /* tuple was updated and key cols * modified, or tuple deleted */ #define HEAP_HOT_UPDATED 0x4000 /* tuple was HOT-updated */ #define HEAP_ONLY_TUPLE 0x8000 /* this is heap-only tuple */ #define HEAP2_XACT_MASK 0xE000 /* visibility-related bits */
注1、t_infomask2的低11 bits记录的是该元组属性的个数(即表的列数)
1.1.4 Hint Bits
Hint Bits是tuple头部的infomask里的4个BIT,用来表示该tuple的事务状态;主要是用于标记由那些已经提交或中止的事务所创建或删除的元组。There are four hint bits:
#define HEAP_XMIN_COMMITTED 0x0100 /* t_xmin committed 即xmin已经提交*/ #define HEAP_XMIN_INVALID 0x0200 /* t_xmin invalid/aborted */ #define HEAP_XMAX_COMMITTED 0x0400 /* t_xmax committed即xmax已经提交*/ #define HEAP_XMAX_INVALID 0x0800 /* t_xmax invalid/aborted */
因为如果要确定没有设置这些位的元组的可见性,那么我们只能从pg_clog中或者PGXACT内存结构中(未结束的或未清除的事务信息内存)得知该tuple对应的事务提交状态,显然如果每条tuple都要查询pg_clog的话,性能一定会很差。 所以为了提升性能,PostgreSQL在tuple的头部t_infomask中通过4个比特位来存储事务的提交状态。从而我们不需要查询pg_clog来获得事务信息。 但是请注意,并不是在事务结束时设置t_infomask的hint bits。而是在后面的DML或者DQL,VACUUM等SQL扫描到对应的TUPLE时,才会触发SET BITS(由SetHintBits函数实现)的操作。 可以通过gdb调试,设置SetHintBits函数断点,来验证。比如在执行INSERT或UPDATE操作时并不会触发SetHintBits函数断点,而通过1.3.5节所示的方法观察所插入或更新元组的t_infomask字段,发现该字段的含义并不是真实的含义,而只有再次执行SELETE或 UPDATE扫描到该元组时,才会更新t_infomask字段,使其为真实的含义。
为了观察元组的实际存储形式,此处借助postgres自带的插件 pageinspect来进一步观察元组在page中的存放格式。 首先加载该插件,直接执行:create extension pageinspect;即可,需要注意的是,如果提示没有该插件,则需要在src/ contrib 目录下执行“make;make install”; pageinspect里边有三个函数是本文用到的,他们分别是:
get_raw_page 根据参数表名、数据文件类型(main、fsm、vm)以及page位置,将当前表文件中的page内容返回。还有一个函数于此同名,只有两个参数,是将第二个参数省略,直接使用’main’。
page_header 参数是函数get_raw_page的返回值,返回值是将本page结构中的PageHeaderData详细信息
heap_page_items 参数是函数get_raw_page的返回值,返回值是将page内的项指针(ItemIddata)以及HeapTupleHeaderData的详细信息。 接着创建一个表,并且分别通过INSERT和UPDATE,插入和修改一条记录,如下所示:
create table table1(id int,name varchar); insert into table1(id,name) values (1,'小刘'); update table1 set name = '小攀' where id =1;
查看表table1中cmin,cmax,xmin,xmax,ctid这五个隐藏字段的值:
select cmin,cmax,xmin,xmax,ctid, * from table1;
从上图中可以看到,在建表前XID为1844,表table1中id为1的记录中xmin为1847,该值就是执行UPDATE语句时的XID,因为create、INSERT、UPDATE都会获取新的XID。 通过pageinspect 观察表table1各版本行在page中的存放,命令为:
select * from heap_page_items(get_raw_page('table1',0));得到的结果如下:
从上图可以看出实际上数据库中存储了两行数据,可外部只能够查看一行,至于哪些行的数据对当前事务可见,就是有MVCC控制的,而MVCC的实现和上图中t_min、t_xmax、t_ctid、t_infomask2、t_infomask这几个字段的值息息相关,这几个值的含义为:
lp:这是插件自己定义的列,在源码中其实没有,这个是项指针的顺序。 lp_off:tuple在page中的位置。 lp_flags:tuple的flags,具体为 #define LP_UNUSED 0 * unused (should always have lp_len=0) */ #define LP_NORMAL 1 * used (should always have lp_len>0) */ #define LP_REDIRECT 2 * HOT redirect (should have lp_len=0) */ #define LP_DEAD 3 * dead, may or may not have storage */ lp_len: HeapTupleHeaderData 长度+Oid长度(8,因为要数据对齐,所以在这里会比原来预计的多4)。 t_min和t_xmax是插入、删除和更新时的事务ID,插入时会在t_min内写入当前事务ID,当删除时就会在t_xmax写入当前事务ID。更新是进行删除后再插入。 t_cid:这个是指一个事务内的命令ID,每个事务都是从0开始。 t_ctid:这个是指物理ID,存储的是(块号,块内偏移号),块号:bi_hi(文件号)<< 16 | bi_lo(page号),来获取磁盘顺序,块内偏移号ip_posid是在page的中序号,以此来准确定位数据。 t_infomask2:表字段的个数以及一些flags t_infomask:tuple的flags t_hoff: HeapTupleHeaderData长度,如果有Oid会增加4,但由于受到对齐的影响,会增加8。 t_bits:具体数据,可以参照 PostgreSQL的基础数据类型分析记录
具体MVCC如何控制当前事务下哪些TUPLE的可见性,由后面介绍,此处只是简单介绍下t_infomask2和t_infomask这两个字段值的解读: 在上图第一行,即lp为1的那个版本行,t_infomask2为16386(0x4002)= HEAP_HOT_UPDATED | 2;表明该行tuple是HOT-updated,且行的属性个数(表的列数)是2。 t_infomask为1282(0x502)=(0x400 | 0x100 | 0x2)= HEAP_XMAX_COMMITTED | HEAP_XMIN_COMMITTED | HEAP_HASVARWIDTH;表明该行tuple的xmax已经提交即删除或更新已经完成,xmin已经提交即插入已经完成,且含有可变长参数(即表中的第二列属性为varchar);实际在结合xmax为1847,不为0,也可以知道该行数据已经被删除或更新了。
二、PostgreSQL中的快照
PostgreSQL采用“快照”的方式来实现MVCC,而快照就是某时刻下数据库所有元组的xmin和xmax满足一定条件的值的集合。三个事务隔离等级拍摄快照的时机不同,但其判断条件都是一样的。 事务启动创建快照的过程简单说就是在事务启动的时刻,遍历当前所有活动的(还未提交)事务,记录在一个活动Transaction的ID数组中;选择所有活跃事务中最小的XID,记录在xmin中,选择所有已提交事务中最大的XID,加1后记录在xmax中。那么:
xmin <= xmax
所有事务ID小于xmin的事务可以被认为已经完成,即事务已提交,其所做的修改对当前快照可见;
所有事务ID大于或等于xmax的事务可以被认为是正在执行,其所做的修改对当前快照不可见;
对于事务ID处在 [xmin, xmax)区间的事务, 需要结合活跃事务列表与事务提交日志CLOG,判断其所作的修改对当前快照是否可见;
2.1 快照的结构
在源码中快照SnapshotData的结构为:
/* * Struct representing all kind of possible snapshots. * * There are several different kinds of snapshots: * * Normal MVCC snapshots * * MVCC snapshots taken during recovery (in Hot-Standby mode) * * Historic MVCC snapshots used during logical decoding * * snapshots passed to HeapTupleSatisfiesDirty() * * snapshots used for SatisfiesAny, Toast, Self where no members are accessed. * * TODO: It's probably a good idea to split this struct using a NodeTag * similar to how parser and executor nodes are handled, with one type for * each different kind of snapshot to avoid overloading the meaning of * individual fields. */ typedef struct SnapshotData { SnapshotSatisfiesFunc satisfies; /* tuple test function */ /* * The remaining fields are used only for MVCC snapshots, and are normally * just zeroes in special snapshots. (But xmin and xmax are used * specially by HeapTupleSatisfiesDirty.) * * An MVCC snapshot can never see the effects of XIDs >= xmax. It can see * the effects of all older XIDs except those listed in the snapshot. xmin * is stored as an optimization to avoid needing to search the XID arrays * for most tuples. */ TransactionId xmin; /* all XID < xmin are visible to me */ TransactionId xmax; /* all XID >= xmax are invisible to me */ /* * For normal MVCC snapshot this contains the all xact IDs that are in * progress, unless the snapshot was taken during recovery in which case * it's empty. For historic MVCC snapshots, the meaning is inverted, i.e. * it contains *committed* transactions between xmin and xmax. * * note: all ids in xip[] satisfy xmin <= xip[i] < xmax */ TransactionId *xip; //所有正在运行的事务的id列表 uint32 xcnt; /* # of xact ids in xip[],正在运行的事务的计数 */ /* * For non-historic MVCC snapshots, this contains subxact IDs that are in * progress (and other transactions that are in progress if taken during * recovery). For historic snapshot it contains *all* xids assigned to the * replayed transaction, including the toplevel xid. * * note: all ids in subxip[] are >= xmin, but we don't bother filtering * out any that are >= xmax */ TransactionId *subxip; //进程中子事务的ID列表 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 */ /* * An extra return value for HeapTupleSatisfiesDirty, not used in MVCC * snapshots. */ uint32 speculativeToken; /* * Book-keeping information, used by the snapshot manager */ 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 */ } SnapshotData;
实际上从上面代码的注释这也可以看到,postgres中存在几种快照,分别用于不同的流程,比如正常流程中一般使用的就是MVCC快照,而在逻辑解码期间使用的历史MVCC快照等。
而快照最核心的功能就是用来判断元组的状态,包括元组的有消息、可见性、可更新性,而这些状态又是有特定的相关函数来实现的,也就是上面快照结构中第一个成员变量:函数指针SnapshotSatisfiesFunc satisfies 所指定,主要有以下几个函数:
HeapTupleSatisfiesMVCC:判断元组对某一快照版本是否有效的函数
HeapTupleSatisfiesUpdate:判断元组是否可更新的函数
HeapTupleSatisfiesDirty:判断当前元组是否已脏的函数
HeapTupleSatisfiesSelf:判断tuple对自身信息是否有效的函数
HeapTupleSatisfiesToast:用于TOAST
HeapTupleSatisfiesVacuum:用在VACUUM,判断某个元组是否对任何正在运行的事务可见,如果是,则该元组不能被VACUUM删除
HeapTupleSatisfiesAny:所有元组都可见
以上几个函数的简介见代码中如下的注释:
/*------------------------------------------------------------------------- * * tqual.c * POSTGRES "time qualification" code, ie, tuple visibility rules. * * NOTE: all the HeapTupleSatisfies routines will update the tuple's * "hint" status bits if we see that the inserting or deleting transaction * has now committed or aborted (and it is safe to set the hint bits). * If the hint bits are changed, MarkBufferDirtyHint is called on * the passed-in buffer. The caller must hold not only a pin, but at least * shared buffer content lock on the buffer containing the tuple. * * NOTE: When using a non-MVCC snapshot, we must check * TransactionIdIsInProgress (which looks in the PGXACT array) * before TransactionIdDidCommit/TransactionIdDidAbort (which look in * pg_xact). Otherwise we have a race condition: we might decide that a * just-committed transaction crashed, because none of the tests succeed. * xact.c is careful to record commit/abort in pg_xact before it unsets * MyPgXact->xid in the PGXACT array. That fixes that problem, but it * also means there is a window where TransactionIdIsInProgress and * TransactionIdDidCommit will both return true. If we check only * TransactionIdDidCommit, we could consider a tuple committed when a * later GetSnapshotData call will still think the originating transaction * is in progress, which leads to application-level inconsistency. The * upshot is that we gotta check TransactionIdIsInProgress first in all * code paths, except for a few cases where we are looking at * subtransactions of our own main transaction and so there can't be any * race condition. * * When using an MVCC snapshot, we rely on XidInMVCCSnapshot rather than * TransactionIdIsInProgress, but the logic is otherwise the same: do not * check pg_xact until after deciding that the xact is no longer in progress. * * * Summary of visibility functions: * * HeapTupleSatisfiesMVCC() * visible to supplied snapshot, excludes current command * HeapTupleSatisfiesUpdate() * visible to instant snapshot, with user-supplied command * counter and more complex result * HeapTupleSatisfiesSelf() * visible to instant snapshot and current command * HeapTupleSatisfiesDirty() * like HeapTupleSatisfiesSelf(), but includes open transactions * HeapTupleSatisfiesVacuum() * visible to any running transaction, used by VACUUM * HeapTupleSatisfiesToast() * visible unless part of interrupted vacuum, used for TOAST * HeapTupleSatisfiesAny() * all tuples are visible
查看当前事务下的事务快照的命令为:SELECT TXID_CURRENT_SNAPSHOT();该命令会将当前的快照xmin和xmax显示出来,并且还会将当前活动的XID也显示出来
HeapTupleSatisfiesMVCC 流程
HeapTupleSatisfiesMVCC函数是用于判断元组对某一快照版本是否有效,具体的判断该元组在当前快照是否可见的流程,见下图所示流程图:
![]()
注1、因为函数的判断逻辑很复杂,所以便于观察,只画出了元组对当前快照可见的流程,其他不可见的分支直接省略,即上图中绿色菱形所代表的判断逻辑中,缺省的那条分支所代表的含义是元组对当前快照不可见。
注2、上图中判断xmin或xmax是否已提交,是依据1.3.4节的Hint Bits位(比如最开始绿色菱形中“xmin已提交”就是根据t_infomask & HEAP_XMIN_COMMITTED是否为0来判断的,若不为0,则表示Hint Bits位中有HEAP_XMIN_COMMITTED,故xmin已提交),而从该节又可知,在实际插入或更新时并不会置该标志位,所以在上图中,有时候还需要进一步判断xmin或xmax是否确实已经提交,判断的依据就是依次从子事务开始递归到父事务,从share buffer中查看所在page中的事务状态,具体参见函数TransactionIdDidCommit。
注3、上图所示流程为postgresql-10beta1版本的源码中所示流程,不同版本里面的判断逻辑可能有所不同。
扫码关注了解更多











