【数据库设计与实现】第二章:数据前像与回滚

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
日志服务 SLS,月写入数据量 50GB 1个月
简介: 数据前像与回滚设计原则事务中重要的一点是事务的原子性,即事务中的所有操作要么全部生效,要么全部都不生效。数据的前像和回滚是保证事务原子性的重要手段。修改记录前,先将记录的前像(即修改前的记录)保存下来,当事务需要回滚时,将记录恢复到前像,从而保证事务的原子性。从当前的实现情况来看,前像保存主要有两种技术手段:本地更新方案:直接在原记录上进行更新,把被更新列的前像保存到独立的undo区域;异地更新方

数据前像与回滚

设计原则

事务中重要的一点是事务的原子性,即事务中的所有操作要么全部生效,要么全部都不生效。数据的前像和回滚是保证事务原子性的重要手段。修改记录前,先将记录的前像(即修改前的记录)保存下来,当事务需要回滚时,将记录恢复到前像,从而保证事务的原子性。从当前的实现情况来看,前像保存主要有两种技术手段:

  • 本地更新方案:直接在原记录上进行更新,把被更新列的前像保存到独立的 undo 区域;
  • 异地更新方案:不在原记录上更新,而是克隆 1 条新记录出来,然后在新记录上进行更新,原记录就是前像;

OracleMySQL采用的都是本地更新方案,本章加入PostgreSQL,从而能够深入地了解异地更新方案的优缺点。在设计数据前像和回滚方案时,有如下几点需要考虑:

  • 记录前像对正常写操作的影响有多大;
  • 根据前像进行回滚,回滚的效率如何;
  • 前像占用的空间如何,回收效率如何;
  • 前像是高价值数据,可以基于前像提供一致性读( MVCC )、闪回等功能,前像的设计是否能高效地实现这些功能;

Oracle设计原理

开启一个事务

图2.2-1 undo segment header block结构

 

在开启事务之前,我们先了解一下undo segment header block的总体结构,如图2.2-1所示,undo segment header block的组成情况如下:

  • undo segment header block 既是 1 block ,又是 1 segment header ,所以 cache layer footer segment control header extent map 也都是存在的(图中未标出),详细情况可以参考“空间管理与数据布局章节”;
  • retention table :记录各 extent 最近一次提交的时间,从而在 extent 到期时回收空间给后继事务使用,详细情况见表 2.2-1
  • transaction control :记录本 undo segment 中事务的总体信息,详细情况见表 2.2-2
  • free block pool :记录当前使用过且有空闲空间的 undo block ,优先使用这些 block (前面的事务未用满),详细情况见表 2.2-3
  • transaction table :记录本 undo segment 中各个事务的详细信息,详细情况见表 2.2-4

表2.2-1 retention table部分关键信息

含义

Extent number

extent

Commit time

最近的提交时间,1970.1.1起始的秒数

表2.2-2 transaction control部分关键信息

长度

含义

seq

2

新事务第1undo记录所在blockseq,与uba中的seq相对应

chd

2

transaction table中的index,指向最老的事务,即优先被替换的事务

ctl

2

transaction table中的index,指向最新的事务,即最后被替换的事务

nfb

2

free block pool中的undo block数量,0表示没有空闲的undo block

uba

7

新事务的第1undo记录的地址

scn

6

transaction table中最近1次被替换事务的提交scn,用于上界提交

表2.2-3 free block pool部分关键信息

一级域

二级域

含义

Array(*n)

uba

Undo block的地址

ext

Undo block所在的extent

spc

Undo block的空闲空间,单位字节

表2.2-4 transaction table部分关键信息

长度

含义

index

0

事务的槽位号,对应于事务表的下标,不占用实际空间,事务id的组成部分之一

state

 

9 unactive10 active

cflag

1

0x00 无事务;0x10 死事务;0x80 active事务;0x90 正在被回滚的死事务

wrap

2

本槽位每被事务复用1次加1,事务id的组成部分之一

uel

2

指向下1个事务的槽位

scn

6

事务提交时的scn,部分oracle版本在事务未提交时记录事务开始时的scn

dba

4

指向事务最近1undo记录对应的data block

nub

4

事务使用的undo block数量

cmt

 

事务提交时的时间

开启1个事务时,首先要到undo segment header blocktransaction table中申请1个位置,该位置的下标就称为该事务id的组成部分之一。xidsegment notransaction table indexwrap3个部分组成。transaction table实际上就是1个大数组,该数组中元素每被复用1次,wrap就会加1

开启的事务在transaction table中申请到位置后,会一直占用这个位置,直到该事务提交或回滚才有可能被其它事务覆盖。1undo segment只有1undo segment header block,受限于block的大小,transaction table一般只能提供34个槽位(随block大小而变化),即同时只能存放34个活跃事务。为此Oracle默认会创建10undo segment,并根据并发事务的数量以及retention参数动态伸缩undo segment数量。

transaction controltransaction tableundo segment header block中最重要的信息,下面结合实例来讲解事务并发运行过程中,transaction controltransaction table是如何记录undo日志的。假设当前的transaction controltransaction table分别如下(仅保留说明原理的关键信息):

TRN CTL

chd=index0 ctl=index34 uba=uba99 scn=scn32

TRN TBL

index0: uel=index1 scn=scn78 dba=dba19 wrap=1

index1: uel=index2 scn=scn98 dba=dba12 wrap=1

index2: uel=index3 scn=scn107 dba=dba99 wrap=1

... ...

index33: uel=index33 scn=scn765 dba=dba72 wrap=1

index34: uel=0xFF scn=scn899 dba=dba31 wrap=1

从上例我们可以看出:

  • 通过 chd ctl uel 可以将 transaction table 中的事务链在一起,这是我们见到第 1 个链表,为了说明方便,我们在本章节将其称为链表 A
  • chd 指向链表头, ctl 指向链表尾;
  • 整个链的顺序表达了事务提交的顺序,上例中的顺序是人为构造的,过于完美,实际上随着事务的频繁生成和消亡,指向顺序会比较杂乱;

针对上例,新开始的事务需要在此基础上申请1个位置,假设复用后的结果如下(仅保留说明原理的关键信息):

TRN CTL

chd=index1 ctl=index0 uba=uba342 scn=scn78

TRN TBL

index0: uel=0xFF scn=0 dba=dba26 wrap=2

index1: uel=index2 scn=scn98 dba=dba12 wrap=1

index2: uel=index3 scn=scn107 dba=dba99 wrap=1

... ...

index33: uel=index33 scn=scn765 dba=dba72 wrap=1

index34: uel=index0 scn=scn899 dba=dba31 wrap=1

假设新事务生成的第1undo记录(该undo记录的地址未uda342,仅保留说明原理的关键信息)如下:

uda=uda99 ctl max scn=scn32  prev tx scn=scn78  prev brb=dba19

