1. 事务标识

  • 事务开始时分配一个txId即事务标识
  • txId为uint_32
  • 特殊的txId:
    • 0: 无效事务
    • 1: 数据库初始化时使用的事务
    • 2: 冻结的事务
  • txId是全局唯一递增Id
  • 事务Id采用回卷(环形Id)的方式达成”无限Id”的需求

2. 事务元信息

回顾一下元组的header
image.png

  • t_xmin: 插入此元组的事务Id

    因为此元组从插入开始到后面的更新哪怕是删除等Modify操作的txId必定比第一次插入操作的txId大. 自然插入此元组的txId为t_xmin了

  • t_xmax: Modify操作的事务Id

    永远是最新一次的Modify操作的事务Id,如果没有被Modify过则为0

  • t_cid: 最新一次修改(或者第一次插入)的事务中, 第几个操作, t_cid就是几

  • t_ctid: 保存指向自己或者新元组的tid(元组Id)

    tid是(pageId,index) 没有更新就是指向自己 更新了新元组就指向新元组

  • 更多详情和举例参考《存储架构》的 元组详解

3. 事务提交日志

3.1. 日志简介

  • 提交日志是在共享内存中的
  • 提交日志的作用是保存每个事务的状态,同步给所有进程

3.2. 状态划分

  • IN_PROGRESS: 正在进行
  • COMMITTED: 已提交
  • ABORTED: 已回滚
  • SUB_COMMITTED: 子事务提交

3.3. 存储格式

数组的形式以为单位存储, 数组下标即txId,页大小8KB,一个共享文件256KB, 存储于 pg_clog目录下 以 0001,0002等命名.

4. 事务快照

  • 事务快照是通过一个字段去判断某一时刻的元组可见性来达成事务隔离的需求
  • 根据不同的时机获取最新的快照可以达成不同的隔离级别

    4.1. 快照格式

    **A:B:C0,C1,...,Cn**

  • A: xmin: txId < A 的事务Id都是不活跃的,即已提交/已回滚

  • B: xmax: txId >= B 的事务Id都是活跃的, 即正在进行
  • C: xip_list: A<= txId < B 之中, txId == Ci 的事务是不活跃

5. 可见性检查

5.1. 10条典型规则

可见性检查是PG对事务隔离提出的一整套规则,规则繁多复杂,这里举例10个典型例子
ps: 这里可见性检查基本规则依赖于元组的 t_xmax 与 t_xmin, CLOG, 以及事务快照, 是针对当前事务某一个元组是否可见的检查.

  1. CLOG(t_xmin) = ABORTED => Invisible

    如果元组的t_xmin在CLOG中查到状态为ABORTED,说明插入此元组的事务是中止了的,那么此元组不可见(废了)

  2. CLOG(t_xmin) = INPROGRESS ∧ t_xmin = current_txId ∧ t_xmax = INVALID => Visible

  3. CLOG(t_xmin) = INPROGRESS ∧ t_xmin = current_txId ∧ t_xmax != INVALID => Invisible
  4. CLOG(t_xmin) = INPROGRESS ∧ t_xmin != current_txId => Invisible

    如果元组的t_xmin在CLOG中查到状态为INPROGRESS, 分为三种情况:

    • 当前事务的txId == t_xmin
      • t_xmax为INVALID(即 0) : 表示当前元组正在被当前事务中插入, 但是t_xmax为0说明次元组没有被更新, 所以可见
      • t_xmax不为INVALID: 表示当前元组正在被当前事务事务中插入, 而t_xmax表示此元组已经被删除或更新了,此元组无效,即不可见
    • 当前事务的txId != t_xmin

    当前事务不等于t_xmin说明此元组正被其他事务插入,当前事务自然不可见

  5. CLOG(t_xmin) = COMMITTED ∧ Snapshot(t_xmin) = active => Invisible

  6. CLOG(t_xmin) = COMMITTED ∧ (t_xmax = INVALID ∨ Status(t_xmax)= ABORTED)=> Visible
  7. CLOG(t_xmin) = COMMITTED ∧ CLOG(t_xmax) = IN_PROGRESS ∧ t_xmax =current_txid => Invisible
  8. CLOG(t_xmin) = COMMITTED ∧ CLOG(t_xmax) = IN_PROGRESS ∧ t_xmax ≠current_txid => Visible
  9. CLOG(t_xmin) = COMMITTED ∧ CLOG(t_xmax) = COMMITTED ∧Snapshot(t_xmax) = active => Visible
  10. CLOG(t_xmin) = COMMITTED ∧ CLOG(t_xmax) = COMMITTED ∧Snapshot(t_xmax) ≠ active => Invisible

