searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

PostgreSQL中多版本控制(一)

2023-08-11 06:34:28
40
0

1.什么是MVCC

在对数据库中的数据进行维护时,会同时存在多个版本。不同的操作针对同样的行数据,读到的版本可能是不一样的。例如在并发操作中,正在写入数据时,如果有用户开始读取数据,而此时写操作并未提交,则新写入的数据对于正在读取的用户是不可见的。这意味着当检索数据时,每个事务看到的只是一段时间之前的数据快照,而不是数据的当前状态。MVCC使得读写可以并发进行,读事务不会阻塞写事务,写事务也不会阻塞读事务。PostgreSQL中使用的方法为:写数据时,旧的数据不删除,直接插入新数据。

1.1 事务ID

PostgreSQL中有时缩写为xid,是一个32bit的无符号自增数字,用来标识每个事务的新旧状况。其中包括三个特别的事务ID:
  • 0:无效事务
  • 1:初始化事务ID,只在初始化数据库集簇的时候使用
  • 2:冻结ID
事务ID是一个循环态,32bit的无符号数字表示的范围是42亿左右,因此对于一个事务来说,在它之前的21亿个事务属于过去,之后的21亿个属于未来。
事务ID的循环态分配
在命令行中,可以通过txid_current函数来获取当前事务的标识。
 
查询当前事务ID
PostgreSQL中的事务包含四种状态,它们在源码中的定义为:
 
事务的四种状态

1.2 Tuple结构

PostgreSQL中的行数据称之为元组(tuple),其中包含了一些可以用来判断可见性的字段,它们定义在元组的头部信息中,结构如下:

tuple的结构

字段的具体作用如下:

  • t_xmin:插入该元组的事务ID
  • t_xmax:删除或者更新该元组的事务ID,没有删除或修改的话值为 0
  • t_cid:32位无符号数字,标识同一个事务中,执行当前命令之前共执行的命令数,从0开始计数。用于判断游标的可见性。保存的是一个到实际 Cmin 和 Cmax 的映射。
  • t_infomask:位掩码,内部保存了事务执行的状态。

1.2.1 t_infomask结构

t_infomask的作用非常大,它是一个32位无符号数字,其中的每一位都代表了不同的含义,源码中的定义如下:

t_infomask的结构

其中较为重要的几个字段用来判断t_xmin和t_xmax的状态,以及判断t_cid是否是一个combo cid的形式。

1.2.2 t_cid(命令ID)的含义

t_cid中保存的是cmin和cmax的值,用来判断同一个事务下的不同命令导致元组版本变化是否可见。如果一个事务内的所有命令都是严格顺序执行的,那么每个命令都能看到之前该事务内的所有变更。但是一个事务内存在命令交替执行的情况,比如使用游标进行查询。Fetch游标时看到的是声明游标时的数据快照而不是Fetch执行时,即声明游标后对数据的变更对该游标不可见。为了判断数据的可见性,引入了t_cid的概念。首先看一下源码中t_cid的定义位置:
 
  • cmin:插入元组时的命令ID(从0开始)
  • cmax:删除元组时的命令ID(从0开始)
如果通过系统查看这两者的数值,会发现二者一直相同,原因如下:如果事务只是插入数据,那么t_cid保存的就是cmin;如果进行了update或者delete时,才会产生cmax,此时在一个事务中既有插入又有更新,则t_cid会保存 combo cid。如果t_infomask中该位置为真,则需要通过combo cid来获取 cmin 和 cmax。源码中combo cid的定义如下,可以看出其中包含的正是cmin和cmax:

事务第一次更新本事务插入的tuple时,开开辟一个数组 comboCids,大小初始为100,每次空间不够时会扩容变为2倍。同时会使用一个Hashmap,根据ComboCidKeyData来快速查找combo cid。具体流程为:
  • 先根据(cmin, cmax) 查找 comboHash
  • 如果找到则返回ComboCidEntryData 中的 combocid
  • 如果没找到,则往comboCids数组中添加一个ComboCidKeyData元组,同时往Hashmap中插入一个entry,让combocid的值为当前comboCids数组的大小,然后数组大小加一。

 