新事务复用transaction table的过程大致如下:

  • transaction control 中找到 chd 指针,遍历链表,直至找到第 1 个处于提交状态的事务,即 scn 最小的提交事务,上例中假设找到的即将被覆盖的位置为 index0
  • index0 从链表 A 中摘除,本例是从链首摘除,所以只需要修改 chd ,如果是中部或者尾部摘除,情况略有不同;
  • 针对新事务生成该事务的第 1 undo 记录,记录被替换事务的 scn dba (分别对应第 1 undo 记录中的 prev tx scn prev brb ),以及 transaction control 中的 uda scn (分别对应第 1 undo 记录中的 uba ctl max scn );
  • transaction control 中的 scn 更新为被替换事务的提交 scn ,用于将来的“上界提交”,将 transaction control 中的 uda 更新为新事务的第 1 undo 记录的地址,至此完成链 B 的组建,即本 undo segment 中的所有事务通过事务的第 1 undo 记录链在一起,根据链表 B 可以回溯任何时候的 transaction control transaction table (当然前提是 undo 日志还没有被清理掉);
  • 根据新事务的信息更新 index0 ,并在事务提交后将 index0 加入到链表 A 的尾部;

在上述过程中,已开始生成undo记录,那么就需要申请undo block来存放,undo block的申请规则如下:

  • 查看 free block pool 中是否有空闲 undo block ,有则使用,否则转下一步;
  • 当前 extent 有空闲 undo block ,有则使用,否则转下一步;
  • 当前 segment 存在过期 extent ,重置该 extent wrap 1 ),然后使用该 extent ,否则转下一步;
  • 如果 undo tablespace 有空闲空间,从中分配 1 个新的 extent ,并加入到本 segment 中并使用,否则转下一步;
  • offline 状态的 segment 中窃取 1 个过期的 extent ,加入到本 segment 中并使用,否则转下一步;
  • online 状态的 segment 中窃取 1 个过期的 extent ,加入到本 segment 中并使用,否则转下一步;
  • 如果 undo tablespace 可以扩展,扩展 undo tablespace ,将新 extent 加入当前 segment 中并使用,否则转下一步;
  • 如果 undo segment 中的 retention 时间设置为非担保,自动调整 undo segment 的保留时间,每次降低 10% ,寻找过期 extent ,如此循环直至找到;

Undo BlockData Block

图2.2-2 undo block结构

 

通过上节介绍的方法找到了空闲的undo block,并向该undo block写入了本事务的第1undo记录。对于单个undo block来说,同时只能属于1个活跃事务,所以从单个事务来看,其undo记录是相对集中的。当然如果事务已经提交,而该事务并没有用满某个undo block,该undo block的剩余空间会被其它事务使用。

undo block的结构如图2.2-2所示(忽略了cache layerfooter,这对所有block来说都是一样的):

  • control section :记录本 undo block 的总体信息,详细情况见表 2.2-5
  • record directory undo 记录的目录, array 型,下标是 uba 的地址,指向 undo 记录在 record heap 中的位置,空间从上向下增长( uba 3 个部分组成, undo dba. seq no. record no ,通过 undo dba 定位到 undo block ,通过 record no 定位到目录,从而找到在 record heap 中的 undo 记录);
  • record heap :存放具体的 undo 记录,空间从下向上增长,详细情况见表 2.2-6

表2.2-5 control section部分关键信息

长度

含义

xid

8

最近1次操作本undo   block的事务id

cnt

1

undo block中的undo记录数

irb

1

指向事务(对应于xid)的最近1undo记录,回滚的起始点

表2.2-6 record heap部分关键信息

一级域

二级域

含义

record header

rec

undo记录在record   directory中的下标,不占用实际空间

undo type

组合标志位:

  • regular undo :正常的 undo 记录;
  • begin trans :含事务开始信息;
  • user undo applied undo 记录已用于回滚,防止重复回滚;

objn

undo记录所涉data   block对应的data object id

objd

object id

tblspc

tablespace no

rci

指向同1个事务的上1undo记录,0x00表示最后1undo记录

rdba

用于单个undo block无法容纳事务的所有undo记录的场景,rdba指向本事务的下1undo   block

此情况一般仅出现在undo block的第1undo记录中,其他undo记录该值都为0x00000000

undo segment header

uba

用于恢复transaction control中被覆盖的uba

ctl max scn

用于恢复transaction control中被覆盖的scn

prev tx scn

用于恢复transaction table中被覆盖的scn

txn start scn

记录新事务开始时的scn

prev brb

用于恢复transaction table中被覆盖的dba

data block ITL

xid

data block中的ITL相对应,用于恢复某条ITL

uba

flg

lck

scn

data block record

kdo op code

  • IRP 插入单行;
  • DRP 删除单行;
  • LKR 锁定行;
  • URP 更新行;
  • ORP 行链接;
  • CKI 簇键索引
  • SKL 设置簇主键指针;
  • QMI 插入多行
  • QMD 删除多行

bdba

undo记录对应的data block

hdba

undo记录对应的data block归属的segment   header block

slot

undo记录所涉记录在data blockrow   directory的下标

ncol

记录前像数据,用于恢复UPDATEDELTE所涉列

nnew

size

cols

如表2.2-6所示,单条undo记录包含如下信息:

  • record header :记录的总体信息,如所涉 data block 对应的 dba object_id tablespace ,并通过 rci 将属于同一个事务的所有 undo 记录链在一起;
  • undo segment header :可选,仅存在于事务的第 1 undo 记录中,用于恢复 undo segment 中的 transaction control transaction table
  • data block ITL :可选,用于恢复 data block 中被本事务覆盖的事务槽位上的事务信息;
  • data block record :记录 data block 上被修改数据的前像:
    • update :仅记录被修改列的前像;
    • delete :记录被删除行的所有列的前像;
    • insert :仅记录插入行的 RowID (通过 objn+bdba+slot 计算得到);

表2.2-7 transaction layer:fixed部分关键信息

长度

含义

typ

1

数据,索引

seg/obj

4

segment, object_id

csn

6

最近一次清除时的scn

itc

1

data blockITL的数量,即事务槽的个数,受参数INITRANSMAXTRANS影响

flg

1

segment空间的管理方式:

  • E ASSM
  • O Free List

fsl

1

data blockITL中,第1个空闲的位置

fnx

4

用于缓存管理

表2.2-8 transaction layer:ITL部分关键信息

长度

含义

xid

8

事务id4个字节的undo segment no+2个字节的transaction   table slot no+2个字节的transaction table slot wrap

uba

8

本事务的最近1undo记录的地址,4个字节的undo block地址+2个字节的seq number+1个字节的目录下标+1个字节的预留

flg

1

事务状态,组合标志位:

---- 事务处于活跃态

  1. -- 事务已提交,锁已被清除

--U- 事务已上界提交

---T 相对于csn时刻,事务仍然处于活跃态

lck

1

本事务在本data block中锁定的记录数(行数)

scn/fsc

6

  • 事务已提交,记录提交或上界提交时的 scn
  • 事务尚未提交,前 2 个字节记录本 data   block 对本事务来说的剩余空间 (free space credit)

现在我们再从data block的角度审视事务的运行过程,在“数据布局与空间管理”章节中,我们知道data block中与事务、数据强相关的部分为transaction layer:fixed+ITLtable directoryrow directoryrow data。当事务修改某data block中的数据时,首先要在data blocktransaction layer:ITL中占用1个新的或者复用1个已处于提交状态的事务槽位。如果是复用槽位,新事务生成的undo记录需要记录被覆盖事务在ITL中的相关(详细情况见2.2-6data block ITLITL槽位数量由initrans指定,普通场景默认2个,create table as select语句默认3个)。至此,我们接触到undo相关的第3个链表,用于按照data block维度将某个data block相关的所有事务的undo链在一起,我们将其称为链C。事务在修改data block中的实际数据时,row data中的lb会记录对应事务的槽位号,这样记录、事务、undo整个1张大网已经组建完成。

