快照是实现事务隔离的基础,快照规定了可见的数据范围,不同的快照之间相互隔离。每个事务都会持有一个快照,通过这个快照来确定可读的数据范围,以及事务中写的数据所属的版本号,从而实现事务之间的隔离。
那么在postgres当中快照在代码中是如何实现的呢?
快照类型
首先来看一下快照的类型,在postgres当中快照并不只是单纯用于MVCC,还有其它的用处。
typedef enum SnapshotType
{
SNAPSHOT_MVCC = 0,
SNAPSHOT_SELF,
SNAPSHOT_ANY,
SNAPSHOT_TOAST,
SNAPSHOT_DIRTY,
SNAPSHOT_HISTORIC_MVCC,
SNAPSHOT_NON_VACUUMABLE
} SnapshotType;不同类型的快照用于不同的场景,限制了不同的可见范围。
| 快照类型 | SNAPSHOT_MVCC | SNAPSHOT_SELF | SNAPSHOT_ANY | SNAPSHOT_TOAST | SNAPSHOT_DIRTY | SNAPSHOT_HISTORIC_MVCC | SNAPSHOT_NON_VACUUMABLE |
|---|---|---|---|---|---|---|---|
| 可见 | 1. 快照开始时间已提交的事务 2. 本事务之前的命令 | 1. 已提交的事务 2. 事务前执行的命令 3. 当前命令做的修改 | 所有行都可见 | TOAST行可见 | 和SNAPSHOT_SELF类似但是进行中的事务做得修改也可见 | 遵循SNAPSHOT_MVCC的规则,但是支持在时间旅行上下文中调用 | 可能对一些事务可见的记录 |
| 不可见 | 1. 在快照当中显示正在进行的事务 2. 在本快照启动之后启动的事务 3. 当前命令(自动提交事务的sql,非本事务)所做的修改 | 1. 当前快照显示进行中的事务 | |||||
| 隔离级别 | 可重复读 | 读已提交 | 读未提交 |
快照结构
这些事务类型都是采用统一的快照结构体来表达的,虽然以后可能拆分为不同的类别使用不同的快照结构体,这样可以保证字段的含义都是独立的,不过嘛现在还是继续基于这个统一的结构体进行说明。
typedef struct SnapshotData
{
SnapshotType snapshot_type; /* 快照类型 */
TransactionId xmin; /* all XID < xmin are visible to me */
TransactionId xmax; /* all XID >= xmax are invisible to me */
// 对于MVCC类型的快照来说,xip记录的是运行中的事务id数组
// 对于historic MVCC快照来说,含义是颠倒的,表示已经提交的事务id数组
TransactionId *xip;
uint32 xcnt; /* xip当中事务的数据 */
// 对于非历史MVCC快照来说,包含正在运行的子事务id
// 对于历史快照来说,包含要这个事务回放的所有的事务id,包括顶级事务id
// subxip当中的所有id都要>=xmin,但是并不保证小于xmax
TransactionId *subxip;
int32 subxcnt; /* subxip元素个数 */
bool suboverflowed; /* subxip数组溢出了吗? */
bool takenDuringRecovery; /* recovery-shaped snapshot? */
bool copied; /* false if it's a static snapshot */
// 在此事务当中,cid小于curcid的命令才可见
CommandId curcid; /* in my xact, CID < curcid are visible */
// HeapTupleSatisfiesDirty使用的额外字段,MVCC不需要关注
uint32 speculativeToken;
// SNAPSHOT_NON_VACUUMABLE类型快照所用字段,用来决定一个行是否可以被清理掉
struct GlobalVisState *vistest;
/*
* Book-keeping information, used by the snapshot manager */
// 快照的使用信息,用于快照管理
// 在ActiveSnapshot栈中的引用计数
uint32 active_count; /* refcount on ActiveSnapshot stack */
// RegisteredSnapshots的引用计数
uint32 regd_count; /* refcount on RegisteredSnapshots */
// RegisteredSnapshots 堆中的连接
pairingheap_node ph_node; /* link in the RegisteredSnapshots heap */
// 快照记录的时间戳
TimestampTz whenTaken; /* timestamp when snapshot was taken */
// 快照记录时在WAL流中的位置
XLogRecPtr lsn; /* position in the WAL stream when taken */
// 使用GetSnapshotData()构建此快照时的事务完成计数。能够避免在上次GetSnapshotData()之后没有事务完成时重新计算静态快照。
// 即调用GetSnapshotData()发现计数没有增加,则说明数据库状态没有改变,这个快照仍然是有效的,无需重新计算生成
uint64 snapXactCompletionCount;
} SnapshotData;