使用的函数为GetComboCommandId,其结构如下:
    
static CommandId
    GetComboCommandId(CommandId cmin, CommandId cmax)
    {
        CommandId   combocid;
        ComboCidKeyData key;
        ComboCidEntry entry;
        bool        found;

        /*
        * Create the hash table and array the first time we need to use combo
        * cids in the transaction.
        */
        if (comboHash == NULL)
        {
            HASHCTL     hash_ctl;

            /* Make array first; existence of hash table asserts array exists */
            comboCids = (ComboCidKeyData *)
                MemoryContextAlloc(TopTransactionContext,
                                sizeof(ComboCidKeyData) * CCID_ARRAY_SIZE);  // 初始的分配空间为 100
            sizeComboCids = CCID_ARRAY_SIZE;
            usedComboCids = 0;

            memset(&hash_ctl, 0, sizeof(hash_ctl));
            hash_ctl.keysize = sizeof(ComboCidKeyData);
            hash_ctl.entrysize = sizeof(ComboCidEntryData);
            hash_ctl.hcxt = TopTransactionContext;

            comboHash = hash_create("Combo CIDs",
                                    CCID_HASH_SIZE,
                                    &hash_ctl,
                                    HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
        }

        /*
        * Grow the array if there's not at least one free slot.  We must do this
        * before possibly entering a new hashtable entry, else failure to
        * repalloc would leave a corrupt hashtable entry behind.
        */
        if (usedComboCids >= sizeComboCids)
        {
            int         newsize = sizeComboCids * 2;  // 扩容的时候每次扩大 2 倍
            comboCids = (ComboCidKeyData *)
                repalloc(comboCids, sizeof(ComboCidKeyData) * newsize);
            sizeComboCids = newsize;
        }

        /* Lookup or create a hash entry with the desired cmin/cmax */

        /* We assume there is no struct padding in ComboCidKeyData! */
        key.cmin = cmin;
        key.cmax = cmax;
        /* 使用 hash 值查找对应的 entry */
        entry = (ComboCidEntry) hash_search(comboHash,
                                            (void *) &key,
                                            HASH_ENTER,
                                            &found);

        if (found)
        {
            /* Reuse an existing combo cid */
            return entry->combocid;
        }

        /* We have to create a new combo cid; we already made room in the array */
        combocid = usedComboCids;

        comboCids[combocid].cmin = cmin;
        comboCids[combocid].cmax = cmax;
        usedComboCids++;

        entry->combocid = combocid;
        return combocid;
    }
其他地方获取实际的cmin和cmax使用如下函数:
 /* 获取 cmin 的数值 */
static CommandId
GetRealCmin(CommandId combocid)
{
    Assert(combocid < usedComboCids);
    return comboCids[combocid].cmin;
}

 /* 获取 cmax 的数值 */
static CommandId
GetRealCmax(CommandId combocid)
{
    Assert(combocid < usedComboCids);
    return comboCids[combocid].cmax;
}

1.2.3 命令ID的储存情况

  • 当xmax为 0,即行版本还没有被删除时,t_cid代表插入命令的命令标识。
  • 当xmax不为 0,且插入事务标识xmin和删除事务标识xmax不同时,t_cid代表删除命令的命令标识。
  • 当xmax不为 0,且插入事务标识xmin和删除事务标识xmax相同时,t_cid代表组合命令标识。在backend的私有空间存储了组合命令标识到实际的{cmin,cmax}组合的映射。
  • 执行VACUUM FULL时不需要cmin和cmax,所以t_xvac可以和t_cid共用一个存储空间。

1.2.4 命令ID的分配情况

  • 每个命令使用事务内全局的命令标识计数器的当前值作为命令标识。事务开始时,命令标识计数器被置为初值0
  • 执行更新性的SQL(包括insert,update,delete,select ... for update等)时,在SQL执行后命令标识计数器增1
  • 当命令标识计数器经过不断累加又回到初值0时,报错"cannot have more than 2^32-1 commands in a transaction"

1.3 xmin 和 xmax 的更新策略

使用 pageinspect 插件,可以查看指定表对应的 page header 内容,可以直观的查看tuple头部的信息。xmin和xmax的更新策略包括以下3种,情况:

1.3.1 插入情况

插入新行时,xmin填写为当前事务ID
 

1.3.2 删除情况

删除时,将被删除行的`xmax`填写为当前事务ID
 
标记为已删除的元组将会在 `VACUUM` 的过程中被清除。

1.3.3 更新情况

元组更新不会删除旧的数据行,而是将旧数据行标记为删除,同时插入新数据行。
 

 

1.4 事务快照

一个数据集合,保存事务在当前时间点所看到的事务状态信息。使用 txid_current_snapshot函数获取。格式为: xmin: xmax: xip_list。xmin 表示所有小于它的事务处于提交或者回滚状态。xmax表示第一个尚未分配的事务ID。xip_list是当前获取快照时还活跃的事务,具体函数定义如下:
typedef struct SnapshotData{
    SnapshotType    snapshot_type;  // 快照类型
    TransactionID   xmin;           // 最小活跃事务ID
    TransactionID   xmax;           // 最小尚未被分配的事务ID
    TransactionID   *xip;           // 快照时所有活跃事务DI
    uint32          xcnt;           // 当前活跃事务的长度
    CommandID       curcid;         // 当前命令的序号
    ...
}SnapshotData;
其中 SnapshotType共有其中类型,不同类型的快照使用不同的可见性函数判断,定义如下:
typedef enum SnapshotType{
    SNAPSHOT_MVCC = 0,        // 判断特定的多版本快照判断tuple是否可见
    SNAPSHOT_SELF,            // 判断元组对于自身信息是否有效
    SNAPSHOT_ANY,             // 任意可见的元组
    SNAPSHOT_TOAST,           // 判断 TOAST 的元组是否可见
    SNAPSHOT_DIRTY,           // 判断在活跃事务中的元组是否可见
    SNAPSHOT_HISTORIC_MVCC,   // 判断逻辑解码上下文中的元组是否可见
    SNAPSHOT_NON_VACUUMABLE   // 判断对于某些事务可见的元组是否真正可见(可能已经被 VACUUM 清除)
} SnapshotType;

1.4.1 SNAPSHOT_MVCC

判断元组对MVCC某一个快照是否有效,考虑的因素有以下:获取快照前所有已经提交的事务;当前事务当前命令之前的命令。

1.4.2 事务隔离级别对快照的影响

数据库的隔离级别有以下几种:
  • READ UNCOMMITTED:读未提交
  • READ COMMITTED:读已提交
  • REPEATABLE READ:重复读
  • SERIALIZABLE:串行化
PostgreSQL中默认的隔离级别是读已提交,可以使用如下命令查询数据库的默认隔离级别。
select currnet_setting('dafault_transaction_isolation');
对于读已提交级别的事务,每次操作都重新获取快照,且在该语句执行期间,仍然保持活跃状态

对于重复读和串行化级别的事务,只在事务的第一条语句开始时生成快照,在事务结束之前,该快照保持活跃状态

1.5 判断可见性

判断元组可见性的规则如下:

读取堆元组时使用 HeapTupleSatisfiesMVCC 函数判断是否对读取的 tuple 可见。函数需要三个参数,第一个参数是要判断的元组,第二个参数是判断可见性时获取到的数据快照信息,第三个参数是保存信息的buffer。
HeapTupleSatisfiesMVCC(HeapTuple htup, Snapshot snapshot, Buffer buffer)

1.5.1 xmin的状态为ABORTED

代表该元组所对应的事务已经回滚了,这是一条需要删除的数据,所以对当前快照不可见。
if (!HeapTupleHeaderXminCommitted(tuple))   // 当前事务未提交
{
    if (HeapTupleHeaderXminInvalid(tuple))  // 事务已经终止
        return false;           // 不可见
    ...
}

1.5.2 xmin状态为IN_PROGRESS

代表创建元组的事务正在进行中,如果是当前事务自己创建了该元组,并在之后使用 SELECT 语句进行了查看,那么此时该元组对于当前快照来说就是可见的。
if (!HeapTupleHeaderXminCommitted(tuple)) {                                         /* 事务未提交 */
    if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetRawXmin(tuple))) {    /* 事务是当前事务本身 */
        if (HeapTupleHeaderGetCmin(tuple) >= snapshot->curcid)
            return false;                                               /* 在快照之后插入的数据 */

        if (tuple->t_infomask & HEAP_XMAX_INVALID)                                  /* 未被当前事务删除 */
            return true;

        if (HeapTupleHeaderGetCmax(tuple) >= snapshot->curcid)
            return true;                                                /* 快照之后删除 */
        else
            return false;                                               /* 快照之前删除 */
    }
       
    /* 该元组在进行中,并且插入语句不由当前事务执行,则不可见 */
    return false;
} 