图2.2-3 undo链表关系全景图

 

如图2.2-3所示,我们得到了整个和undo相关的全景图:

  • A :存在于 undo segment header block 中,可以将 transaction control transaction table 中的事务链接在一起,方便事务申请;
  • B :存在于 undo segment 内,将本 undo segment 内的所有事务通过第 1 undo 记录链接在一起,这样即使因为 transaction control transaction table 中的事务信息发生了覆盖,所有的历史事务信息都可以通过链 B 进行回溯;
  • C :存在于 undo 记录中,用于将某个 data block 相关的所有事务的 undo 链接在一起,这样即使因为 transaction layer:ITL 数量有限,事务被频繁覆盖,所有修改过本 data block 的所有事务的 undo 记录都可以通过链 C 进行回溯(为一致性读提供了条件);
  • 对于仍然保留在 transaction layer:ITL 中的事务(活跃或已提交)则非常简单,通过 xid 可以找到 undo segment header block 中的 transaction table 中的详细信息,也可以直接通过 uba 访问到本事务的 undo 记录;

事务提交与Block清除

undo的角度来看,事务的提交需要做下列工作:

  • 工作 1 :找到 undo segment header block ,定位到 transaction table 中的对应位置,修改事务状态和提交 scn ,并记录 redo 日志;
  • 工作 2 :遍历本事务修改过的所有 data block ,在其中的 transaction layer:ITL 找到本事务,修改事务状态和提交 scn
  • 工作 3 :遍历本事务修改过的所有 data block ,在其中的 row data 部分,找到相关行并清理锁信息,记录 redo 日志;

工作1是一个常量,和事务修改的data block数量无关,且事务提交时工作1是必须实施的,这也是我们通常说的“事务提交”日志(redo日志)。

工作2的工作仅涉及transaction layer:ITL,位置相对固定,且不用记录redo日志,但又和修改的data block数量强相关,所以情况比较复杂:

  • Oracle 会在会话内存中跟踪本事务修改过的 data block (有说最多 1000 block ,有说 10% 左右的缓存大小),事务提交时这些 data block 如果还在内存中,直接修改这些 data block 中的 ITL ,将 flg 打上 C 标志,表示事务已经提交,更新 scn 为事务的提交 scn
  • 对于超出跟踪数量的 data block ,或者已经被换出缓存的 data block Oracle 为了提高 commit 速度,不再处理这些 data block ,而是采用延迟块清除策略,即将更新事务状态和 scn 的工作延迟到将来再做。当将来某个事务读到该 data block 时,发现 ITL 中事务处于活跃状态:
    • 读取对应的 undo segment header block ,发现该事务还处于 transaction table 中,且已经提交,将 ITL 中该事务的 flg 打上 C 标志,并更新事务的提交 scn
    • 读取对应的 undo segment header block 时,发现该事务所在的 transaction table 已经被覆盖,将 ITL 中该事务的 flg 打上 CU 标志,并事务的提交 scn 更新为 transaction control 中的 scn ,这就是“上界提交”,即在该 scn 时刻事务一定已经提交了,但也可能在这之间已经提交。 Oracle 通过上界提交在效率和准确度之间做了一个平衡,上界提交通过最小的代价找到尽可能老的 scn ,将来一致性读构建 CR 时,如果 scn 已经满足要求那最好,如果不满足要求再通过 undo (链 B )进一步回溯,找到更新精准的提交 scn (实际上找到满足一致性判断要求的 scn 即可);

可见对于工作2Oracle采取了综合措施,在commit效率和ITL准确度之间做了平衡。延时块清除将大量工作分解到将来的各个data block的读过程中。工作3是最耗时的,既要找到data block中的行记录(行记录会在data block内部移动),又要记录redo日志,Oracle的策略是将工作3延期分解到将来的data block读上,这也是为什么读操作有时也产生redo日志的原因。

用户回滚与重启回滚

用户回滚时,其中一个线索是通过undo segment header block中的transaction table找到该事务,再根据其中记录的dba获取本事修改的最近的data block,通过该data block中的ITL:uda获取该事务的最后1undo记录,而undo记录是链在一起的,至此可以顺利实施回滚。实际执行用户回滚更加容易,因为内存中拥有完备的跟踪信息,可以加速上述过程。

重启回滚和上述类似,不同的是不存在内存加速(重启了),而是通过扫描所有undo segment header block获得所有待回滚事务及其入口信息。

另外需要说明的是实施回滚时除了data block的修改要记录redo日志外,undo block中的undo记录也要打上“user undo applied”标签,也需要记录redo日志,防止重复回滚。

一致性读

本节仅讲述Oracle基于undo记录构建一致性读的过程(构建CR),至于背后的理论基础请参考“事务”章节。假设事务A的启动scnscn_a,事务A读取某个data block时,首先对该data block做延迟块清除。如果ITL中的事务都处于提交状态,且scn都小于scn_a则不需要构建CR,直接读取即可,否则需要构建CR

  • 步骤 1 :在缓存区中对当前 data block 做一个拷贝,即 CR
  • 步骤 2 :反向更改所有未提交事务;
  • 步骤 3 :如果有事务提交 scn 大于 scn_a ,应用该事务的 undo 日志;
  • 重复上述步骤 3 ,直至所有事务的 scn 小于等于 scn_a

因为构建CR并不是实际更新数据,所以不需要记录undoredo。至于CR版本的生命周期,Oracle综合应用了数量控制(有说1data block至多3CR)和缓存控制策略。

闪回

既然数据库内部可以通过undo记录构建CR版本实现读取历史数据的功能,该功能自然可以开放给用户直接使用,这就是闪回查询。当然还可以更进一步,对表进行闪回,即将数据回退到过去某个时间,解决用户误操作问题。闪回drop和闪回database有所不同,前者是将recyclebin中的对象重新可见,后者除了将前像放在undo tablespace中,还想日志归档一样,将前像归档到闪回日志中,用于更大规模的闪回,但基本原理是一致的。

MySQL设计原理

开启一个事务

事务的开启与undo segment强相关,但MySQL的特殊地方在于发明了rollback segmentundo log segment2个概念。rollback segment用于存放总体信息,而undo log segment用于存放具体的undo日志记录。

图2.3-1 rollback segment page结构

 

表2.3-1 ROLLBACK PAGE HEADER结构

长度

含义

TRX_RSEG_MAX_SIZE

4

rollback segment允许使用的最大page数,当前值为ULINT_MAX

TRX_RSEG_HISTORY_SIZE

4

history listundo page数量

TRX_RSEG_HISTORY

16

链表指针头,指向history list链表

TRX_RSEG_FSEG_HEADER

10

rollback segment对应的INODE Entry的地址

rollback segment只有1pagetypeFIL_PAGE_TYPE_SYS),如图2.3-1所示,rollback segment page由如下关键部分组成:

  • TRX_RSEG_UNDO_SLOTS :每个 slot 对应 1 undo log segment 1 rollback segment 可以管理 1024 undo log segment slot 本身仅占用 4 个字节,记录是 undo log segment header page page no ,没有 space id ,所以 rollback segment 及其管理的 undo log segment 必须在同一个 undo tablespace 中;
  • ROLLBACK PAGE HEADER :存放了 rollback segment 的总体信息,详细情况如表 2.3-1 所示,其中最重要的是 TRX_RSEG_HISTORY ,其为链首指针,通过和 UNDO LOG HEADER 中的 TRX_UNDO_HISTORY_NODE 组成 history list 链表,将所有归属于本 rollback segment 且已经提交的 undo log 链接在一起;