5.2. 可见性检查过程

简单来说可见性检查就是在开启事务的情况下每个句子(SELECT,UPDATE,INSERT,DELETE)涉及元组都会一一检查,检查方式就是根据元组的t_xmin,t_xmax,CLOG,快照,根据上述的规则得到一个bool去判断该元组是否可见.
优化:每次都读取CLOG消耗极大,对于一些确定的事务状态,例如已提交的事务,会将状态择机写入元组的header里面,后续事务可见性检查发现header里面有事务状态”缓存”就不会去读CLOG了.

6. 事务的冻结

6.1. 冻结的原因

  • txId为32位无符号整形,为了让rxId无限可用,txId使用了环形Id
  • 环形Id就涉及了一个回卷问题

image.png

  1. 如图假设当前最新的txId为 (1<<31) + 100
  2. 而txId=100的事务是一个已提交的事务,对其他事务可见
  3. 此时又新增一个事务, txId为 (1<<31) + 101
  4. 那么txId=100的事务会被回卷为不可见状态
  5. 需要一个方式让注入txId<=100的这些古老的事务Id能被回收且事务对应的元组仍可见

6.2. 冻结的方式与结果

  • 冻结的方式很简单, 将元组的txId改为2即可
  • 冻结的特点:
    1. 冻结的事务永远全局可见
    2. 冻结的事务id为2,所以永远是最小的事务Id
    3. 冻结的事务称为”非活跃”状态

6.3. 冻结过程

6.3.1. 惰性冻结

并发清理(VACUMM)通常在内部被称为“惰性清理”。但是,本文中定义的惰性模式是冻结过程执行的模式。

  • 过程:
    1. 首先计算 freezeLimit_txid
    2. 扫描每一页, 如果页没有死元组则跳过
    3. 扫描到有死元组的页, 清除死元组后, 将所有 t_xmin < freezeLimit_txid 的元组冻结
  • 优点: 效率相对高,可以和VACUMM一起执行
  • 缺点: 不能保证全部”老元组”都冻结,因为有可能有老元组在无死元组的页中被跳过

    6.3.2. 命令性(imperative)冻结

  • 区别: 与惰性冻结的区别在于,imperative-frozen会扫描所有页,彻底地冻结所有老元组

  • 优缺点: 与惰性冻结刚好相反

    6.3.3. 优化

    imperative冻结太耗资源,与是参考VACUMM的优化,在可见性映射(VM)中, 同样存有页当中是否仅包含冻结元组的标识, imperative冻结会跳过标识页.

    6.3.4. 补充

    pg_database.relfrozenxid: 冻结一张表后,目标表的pg_class.relfrozenxid将被更新。pg_class是一个系统视图,每个pg_class.relfrozenxid列都保存着相应表的最近冻结的事务标识.
    pg_class.datfrozenxid: 在完成清理过程之前,必要时会更新pg_database.datfrozenxid。每个pg_database. datfrozenxid列都包含相应数据库中的最小pg_class.relfrozenxid。

6.4. CLOG的删除

  • 之前说过CLOG写满256KB会以0002,0003等命名新增文件.
  • pg_class.datfrozenxid在0002文件中, 那么0000,0001文件都会被删除