1.5.3 xmin的状态为COMMITTED

当创建元组的事务已经提交,如果该元组没有被删除或者不在当前快照的活跃事务链表中,对于当前快照可见。
/* 事务已经提交, 但是该事务在当前快照生成时还处于活跃状态,则不可见 */
if (!HeapTupleHeaderXminFrozen(tuple) &&
    XidInMVCCSnapshot(HeapTupleHeaderGetRawXmin(tuple), snapshot))
    return false;

/* 以下的情况,插入事务都已经提交 */

if (tuple->t_infomask & HEAP_XMAX_INVALID)  /* 元组未被删除,即 xmax 无效 */
            return true;

/* 元组被删除,但删除元组的事务正在进行中,尚未提交 */
if (!(tuple->t_infomask & HEAP_XMAX_COMMITTED)) {

    /* 若删除行为是当前事务自己进行的,则删除有效,但是仍然需要进行游标的判断 */
    if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetRawXmax(tuple))) {
        if (HeapTupleHeaderGetCmax(tuple) >= snapshot->curcid)
            return true;    /* deleted after scan started */
        else
            return false;   /* deleted before scan started */
    }

    /* 删除行为不是本事务执行的,并且在删除元组的事务在获取快照时还处理活跃状态,故删除无效 */
    if (XidInMVCCSnapshot(HeapTupleHeaderGetRawXmax(tuple), snapshot))
            return true;
} else {
    /* 删除元组事务已提交,但是在删除元组的事务在获取快照时还处理活跃状态,故删除无效 */
    if (XidInMVCCSnapshot(HeapTupleHeaderGetRawXmax(tuple), snapshot))
            return true;        /* treat as still in progress */
}