图2.3-2 rollback segment 总体布局

 

开始事务需要在rollback segment中找到1slot,那又是如何找到rollback segment的呢?如图2.3-2所示,在“空间管理与数据布局”章节中我们知道系统tablespace中有1sys trx segment,该segment中的sys trx segment page中存放了128RSEG地址,其中:

  • 1 rollback segment 预留在系统 tablespace 中;
  • 2~33 rollback segment 存放在临时 tablespace 中;
  • 34~128 rollback segment 存放在独立的 undo tablesapce 中;

如此就可以获得所有rollback segment的入口。开始事务时,MySQL默认为只读事务,当后继发生写操作时,转换为读写模式,为事务分配事务idrollback segment,分配方式如下:

  • 如果事务操作了临时表,为其分配临时表 rollback segment 2-33 ,临时表的 undo 无需记录 redo );
  • 采用 round-robin 的方式分配 rollback segment 34~128 ),不过在遍历的过程中,如果某个 rollback segment history list 过长,意味着该 rollback segment 已经非常大了,会跳过该 rollback segment ,继续遍历下一个 rollback segment

Undo Log Segment

在上一节我们知道MySQL定义了rollback segmentundo log segment两种segmentrollback segment负责管理作用,undo log segment则负责记录具体的undo日志。

图2.3-3 undo log segment header page & normal undo log page

 

undo log segment包含1header page和若干个normal pagerollback segment中的TRX_RSEG_UNDO_SLOTS记录的就是header pagepage no。如图2.3-3所示,两种page基本类似,只是header page多了1UNDO LOG SEGMENT HEADER,而normal page对应的区域为空。下面详细了解一下各个组成部分之间的关系:

  • UNDO LOG PAGE HEADER 用于描述本 page 的总体信息,详细情况见表 2.3-3 ,而 UNDO LOG SEGMENT HEADER 则描述本 segment 的总体信息,详细情况见表 2.3-2
  • MySQL 出于效率考虑,将 undo log segment 分为 TRX_UNDO_INSERT TRX_UNDO_UPDATE 。因为一旦事务提交完成, insert undo 日志就可以释放,不需要用于多版本并发控制,将 insert 区分出来,可以集中管理,高效释放;
  • 1 undo log segment 同时只能被 1 个事务使用, 1 个事务可以使用多个 undo log segment (例如事务既有 delete ,也有 insert ,就需要申请 2 undo log segment ,分别记录 delete insert undo 日志)。这样在极限情况下,系统最多支持 ((128-32)*1024)/2 个并发事务(此数据是假设所有事务都包含 insert 和非 insert 类写操作);
  • SEGMENT HEADER 中的 TRX_UNDO_PAGE_LIST PAGE HEADER 中的 TRX_UNDO_PAGE_NODE 将本 undo log segment 中所有的 undo page 链接在一起,方便管理;

表2.3-2 UNDO LOG SEGMENT HEADER结构

长度

含义

TRX_UNDO_STAT

2

undo log segment的当前状态:

  • TRX_UNDO_ACTIVE :本 segment 上的事务处于活跃态;
  • TRX_UNDO_CACHED :本 segment 上的事务已提交,本 segment 可以被复用;
  • TRX_UNDO_TO_FREE :本 segment 上的事务已提交,本 segment 可以被立刻释放;
  • TRX_UNDO_TO_PURGE :本 segment 上的事务已提交,本 segment 等待被 purge
  • TRX_UNDO_PREPARED :本 segment 上的事务提交过程中,当前处于 prepare 阶段( MySQL 需要协调 MySQL 上层和 InnoDB ,所以事务提交采用的是二阶段提交);

TRX_UNDO_LAST_LOG

2

最近1undo logheaderpage内的偏移

TRX_UNDO_FSEG_HEADER

10

记录本segment对应的INODE   Entry的地址

TRX_UNDO_PAGE_LIST

16

链表头,将所有属于本undo log segmentpage链接在一起

表2.3-3 UNDO LOG PAGE HEADER结构

长度

含义

TRX_UNDO_PAGE_TYPE

2

undo日志的类型:

  • TRX_UNDO_INSERT :针对 insert
  • TRX_UNDO_UPDATE :针对非 insert

TRX_UNDO_PAGE_START

2

page中最近1undo日志的位置

TRX_UNDO_PAGE_FREE

2

page中空闲空间的偏移量

TRX_UNDO_PAGE_NODE

12

链表的双向指针,用于将本undo log segment中的所有page链接在一起

图2.3-4 UNDO LOG结构

 

表2.3-4 UNDO LOG HEADER结构

长度

含义

TRX_UNDO_TRX_ID

8

产生本条undo log的事务id

TRX_UNDO_TRX_NO

8

仅对update undo日志有效,加入history   listUNDO LOG RECORD数量

TRX_UNDO_DEL_MARKS

2

是否有delete mark操作

TRX_UNDO_LOG_START

2

UNDO LOG RECORD开始的位置

TRX_UNDO_DICT_OPERATION

2

是否是DDL

TRX_UNDO_TABLE_ID

8

若是DDL,记录table   id

TRX_UNDO_NEXT_LOG

2

下一个undo log header的位置

TRX_UNDO_PREV_LOG

2

上一个undo log header的位置

TRX_UNDO_HISTORY_NODE

12

双向指针,用于构建history list链表

表2.3-5 INSERT型UNDO LOG RECORD结构

长度

含义

NEXT

2

本条undo log record结束的位置

TYPE_CMPL

1

日志类型,取值TRX_UNDO_INSERT_REC表示INSERT

UNDO_NO

compact

1undo log   header下各个undo log record顺序编号

TABLE_ID

compact

对应的表id

PRIMARY KEY

var

主键列的长度和取值:

primary_col1_len

primary_col1_val

primary_col2_len

primary_col2_val

... ...

primary_colN_len

primary_colN_val

START

2

本条undo log record的起始位置

表2.3-6 UPDATE型UNDO LOG RECORD结构

长度

含义

NEXT

2

本条undo log record结束的位置

TYPE_CMPL

1

日志类型及标志位

标志位:

  • UPD_NODE_NO_ORD_CHANGE :操作是否修改了二级索引的列;
  • TRX_UNDO_UPD_EXTERN :是否涉及 extern 列;

日志类型:

  • TRX_UNDO_DEL_MARK_REC :删除操作,并不实际删除,而是在原记录上打上删除标签;
  • TRX_UNDO_UPD_EXIST_REC :更新操作;
  • TRX_UNDO_UPD_DEL_REC :更新已经被删除的记录,即记录已经打上删除标签,这时再插入相同主键的记录;

UNDO_NO

compact

1undo log   header下各个undo log record顺序编号

TABLE_ID

compact

对应的表id

INFO_BITS

1

标志位

DATA_TRX_ID

compact

修改前,原记录上的事务id(隐藏列)

DATA_ROLL_PTR

compact

修改前,原记录上的回滚指针(隐藏列)

PRIMARY KEY

var

主键列的长度和取值:

primary_col1_len

primary_col1_val

primary_col2_len

primary_col2_val

... ...

primary_colN_len

primary_colN_val

UPDATE VECTOR

var

记录被更新列的前像(列位置、长度、前像):

upd_col1_pos

upd_col1_len

upd_col1_val

upd_col2_pos

upd_col2_len

upd_col2_val

.. ...

upd_colN_pos

upd_colN_len

upd_colN_val

N_BYTES_BELOW

compact

INDEX_COLUMS部分的总长度

INDEX_COLUMS

var

所有二级索引的前像(包括所有主键列和索引列)(列位置、长度、前像):

upd_col1_pos

upd_col1_len

upd_col1_val

upd_col2_pos

upd_col2_len

upd_col2_val

.. ...

upd_colN_pos

upd_colN_len

upd_colN_val

START

2

本条undo log record的起始位置

如图2.3-4所示,每条undo log1undo log header和若干条undo log record组成,每条undo log record保持数据的前像,和data block中的一条row data相对应,具体情况如下:

  • undo log header :存放本条 undo log 的总体信息,详细情况如表 2.3-4 所示,其中 TRX_UNDO_NEXT_LOG TRX_UNDO_PREV_LOG 用于将本 undo log segment header page 中的所有 undo log header 链接在一起( undo log segment 可以被事务复用),而 TRX_UNDO_HISTORY_NODE history list 链表的组成部分,用于将所有提交事务的 undo log header (不含 insert 型)链接在一起,为将来 purge 打好基础;
  • undo log record :分为 insert 型和 update 型,存放在不同的 undo log segment 中, insert 型不用于构造 MVCC ,事务提交后对应的 undo log segment 就可以立刻释放;

deleteupdate将来都需要用于构造MVCC,所以对应的undo log record不能立刻释放。原始data page中的每条记录保存有当前事务id和指向undo log record的回滚指针,这就要求删除操作不能真正的实施,否则隐藏列无处存放。undo log record还保存了该条数据记录隐藏列上的前事务id和回滚指针,这样就可以回溯到该记录的任意历史版本(只要undo log record还在)。对于二级索引来说,二级索引上的updateMySQL都是转换为delete+insert。有了这些背景之后,我们再来看MySQLundo log record记录了哪些内容:

  • delete :生成类型为 TRX_UNDO_DEL_MARK_REC undo log record ,主键索引和二级索引都不实施真正的删除,都是打上删除标签, undo log record 中需要记录主键值( PRIMARY KEY )和二级索引列值( INDEX_COLUMS );
  • update :生成类型为 TRX_UNDO_UPD_EXIST_REC undo log record ,记录主键值( PRIMARY KEY )和更新列的前像( UPDATE VECTOR ),以及二级索引相关列的前像( INDEX_COLUMS )。二级索引上的 update 实际上是 delete+insert ,所以需要记录二级索引的全部前像;
  • insert :生成类型为 TRX_UNDO_INSERT_REC undo log record ,记录主键值( PRIMARY KEY )。还有一种特殊情况,某条记录已经被打上删除标签,这时再次插入相同主键的行,这时生成类型为 TRX_UNDO_UPD_DEL_REC undo log record ,记录主键值( PRIMARY KEY )和更新列的前像( UPDATE VECTOR ),以及二级索引相关列的前像( INDEX_COLUMS );

图2.3-5 UNDO全景图

 

最后我再回顾总结一下undo的全貌,如图2.3-5所示:

  • 每个 rollback segment 1 header page ,每个 undo log segment 1 header page 和若干个 normal page
  • 每个事务可以有多个 undo log ,但单个 undo log 只能属于 1 个事务。对于某个具体的 undo log segment 来说, 1 个事务只有 1 undo log
  • 1 undo log segment 同时只能属于 1 个事务,但当 undo log segment 仅使用了 header page ,且该 header page 至少有 1/4 空闲空间时,该 undo log segment 在上一个事务提交后可以被下一个事务复用,所以有时 header page 中有多个 undo log header
  • A :每个 rollback segment 通过 1024 slots 可同时管理 1024 个活跃态的 undo log segment
  • B UNDO LOG SEGMENT HEADER 中的 TRX_UNDO_PAGE_LIST 结合 UNDO LOG PAGE HEADER 中的 TRX_UNDO_PAGE_NODE ,将本 undo log segment 中的所有 undo page 链接在一起;
  • C UNDO LOG HEADER 中的 TRX_UNDO_LOG_START 结合 UNDO LOG RECORD 中的 NEXT START ,将本 undo log 中的所有 undo log record 链接在一起;
  • D UNDO LOG SEGMENT HEADER 中的 TRX_UNDO_LAST_LOG 结合 UNDO LOG HEADER 中的 TRX_UNDO_NEXT_LOG TRX_UNDO_PREV_LOG ,将本 undo log segment 中的所有 undo log 链接在一起;
  • E ROLLBACK PAGE HEADER 中的 TRX_RSEG_HISTORY 结合 UNDO LOG HEADER 中的 TRX_UNDO_HISTORY_NODE ,将所有已经提交的 undo log 链接在一起(不含 insert cached );
  • F user records 中的 DATA_ROLL_PTR 隐藏列结合 undo log record 中的 DATA_ROLL_PTR ,将和某条记录相关的所有 undo log record 链接在一起,为 MVCC 打下基础;

事务提交

MySQLInnoDB是松耦合结构,为了协调上下层的提交一致性,采用了2阶段提交。不过从undo的角度来看,prepare阶段的工作不多,但还是要将UNDO LOG SEGMENT HEADER中的TRX_UNDO_STATTRX_UNDO_ACTIVE改为TRX_UNDO_PREPARED,并更新其它一些信息。重点工作发生在commit阶段:

  • 如果是 insert undo log segment ,将 TRX_UNDO_STAT 设置为 TRX_UNDO_TO_FREE ,后继直接释放;
  • 如果是 update undo log segment ,只有 1 page page 的空闲空间至少有 1/4 ,将 TRX_UNDO_STAT 设置为 TRX_UNDO_CACHED ,并将本 undo log segment 加入到 undo cache list 中,以供将来复用( undo cache list 是内存结构);
  • 如果是 update undo log segment ,不满足只有 1 page page 的空闲空间至少有 1/4 ,将 TRX_UNDO_STAT 设置为 TRX_UNDO_TO_PURGE ,同时将 rollback segment 中对应的 slot 置为 FIL_NULL ,并将本 undo log segment undo log header )加入到 TRX_UNDO_HISTORY_NODE 中(链 E ),等待被清理(不再用于 MVCC 后即可被清理);

用户回滚与重启回滚

当用户主动或者数据库内部原因对事务发起回滚时,内存中保存有相关对象和指针,直接进行逆向操作即可,主要包括如下动作:

  • 二级索引对主键索引有依赖关系,所以先回滚二级索引,再回滚主键索引;
  • 对于打上删除标记的记录,清理删除标记;
  • 对于更新操作,将数据回退到前像版本;
  • 对于插入操作,直接删除记录;

重启回滚涉及的动作和用户回滚类似,但重启内存丢失,需要根据以下原则找到回滚记录:

  • 遍历所有 rollback segment ,根据每个 rollback segment 中的 slot 遍历 undo log segment
  • 如果 undo log segment TRX_UNDO_STAT TRX_UNDO_ACTIVE ,表示需要回滚,按照类似用户回滚的动作进行回滚;
  • 如果 undo log segment TRX_UNDO_STAT TRX_UNDO_PREPARED ,比较事务的 XID binlog ,如果 binlog 已经完成,则走提交流程,否则走回滚流程;