/* 删除元组事务已提交且不在快照的活跃事务中,即删除有效,不可见 */
return false;

1.6 判断是否可更新

使用函数 HeapTupleSatisfiesUpdate中,其返回的结果是枚举类型 TM_Result。
typedef enum TM_Result
{
    TM_Ok,
    TM_Invisible,
    TM_SelfModified,
    TM_Updated,
    TM_Deleted,
    TM_BeingModified,
    TM_WouldBlock
} TM_Result;
不同的返回结果解释如下:
 
因此如果在一个事务中进行了插入和更新语句,则在更新语句之前需要先检查版本的合法性,然后通过此函数判断是否可更新,针对不同的返回结果对应操作。
0条评论
0 / 1000
g****n
4文章数
1粉丝数
g****n
4 文章 | 1 粉丝
g****n
4文章数
1粉丝数
g****n
4 文章 | 1 粉丝
原创

PostgreSQL中多版本控制(一)

2023-08-11 06:34:28
40
0

1.什么是MVCC

在对数据库中的数据进行维护时,会同时存在多个版本。不同的操作针对同样的行数据,读到的版本可能是不一样的。例如在并发操作中,正在写入数据时,如果有用户开始读取数据,而此时写操作并未提交,则新写入的数据对于正在读取的用户是不可见的。这意味着当检索数据时,每个事务看到的只是一段时间之前的数据快照,而不是数据的当前状态。MVCC使得读写可以并发进行,读事务不会阻塞写事务,写事务也不会阻塞读事务。PostgreSQL中使用的方法为:写数据时,旧的数据不删除,直接插入新数据。