一致性读

RR隔离级别下一致性读的原则:事务开始时所有已经提交的事务对本事务来说都是可见的,所有未提交以及将来启动的事务对本事务来说都是不可见的。为此,开启事务时需要根据当前系统的活跃事务情况构建一个属于本事务的ReadView,包含如下要素:

  • up_limit_id :最小活跃事务 id ,比该 id 小的事务对本事务来说都是可见的;
  • low_limit_id :最大事务 id ,比该事务 id 大的事务都是在本事务开启之后生成的,所以对本事务来说都是不可见的;
  • trx_ids trx_ids 为本事务开启时系统活跃事务列表的快照,对于 up_limit_id low_limit_id 之间的所有事务,如果在 trx_ids 中表示本事务开启时该事务处于活跃态,尚未提交,所以不可见,否则可见;

图2.3-6 可见性判断

 

数据记录中有事务id和回滚指针的隐藏列,而每条undo log record中也有上1个事务id和指向上1undo log record的指针,通过这些事务id和回滚指针,按照图2.3-6中的原则就可以进行无锁的一致性读。在RC场景下,原则和RR场景基本一致,不同的是RR在事务开始时建立ReadView,而RC在每条SQL语句开始前都要建立一个新的ReadView

Purge

一致性读机制不再读到的这些历史版本就需要及时被清理掉,以便及时释放出undo空间。为此,获得系统中最老的ReadView,比该ReadViewup_limit_id小的事务对应的undo log都可以被清理掉。方法是遍历各个rollback segmentTRX_RSEG_HISTORY链表,链表中事务id小于up_limit_idundo log都可以被清理掉。清理的动作主要包括2个方面:

  • 清理记录:清理二级索引和主键索引中删除标记为真的记录;
  • 清理 undo :清理 undo log undo log segment

PostgreSQL设计原理

Page与前像设计

图2.4-1 page结构

 

表2.4-1 PageHeaderData结构

长度

含义

pd_lsn

8

page被修改的lsn

pd_checksum

2

校验值,检查page异常

pd_flags

2

Page状态,组合标志位:

  • PD_HAS_FREE_LINES(0x0001) ItemIdData 中有未使用的指针;
  • PD_PAGE_FULL(0x0002) page 满;
  • PD_ALL_VISIBLE(0x0004) :本 page 中所有记录都可见;

pd_lower

2

Free space的起始位置

pd_upper

2

Free space的结束位置

pd_special

2

Special space的位置

pd_pagesize_version

2

高位字节表示page大小,低位字节表示版本号

pd_prune_xid

4

page最老的事务idvacuum时使用

表2.4-2 ItemIdData结构

长度

含义

lp_off

15bit

记录在Items中的位置

lp_flag

2bit

Item状态:

  • LP_UNUSED(0) :未使用;
  • LP_NORMAL(1) :已使用;
  • LP_REDIRECT(2) HOT 重定向;
  • LP_DEAD(3) Dead 记录;

lp_len

15bit

Items中的长度

PostgreSQL采用异地更新方案,前像没有存放在独立的undo区域中,而是和数据一起存放在data page中。因此,在研究PostgreSQL回滚前,有必要先了解一下page结构。如图2.4-1所示,page由如下部分组成:

  • PageHeaderData :头部,定义了整个 page 的总体信息,详细情况见表 2.4-1
  • ItemIdData :目录信息,每行记录占 1 个位置,其下标是后继定位该记录的重要组成部分,详细情况见表 2.4-2 ,随着记录的增加,目录从上向下增加;
  • Items :用户数据,随着记录的增加,数据从下向上增加;
  • Special Space :存放和访问方法相关的数据,如对于 BTree 索引存放左右兄弟节点的指针,普通表 page 为空;

图2.4-2 Item结构

 

表2.4-3 HeapTupleHeaderData结构

长度

含义

t_xmin

4

insert本记录的事务id

t_xmax

4

delete/update本记录的事务id

t_cid/t_xvac

4

Insert/delete时的事务id和命令id

t_ctid

6

本记录下一个版本的地址,如果自己是最新版本则指向自己

t_infomask2

2

11位表示当前记录的列数;

其它位为标志位:

  • HEAP_KEYS_UPDATED(0x2000) :被更新或删除;
  • HEAP_HOT_UPDATED(0x4000) HOT-updated
  • HEAP_ONLY_TUPLE(0x8000) heap only tuple
  • HEAP_XACT_MASK(0xE000) :用于可见性判断;

t_infomask

2

组合标志位:

  • HEAP_HASNULL(0x0001) :有 NullBitmap 域;
  • HEAP_HASVARWIDTH(0x0002) :有变长列;
  • HEAP_HASEXTERNAL(0x0004) :有外部存储列;
  • HEAP_HASOID_OLD(0x0008) :有 object id 域;
  • HEAP_XMAX_KEYSHR_LOCK(0x0010) t_xmax 状态;
  • HEAP_COMBOCID(0x0020) t_cid combo id
  • HEAP_XMAX_EXCL_LOCK(0x0040) t_xmax 状态;
  • HEAP_XMAX_LOCK_ONLY(0x0080) t_xmax 状态;
  • HEAP_XMIN_COMMITTED(0x0100) t_xmin 状态;
  • HEAP_XMIN_INVALID(0x0200) t_xmin 状态;
  • HEAP_XMAX_COMMITTED(0x0400) t_xmax 状态;
  • HEAP_XMAX_INVALID(0x0800) t_xmax 状态;
  • HEAP_XMAX_IS_MULTI(0x1000) MultiXactId
  • HEAP_UPDATED(0x2000) Updated Version

t_hoff

1

记录头大小

Item存放一条具体的记录,如图2.4-2所示,ItemHeapTupleHeaderDataNullBitmapObjectIdUserData 4个部分组成:

  • HeapTupleHeaderData :记录头,固定大小, t_xmin t_xmax 用于判断记录在事务间的可见性判断, t_cid 用于记录在本事务内的可见性判断, t_ctid 用于将记录的多个版本链接在一起,详细情况见表 2.4-3
  • NullBitmap :可选, t_infomask HEAP_HASNULL 标志位为真时有此字段,用于表达相关列是否为 null
  • ObjectId :可选, t_infomask HEAP_HASOID_OLD 标志位为真时有此字段;
  • UserData :真正的用户数据;

图2.4-3 前像设计示例

 

了解了PostgreSQLpageitem设计,下面来看PostgreSQL是如何进行异地更新的,如图2.4-3所示,DML主要包括下面3种情况:

  • Insert insert 直接插入数据,不需要记录前像, t_xmin 记录插入本记录的事务 id
  • Delete delete 不进行实际删除,记录本身就是前像, t_xmax 记录删除本记录的事务 id
  • Update :不进行本地更新,而是克隆 1 个新记录,然后在新纪录上更新(实际上是 delete+insert ),旧记录就是前像,并将旧记录的 t_ctid 指向新记录的位置,从而形成一个旧记录指向新记录的链表;

可见PostgreSQL通过上述方法保存了前像,同时通过clog、记录中的ItemIdData.lp_flag、记录中的t_infomask保存相关事务的状态,这样就可以合理地应用保存的前像。当然记录中的ItemIdData.lp_flagt_infomask的状态可能不是最新的,需要后台根据clog中的真实状态进行刷新。

用户回滚

PostgreSQL采用异地更新的最大优势就是回滚。由于前像仍然以记录的方式存在于数据当中,所以用户回滚只需要记录事务的状态,释放相关资源即可,非常高效。至于遗留下来的垃圾数据,通过后台进程逐步清理即可。

一致性读

MySQL类似,PostgreSQL也采用基于事务id的一致性读,该方法需要获取当前的活跃事务id列表,PostgreSQL将其称为transaction snapshotRR或者SERIALIZABLE隔离级别下,事务在启动时会生成本事务的transaction snapshotRC隔离级别下,每条SQL语句执行前生成最新的transaction snapshot

由于PostgreSQL采用的是异地更新,一致性判断的规则更加复杂,需要综合判断事务状态、t_xmint_xmaxtransaction snapshot,具体规则如下:

  • 类别 1 t_xmin 的事务状态为 ABORTED

插入此条记录的事务已经回滚,所以本记录对任何事务都不可见。

  • 类别 2 t_xmin 的事务状态为 IN_PROGRESS

R1: t_xmin=current_txid & t_xmax=INVALID -> visible

R2: t_xmin=current_txid & t_xmax!=INVALID -> invisible

R3: t_xmin!=current_txid -> invisible

R1表示当前事务读取本事务之前插入的数据,所以可见。R2表示该记录虽然是本事务插入的,但已被本事务删除,所以不可见。R3表示当前事务读取其它事务插入的记录,且其它事务尚未提交,所以不可见。

  • 类别 3 t_xmin 的事务状态为 COMMITTED

R4: snapshot(t_xmin)=active -> invisible

R5: t_xmax=INVALID | status(t_xmax)=ABORTED -> visible

R6: status(t_xmax)=IN_PROGRESS & t_xmax=current_txid -> invisible

R7: status(t_xmax)=IN_PROGRESS & t_xmax!=current_txid -> visible

R8: status(t_xmax)=COMMITTED & snapshot(t_xmax)=active -> visible

R9: status(t_xmax)=COMMITTED & snapshot(t_xmax)!=active -> invisible

判断规则是R4R9依次判断,一旦满足条件就以此结果为依据。R4表示插入这条记录的事务虽然已经提交了,但该事务仍然在snapshot中,所以不可见。R5表示没有其它事务更新过该记录或者更新过该记录的事务已经回滚,所以可见。R6表示本事务已经更新过该记录,所以该记录的上一个版本不可见。R7表示其它事务正在更新该记录,所以该记录的上一个版本可见。R8表示更新或删除该记录的事务已经提交,但该事务在snapshot中,所以可见。R9表示更新或删除该记录的事务已经提交,但该事务不在snapshot中,需要看更新的版本,所以不可见。

HOTIndex-Only Scan

图2.4-4 索引与记录关系示意图(非HOT)

 

如图2.4-4所示,由于记录采用了异地更新,当记录生成一个新的版本时,为了保持同步,索引项不管有没有发生变化,都需要生成一个对应的新版本,和相应的数据记录对应起来。这导致了索引不必要的浪费,并为后继的vacuum带来了额外的负担。

图2.4-5 索引与记录关系示意图(HOT)

 

图2.4-6 索引与记录关系示意图(HOT, After Prune)

 

图2.4-7 索引与记录关系示意图(HOT, After Defragmentation)

 

HOT指当更新同时满足:1)新版本的记录和旧版本的据在同一个page内;2)更新的列不涉及索引列时,索引不再生成新版本,从而解决索引成本过高的问题。上述条件2很容易理解,条件1的主要目的是平衡读性能,否则通过索引访问很可能需要2page调度,即先通过索引定位到旧版本,然后再通过旧版本的c_ctid或者ItemIdData中重定向定位到新版本。

如图2.4-5所示,HOT生效时索引仍然指向旧记录,新记录通过旧记录的t_ctid访问,同时旧记录的t_infomask2打上HEAP_HOT_UPDATED标志,新记录的t_infomask2打上HEAP_ONLY_TUPLE标志。当旧记录的状态为dead后,图2.4-6和图2.4-7分别给出修剪(prune)和去碎片化(defragmentation)实施后的情况,旧记录的ItemIdData保留,并重定向到记录的ItemIdData上。

Index-Only Scan指索引中已包含查询所需所有列,原则上直接扫描或定位索引page,无需扫描或定位数据page即可完成本次查询。PostgreSQL的一致性判断规则复杂,且依赖于数据page中各记录的t_xmint_xmax及其状态信息,从而导致Index-Only Scan无法实施。为此,PostgreSQL采用了一个简单直接的办法,在后台维护一个visibility map,一旦确定某个数据page中的所有记录都是可见的,就将该page加入到visibility map中,直接对外可见。这样扫描索引时,如果发现对应的数据pagevisibility map中就仅扫描索引,不再访问数据page,否则仍然访问数据page,根据记录的t_xmint_xmax及其相应状态进行可见性判断。

Vacuum

从前面几个小节我们知道数据page和索引page都会有过期数据(dead tuple),vacuum的工作主要就是清理这些过期数据。vacuum分为concurrent vacuumfull vacuum,前者允许vacuum期间并发执行其它只读事务,后者不允许vacuum期间执行其它事务,但两者的实现原理是一致的。

concurrent vacuum的主要工作是针对某个特定的table或所有table执行如下任务:

  • 清理 dead tuple ,即删除无用的数据前像:
    • 删除数据 page 中的 dead tuple ,并对数据 page 做去碎片化整理;
    • 删除索引 page 中指向 dead tuple 的索引记录;
  • 冻结过期 txids PostgreSQL txid 只有 4 个字节,需要及时清理,以解决循环使用的问题:
    • 冻结过期的 txids
    • 更新系统 catalogs
    • 清理不必要的 clog
  • 其它任务:
    • 更新 FSM VM
    • 更新系统统计项;

concurrent vacuum的大致实现过程如下:

  • step1 :从 table list 中获取某个待实施的 table
  • step2 :针对该 table 申请 UpdateExclusiveLock ,该锁仍然允许其它事务读本 table
  • step3 :扫描本 table 中所有数据 page ,识别出所有 dead tuple ,必要时冻结过期 txids
  • step4 :删除指向 dead tuple 的所有索引记录;
  • step5 :针对本 table 的每个数据 page ,重复 step6 step7
  • step6 :删除 dead tuple ,并做去碎片化工作;
  • step7 :更新 FSM VM
  • step8 :如果数据 page 中不再含有任何 tuple ,释放该数据 page
  • step9 :更新统计信息和 catalogs
  • step10 :清理不必要的 clog page clog 文件;

其中第3步是将扫描的结果存放在内存中(maintenance_work_mem),如果内存已满则开始实施第4步到第8步,然后清理内存,并回到第3步从上次断点开始继续扫描。

总结与分析