1.1 事务ID

PostgreSQL中有时缩写为xid,是一个32bit的无符号自增数字,用来标识每个事务的新旧状况。其中包括三个特别的事务ID:
  • 0:无效事务
  • 1:初始化事务ID,只在初始化数据库集簇的时候使用
  • 2:冻结ID
事务ID是一个循环态,32bit的无符号数字表示的范围是42亿左右,因此对于一个事务来说,在它之前的21亿个事务属于过去,之后的21亿个属于未来。
事务ID的循环态分配
在命令行中,可以通过txid_current函数来获取当前事务的标识。
 
查询当前事务ID
PostgreSQL中的事务包含四种状态,它们在源码中的定义为:
 
事务的四种状态

1.2 Tuple结构

PostgreSQL中的行数据称之为元组(tuple),其中包含了一些可以用来判断可见性的字段,它们定义在元组的头部信息中,结构如下:

tuple的结构

字段的具体作用如下:

  • t_xmin:插入该元组的事务ID
  • t_xmax:删除或者更新该元组的事务ID,没有删除或修改的话值为 0
  • t_cid:32位无符号数字,标识同一个事务中,执行当前命令之前共执行的命令数,从0开始计数。用于判断游标的可见性。保存的是一个到实际 Cmin 和 Cmax 的映射。
  • t_infomask:位掩码,内部保存了事务执行的状态。

1.2.1 t_infomask结构

t_infomask的作用非常大,它是一个32位无符号数字,其中的每一位都代表了不同的含义,源码中的定义如下:

t_infomask的结构

其中较为重要的几个字段用来判断t_xmin和t_xmax的状态,以及判断t_cid是否是一个combo cid的形式。

1.2.2 t_cid(命令ID)的含义

t_cid中保存的是cmin和cmax的值,用来判断同一个事务下的不同命令导致元组版本变化是否可见。如果一个事务内的所有命令都是严格顺序执行的,那么每个命令都能看到之前该事务内的所有变更。但是一个事务内存在命令交替执行的情况,比如使用游标进行查询。Fetch游标时看到的是声明游标时的数据快照而不是Fetch执行时,即声明游标后对数据的变更对该游标不可见。为了判断数据的可见性,引入了t_cid的概念。首先看一下源码中t_cid的定义位置:
 
  • cmin:插入元组时的命令ID(从0开始)
  • cmax:删除元组时的命令ID(从0开始)
如果通过系统查看这两者的数值,会发现二者一直相同,原因如下:如果事务只是插入数据,那么t_cid保存的就是cmin;如果进行了update或者delete时,才会产生cmax,此时在一个事务中既有插入又有更新,则t_cid会保存 combo cid。如果t_infomask中该位置为真,则需要通过combo cid来获取 cmin 和 cmax。源码中combo cid的定义如下,可以看出其中包含的正是cmin和cmax:

事务第一次更新本事务插入的tuple时,开开辟一个数组 comboCids,大小初始为100,每次空间不够时会扩容变为2倍。同时会使用一个Hashmap,根据ComboCidKeyData来快速查找combo cid。具体流程为:
  • 先根据(cmin, cmax) 查找 comboHash
  • 如果找到则返回ComboCidEntryData 中的 combocid
  • 如果没找到,则往comboCids数组中添加一个ComboCidKeyData元组,同时往Hashmap中插入一个entry,让combocid的值为当前comboCids数组的大小,然后数组大小加一。

 

使用的函数为GetComboCommandId,其结构如下:
    
static CommandId
    GetComboCommandId(CommandId cmin, CommandId cmax)
    {
        CommandId   combocid;
        ComboCidKeyData key;
        ComboCidEntry entry;
        bool        found;

        /*
        * Create the hash table and array the first time we need to use combo
        * cids in the transaction.
        */
        if (comboHash == NULL)
        {
            HASHCTL     hash_ctl;

            /* Make array first; existence of hash table asserts array exists */
            comboCids = (ComboCidKeyData *)
                MemoryContextAlloc(TopTransactionContext,
                                sizeof(ComboCidKeyData) * CCID_ARRAY_SIZE);  // 初始的分配空间为 100
            sizeComboCids = CCID_ARRAY_SIZE;
            usedComboCids = 0;

            memset(&hash_ctl, 0, sizeof(hash_ctl));
            hash_ctl.keysize = sizeof(ComboCidKeyData);
            hash_ctl.entrysize = sizeof(ComboCidEntryData);
            hash_ctl.hcxt = TopTransactionContext;

            comboHash = hash_create("Combo CIDs",
                                    CCID_HASH_SIZE,
                                    &hash_ctl,
                                    HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
        }

        /*
        * Grow the array if there's not at least one free slot.  We must do this
        * before possibly entering a new hashtable entry, else failure to
        * repalloc would leave a corrupt hashtable entry behind.
        */
        if (usedComboCids >= sizeComboCids)
        {
            int         newsize = sizeComboCids * 2;  // 扩容的时候每次扩大 2 倍
            comboCids = (ComboCidKeyData *)
                repalloc(comboCids, sizeof(ComboCidKeyData) * newsize);
            sizeComboCids = newsize;
        }

        /* Lookup or create a hash entry with the desired cmin/cmax */

        /* We assume there is no struct padding in ComboCidKeyData! */
        key.cmin = cmin;
        key.cmax = cmax;
        /* 使用 hash 值查找对应的 entry */
        entry = (ComboCidEntry) hash_search(comboHash,
                                            (void *) &key,
                                            HASH_ENTER,
                                            &found);

        if (found)
        {
            /* Reuse an existing combo cid */
            return entry->combocid;
        }

        /* We have to create a new combo cid; we already made room in the array */
        combocid = usedComboCids;

        comboCids[combocid].cmin = cmin;
        comboCids[combocid].cmax = cmax;
        usedComboCids++;

        entry->combocid = combocid;
        return combocid;
    }
其他地方获取实际的cmin和cmax使用如下函数:
 /* 获取 cmin 的数值 */
static CommandId
GetRealCmin(CommandId combocid)
{
    Assert(combocid < usedComboCids);
    return comboCids[combocid].cmin;
}

 /* 获取 cmax 的数值 */
static CommandId
GetRealCmax(CommandId combocid)
{
    Assert(combocid < usedComboCids);
    return comboCids[combocid].cmax;
}

1.2.3 命令ID的储存情况

  • 当xmax为 0,即行版本还没有被删除时,t_cid代表插入命令的命令标识。
  • 当xmax不为 0,且插入事务标识xmin和删除事务标识xmax不同时,t_cid代表删除命令的命令标识。
  • 当xmax不为 0,且插入事务标识xmin和删除事务标识xmax相同时,t_cid代表组合命令标识。在backend的私有空间存储了组合命令标识到实际的{cmin,cmax}组合的映射。
  • 执行VACUUM FULL时不需要cmin和cmax,所以t_xvac可以和t_cid共用一个存储空间。

1.2.4 命令ID的分配情况

  • 每个命令使用事务内全局的命令标识计数器的当前值作为命令标识。事务开始时,命令标识计数器被置为初值0
  • 执行更新性的SQL(包括insert,update,delete,select ... for update等)时,在SQL执行后命令标识计数器增1
  • 当命令标识计数器经过不断累加又回到初值0时,报错"cannot have more than 2^32-1 commands in a transaction"

1.3 xmin 和 xmax 的更新策略

使用 pageinspect 插件,可以查看指定表对应的 page header 内容,可以直观的查看tuple头部的信息。xmin和xmax的更新策略包括以下3种,情况:

1.3.1 插入情况

插入新行时,xmin填写为当前事务ID
 

1.3.2 删除情况

删除时,将被删除行的`xmax`填写为当前事务ID
 
标记为已删除的元组将会在 `VACUUM` 的过程中被清除。

1.3.3 更新情况

元组更新不会删除旧的数据行,而是将旧数据行标记为删除,同时插入新数据行。
 

 

1.4 事务快照

一个数据集合,保存事务在当前时间点所看到的事务状态信息。使用 txid_current_snapshot函数获取。格式为: xmin: xmax: xip_list。xmin 表示所有小于它的事务处于提交或者回滚状态。xmax表示第一个尚未分配的事务ID。xip_list是当前获取快照时还活跃的事务,具体函数定义如下:
typedef struct SnapshotData{
    SnapshotType    snapshot_type;  // 快照类型
    TransactionID   xmin;           // 最小活跃事务ID
    TransactionID   xmax;           // 最小尚未被分配的事务ID
    TransactionID   *xip;           // 快照时所有活跃事务DI
    uint32          xcnt;           // 当前活跃事务的长度
    CommandID       curcid;         // 当前命令的序号
    ...
}SnapshotData;
其中 SnapshotType共有其中类型,不同类型的快照使用不同的可见性函数判断,定义如下:
typedef enum SnapshotType{
    SNAPSHOT_MVCC = 0,        // 判断特定的多版本快照判断tuple是否可见
    SNAPSHOT_SELF,            // 判断元组对于自身信息是否有效
    SNAPSHOT_ANY,             // 任意可见的元组
    SNAPSHOT_TOAST,           // 判断 TOAST 的元组是否可见
    SNAPSHOT_DIRTY,           // 判断在活跃事务中的元组是否可见
    SNAPSHOT_HISTORIC_MVCC,   // 判断逻辑解码上下文中的元组是否可见
    SNAPSHOT_NON_VACUUMABLE   // 判断对于某些事务可见的元组是否真正可见(可能已经被 VACUUM 清除)
} SnapshotType;

1.4.1 SNAPSHOT_MVCC

判断元组对MVCC某一个快照是否有效,考虑的因素有以下:获取快照前所有已经提交的事务;当前事务当前命令之前的命令。

1.4.2 事务隔离级别对快照的影响

数据库的隔离级别有以下几种:
  • READ UNCOMMITTED:读未提交
  • READ COMMITTED:读已提交
  • REPEATABLE READ:重复读
  • SERIALIZABLE:串行化
PostgreSQL中默认的隔离级别是读已提交,可以使用如下命令查询数据库的默认隔离级别。
select currnet_setting('dafault_transaction_isolation');
对于读已提交级别的事务,每次操作都重新获取快照,且在该语句执行期间,仍然保持活跃状态

对于重复读和串行化级别的事务,只在事务的第一条语句开始时生成快照,在事务结束之前,该快照保持活跃状态

1.5 判断可见性

判断元组可见性的规则如下:

读取堆元组时使用 HeapTupleSatisfiesMVCC 函数判断是否对读取的 tuple 可见。函数需要三个参数,第一个参数是要判断的元组,第二个参数是判断可见性时获取到的数据快照信息,第三个参数是保存信息的buffer。
HeapTupleSatisfiesMVCC(HeapTuple htup, Snapshot snapshot, Buffer buffer)

1.5.1 xmin的状态为ABORTED

代表该元组所对应的事务已经回滚了,这是一条需要删除的数据,所以对当前快照不可见。
if (!HeapTupleHeaderXminCommitted(tuple))   // 当前事务未提交
{
    if (HeapTupleHeaderXminInvalid(tuple))  // 事务已经终止
        return false;           // 不可见
    ...
}

1.5.2 xmin状态为IN_PROGRESS

代表创建元组的事务正在进行中,如果是当前事务自己创建了该元组,并在之后使用 SELECT 语句进行了查看,那么此时该元组对于当前快照来说就是可见的。
if (!HeapTupleHeaderXminCommitted(tuple)) {                                         /* 事务未提交 */
    if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetRawXmin(tuple))) {    /* 事务是当前事务本身 */
        if (HeapTupleHeaderGetCmin(tuple) >= snapshot->curcid)
            return false;                                               /* 在快照之后插入的数据 */

        if (tuple->t_infomask & HEAP_XMAX_INVALID)                                  /* 未被当前事务删除 */
            return true;

        if (HeapTupleHeaderGetCmax(tuple) >= snapshot->curcid)
            return true;                                                /* 快照之后删除 */
        else
            return false;                                               /* 快照之前删除 */
    }
       
    /* 该元组在进行中,并且插入语句不由当前事务执行,则不可见 */
    return false;
} 