首先来看记录前像对正常写操作的影响,下面从DML操作类型和索引的角度来分析:

  • insert Oracle MySQL 都会生成 1 undo 记录, PostgreSQL 无需做额外处理;
  • delete Oracle MySQL 都会生成 1 undo 记录, PostgreSQL 只需设置相关状态;
  • update Oracle MySQL 会生成 1 undo 记录,仅记录更改列的前像, PostgreSQL 需要完整克隆 1 条新记录,并更改旧记录的状态;
  • 索引: Oracle 将索引当数据,数据与索引的处理方式一致。 MySQL 的索引不记 undo ,而是通过数据的 undo 间接记录索引的变化。 PostgreSQL 的索引页采用异地更新,但不记录事务状态,同时 HOT 方式进行一定程度的优化。当然对于索引来说,索引列的更新涉及索引记录在 BTree 中位置的调整,所以 Oracle MySQL 都是将索引的 update 转换为 delete+insert ,即异地更新方案;

总的来说OracleMySQLPostgreSQL相差不大,insertdelete操作PostgreSQL更加轻量,而update操作OracleMySQL相对较为轻量。但Oracle在设计上相对更加简洁,索引、数据的处理原则基本一致,没有太多的特殊情况要考虑。

其次看回滚效率,PostgreSQL是最高效的。PostgreSQL的前像就再数据之中,只需要记录事务的状态,不需要实施回滚动作。OracleMySQL则厚重得多,需要从undo日志中读出前像,并应用到实际数据和索引上,同时还要记录重做日志。

再次看存储效率,我们还是从DML操作类型、数据和索引这几个角度来看:

  • insert Oracle 仅记录 RowID MySQL 记录主键值, PostgreSQL 不需要做额外记录;
  • delete Oracle 需要记录整条记录的前像, MySQL 记录主键值, PostgreSQL 不需要做额外记录;
  • update Oracle 需要记录被更新的列, MySQL 记录主键值和被更新的列, PostgreSQL 需要拷贝一个完整的新行(所有列);
  • 记录: Oracle 的记录不需要做太多特殊处理, MySQL 需要引入 13 个字节的事务 id 和回滚指针, PostgreSQL 需要引入 22 个字节的 t_xmin t_xmax t_cid t_ctid t_infomask t_infomask2
  • 索引: Oracle 的索引处理等同于数据, MySQL 需要将索引的所有列记录在数据的 undo 中,并将 update 转换为 insert+delete PostgreSQL 需要生成新的索引记录;

从数据的前像来看,对于insertdelete操作,PostgreSQL基本不消耗存储空间;对于update操作,OracleMySQL优于PostgreSQL,因为前者仅需要记录被更新列的前像。对于delete操作,OracleMySQL都是在原记录上打上删除标签,但Oracle会在undo中记录整条记录的前像,而MySQL仅记录主键值。虽然MySQL更省空间,但Oracleundo和数据block清理的工作完全解耦,没有依赖关系,且为将来的闪回提供了可能。从对记录的影响来看,Oracle是最优的,MySQL其次,PostgreSQL最后,虽然单条记录占用的字节数不是太多,但对于大数据量的窄表(表的列数很少,但记录非常庞大)来说,占用的空间会比较可观。

再再次看回收效率。Oracleundo segment中维护retention table,直接按照时间将相关extent失效即可,不需要做太多的回收动作,block上的事务状态主要集中在ITL上,调整面也比较小。MySQL相对复杂一点,通过rollback segmentundo log segment进行日志管理,1undo log segment同时只能服务于1个事务,segment的管理成本比Oracle高,但通过history list进行回收,回收涉及的undo page也相对比较集中。Page上基本不涉及事务状态,调整面最小。PostgreSQL的前像和数据存放在一起,需要通过扫描所有page确认dead tuple,成本非常高,事务状态涉及每条记录,调整面也非常大。

最后看一致性读和闪回。Oracle通过undo日志和scn进行一致性判断,简洁高效。MySQLPostgreSQL分别需要维护read viewtransaction snapshot,高并发时事务列表会很长,活跃事务列表的比较效率也较低,且每个事务都需要维护一个read viewtransaction snapshotRC隔离级别下每执行一次SQL还需要更新一次。虽然MySQLPostgreSQL原理相似,但由于PostgreSQL涉及t_xmint_xmaxt_maskinfotransaction snapshotclog,判断规则极其复杂,效率最低。在扫描数据和索引时,涉及多个版本(关键看vacuum的频率),虽然VM可以有一定程度的缓解,但效率要低于OracleMySQL

原则上只要前像存在,就可以提供闪回功能。Oracle在系统中维护有逻辑时间(scn),且可以和墙上时间转换,事务提交时记录当时的scn,这样就可以基于scn和墙上时间进行闪回查询和闪回恢复。MySQLPostgreSQL当前还没有提供闪回功能,提供该功能的挑战在于:1)可以基于LSN进行闪回回退,但需要对当前的前像回收机制进行较大幅度的调整;2)当前的可见性判断机制是基于read viewtransaction snapshot的,该机制无法复用到闪回查询,因为用户并不知道历史某个时刻的read viewtransaction snapshot

PDF下载地址:http://blog.itpub.net/69912723/viewspace-2717309/

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
6月前
|
数据库
数据库设计过程
数据库设计过程
35 0
|
5月前
|
存储 数据库连接 数据库
逆向学习数据库篇:表设计和数据库操作的核心概念与流程
逆向学习数据库篇:表设计和数据库操作的核心概念与流程
33 0
|
JavaScript
Selenium最全超时等待问题的处理方案
Selenium广泛应用于自动化测试和自动化业务开发,同时在网络爬虫中也有较多的应用,使用Selenium有两个核心的问题:第一个是如何在爬虫领域不被识别出来,另一个是在自动化领域如何解决超时加载的问题。
3628 0
|
SQL 存储 缓存
【数据库设计与实现】第二章:数据前像与回滚
数据前像与回滚设计原则事务中重要的一点是事务的原子性,即事务中的所有操作要么全部生效,要么全部都不生效。数据的前像和回滚是保证事务原子性的重要手段。修改记录前,先将记录的前像(即修改前的记录)保存下来,当事务需要回滚时,将记录恢复到前像,从而保证事务的原子性。从当前的实现情况来看,前像保存主要有两种技术手段:本地更新方案:直接在原记录上进行更新,把被更新列的前像保存到独立的undo区域;异地更新方
【数据库设计与实现】第二章:数据前像与回滚
|
存储 SQL 缓存
【数据库设计与实现】第三章:数据后像与前滚
数据后像与前滚设计原则事务的持久性要求事务提交时本次事务的修改必须完成持久化工作,而事务修改的block或page在大部分场景下并不是连续的,在持久化设备上表现为大量的随机IO。通过记录后像,可以将随机IO转换为对持久化设备更为有利的顺序IO,并将dirty block或dirty page(指被修改过但尚未完成持久化的block或page)的多次修改合并,节约block或page的持久化次数。后
【数据库设计与实现】第三章:数据后像与前滚
|
6月前
WordPress插件:链接自动识别转为超链接
WordPress插件:链接自动识别转为超链接
84 3
|
SQL 存储 关系型数据库
Mysql数据库基础第五章:(一)事务
TCL:Transaction Control Language 事务控制语言
|
6月前
|
前端开发
前端 TS 快速入门之六:枚举 enum
前端 TS 快速入门之六:枚举 enum
124 0
|
数据可视化 数据处理 Python
使用Python绘制多个股票的K线图
使用Python绘制多个股票的K线图
|
存储 数据建模 程序员
【数据库】数据库设计之数据库设计过程(二)
数据库设计之数据库设计过程(二)
155 0
【数据库】数据库设计之数据库设计过程(二)