1.5.3 xmin的状态为COMMITTED

当创建元组的事务已经提交,如果该元组没有被删除或者不在当前快照的活跃事务链表中,对于当前快照可见。
/* 事务已经提交, 但是该事务在当前快照生成时还处于活跃状态,则不可见 */
if (!HeapTupleHeaderXminFrozen(tuple) &&
    XidInMVCCSnapshot(HeapTupleHeaderGetRawXmin(tuple), snapshot))
    return false;

/* 以下的情况,插入事务都已经提交 */

if (tuple->t_infomask & HEAP_XMAX_INVALID)  /* 元组未被删除,即 xmax 无效 */
            return true;

/* 元组被删除,但删除元组的事务正在进行中,尚未提交 */
if (!(tuple->t_infomask & HEAP_XMAX_COMMITTED)) {

    /* 若删除行为是当前事务自己进行的,则删除有效,但是仍然需要进行游标的判断 */
    if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetRawXmax(tuple))) {
        if (HeapTupleHeaderGetCmax(tuple) >= snapshot->curcid)
            return true;    /* deleted after scan started */
        else
            return false;   /* deleted before scan started */
    }

    /* 删除行为不是本事务执行的,并且在删除元组的事务在获取快照时还处理活跃状态,故删除无效 */
    if (XidInMVCCSnapshot(HeapTupleHeaderGetRawXmax(tuple), snapshot))
            return true;
} else {
    /* 删除元组事务已提交,但是在删除元组的事务在获取快照时还处理活跃状态,故删除无效 */
    if (XidInMVCCSnapshot(HeapTupleHeaderGetRawXmax(tuple), snapshot))
            return true;        /* treat as still in progress */
}

/* 删除元组事务已提交且不在快照的活跃事务中,即删除有效,不可见 */
return false;

1.6 判断是否可更新

使用函数 HeapTupleSatisfiesUpdate中,其返回的结果是枚举类型 TM_Result。
typedef enum TM_Result
{
    TM_Ok,
    TM_Invisible,
    TM_SelfModified,
    TM_Updated,
    TM_Deleted,
    TM_BeingModified,
    TM_WouldBlock
} TM_Result;
不同的返回结果解释如下:
 
因此如果在一个事务中进行了插入和更新语句,则在更新语句之前需要先检查版本的合法性,然后通过此函数判断是否可更新,针对不同的返回结果对应操作。
文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
0
0