关于存储:如何实现事务原子性PolarDB原子性深度剖析

38次阅读

共计 12304 个字符,预计需要花费 31 分钟才能阅读完成。

简介:在巍峨的数据库大厦体系中,查问优化器和事务体系是两堵重要的承重墙,二者是如此重要以至于整个数据库体系结构设计中大量的数据结构、机制和个性都是围绕着二者搭建起来的。他们一个负责如何更快的查问到数据,更无效的组织起底层数据体系;一个负责平安、稳固、长久的存储数据,为用户的读写并发提供逻辑实现。咱们明天摸索的主题是事务体系,然而事务体系太过宏大,咱们须要分成若干次的内容。本文就针对 PolarDB 事务体系中的原子性进行分析。

作者 | 佑熙
起源 | 阿里技术公众号

一 前言

在巍峨的数据库大厦体系中,查问优化器和事务体系是两堵重要的承重墙,二者是如此重要以至于整个数据库体系结构设计中大量的数据结构、机制和个性都是围绕着二者搭建起来的。他们一个负责如何更快的查问到数据,更无效的组织起底层数据体系;一个负责平安、稳固、长久的存储数据,为用户的读写并发提供逻辑实现。咱们明天摸索的主题是事务体系,然而事务体系太过宏大,咱们须要分成若干次的内容。本文就针对 PolarDB 事务体系中的原子性进行分析。

二 问题

在浏览本文之前,首先提出几个重要的问题,这几个问题或者在接触数据库之前你也已经纳闷过。然而已经这些问题的答案可能只是简略的被诸如“预写日志”,“解体复原机制”等简略的答案答复过了,本文心愿可能更深一步的探讨这些机制的实现及外在原理。

  • 数据库原子性到底是如何保障的?应用了哪些非凡的数据结构?为什么要用?
  • 为什么我写入胜利的数据可能被保障不失落?
  • 为什么数据库解体后能够残缺的复原进去逻辑上我曾经提交的数据?
  • 更进一步,什么是逻辑上已提交的数据?哪一个步骤才算是真正的提交?

三 背景

1 原子性在 ACID 中的地位

赫赫有名的 ACID 个性被提出后这个概念一直的被援用(最后被写入 SQL92 规范),这四种个性能够大略概括出人们心中对于数据库最外围的诉求。本文要讲的原子性便是其中第一个个性,咱们先关注原子性在事务 ACID 中的地位。

这是集体对于数据库 ACID 个性关系的了解,我认为数据库 ACID 个性其实能够分为两个视角去定义,其中 AID(原子、长久、隔离)个性是从事务自身的视角去定义,而 C(统一)个性是从用户的视角去定义。上面我会别离谈下本人的了解。

  • 原子性:咱们还是从这些个性的概念登程去探讨,原子性的概念是一个事务要么执行胜利,要么执行失败,即 All or nothing。这种特质咱们能够用一个最小的事务模型去定义进去,咱们假如有一个事务,咱们通过一套机制可能实现它真正的提交或回滚,这个目标就达成了,用户只是通过咱们的零碎进行了一次提交,而原子性的重心不在于事务胜利或失败自身;而是保障了事务体系只承受胜利或失败两种状态,而且有相应的策略来保障胜利或失败的物理后果和逻辑后果是统一的。原子性能够通过最小事务单元的个性定义进去,是整个事务体系的基石。
  • 持久性:而持久性指的是事务一旦提交后就能够永恒的保留在数据库中。持久性的范畴与视角简直与原子性是统一的,其实也导致了二者在概念和实现上也是严密相连的。二者都肯定意义上保障了数据的统一和可恢复性,而界线便是事务提交的时刻。举例来说,一个数据目前的状态是 T,如果某个事务 A 试图将状态更新到 T +1,如果这个事务 A 失败了,那么数据库状态回到 T,这是原子性保障的;如果事务 A 提交胜利了,那么事务状态变成 T + 1 的那一刻,这个是原子性保障的;而一旦事务状态变成 T + 1 且事务胜利提交,事务曾经完结不再存在原子性,这个 T + 1 的状态就是由持久性负责保障。从这个角度能够推断原子性保障了事务提交前数据的解体复原,而持久性保障了事务提交后的解体复原。
  • 隔离性:隔离性同样是定义在事务层面的一个机制,给事务并发提供了某种程度的隔离保障。隔离性的实质是避免事务并发会导致不统一的状态。因为不是本文的重点这里不做详述。
  • 一致性:相较于其余几个个性很非凡,一致性的概念是数据库在通过一个或多个事务后,数据库必须放弃在一致性的状态。如果从事务的角度去了解,保障了 AID 就能够保障事务是可串行、可复原、原子性的,然而这种事务状态的一致性就是真正的一致性吗?毁坏了 AID 就肯定毁坏 C,然而反之 AID 都保障了 C 肯定会被保障吗?如果答案是是的话那这个概念就会失去它的意义。咱们能够保障 AID 来保障事务是统一的,然而是否可能证实事务的统一肯定保证数据的统一呢?另外数据统一这个概念通过事务很难去精确定义,而如果通过用户层面就很好定义。数据统一就是用户认为数据库中数据任何时候的状态是满足其业务逻辑的。比方银行存款不能是正数,所以用户定义了一个非负束缚。我认为这是概念设计者的一个留白,偏向于将一致性视为一种高阶指标。

本文次要还是围绕原子性进行,而两头波及到解体复原的话题可能会波及到持久性。隔离性和一致性本文不探讨,在可见性的局部咱们默认数据库具备实现的隔离性,即可串行化的隔离级别。

2 原子性的外在要求

下面讲了很多对于数据库事务个性的了解,上面进入咱们的主题原子性。咱们还是须要拿方才的例子来持续论述原子性。目前数据库的状态是 T,当初心愿通过一个事务 A 将数据状态降级为 T +1。咱们探讨这个过程的原子性。

如果咱们要保障这个事务是原子的,那么咱们能够定义三个要求,只有满足了下者,才能够说这个事务是原子性的:

  • 数据库存在一个事务真正胜利提交的工夫点。
  • 在这个工夫点之前开启的事务(或者获取的快照)只应该看到 T 状态,这个工夫点之后开启的事务(或者获取的快照),只应该看到 T + 1 状态。
  • 在这个工夫点之前任何时候的解体,数据库都应该可能回到 T 状态;在这个工夫点之后任何时候解体,数据库都应该能回到 T + 1 状态。

留神这个工夫点咱们并没有定义进去,甚至咱们都不能确定 2 / 3 中的这个工夫点是不是同一个工夫点。咱们能确定的是这个工夫点肯定存在,否则就没方法说事务是原子性的,原子性确定了提交 / 回滚必须有一个确定的工夫点。另外依据咱们方才的形容,能够揣测出 2 中的工夫点,咱们能够定义为原子性位点。因为原子性位点之前的提交咱们不可见,之后可见,那么这个原子性位点对于数据库中其余事务来说就是该事务提交的工夫点;而 3 中的位点能够定位为持久性位点,因为这合乎持久性对于解体复原的定义。即对于持久性来说,3 这个位点后事务曾经提交了。

四 原子性计划探讨

1 从两种简略的计划说起

首先咱们从两个简略的计划来谈起原子性,这一步的目标是试图阐明为什么咱们接下来每一步介绍的数据结构都是为了实现原子性必不可少的。

简略 Direct IO

构想咱们存在这样一个数据库,每次用户操作都会把数据写到磁盘中。咱们把这种形式叫做简略 Direct IO,简略的意思是指咱们没有记录任何数据日志而只记录了数据自身。假如初始的数据版本是 T,这样当咱们插入了一些数据之后如果产生了数据解体,磁盘上会写着一个 T +0.5 版本的数据页,并且咱们没有任何方法去回滚或持续进行后续的操作。这样失败的 CASE 无疑突破了原子性,因为目前的状态既不是提交也不是回滚而是一个介于两头的状态,所以这是一次失败的尝试。

简略 Buffer IO

接下来咱们有了一种新的计划,这种计划叫做简略 Buffer IO。同样咱们没有日志,然而咱们退出了一个新的数据结构叫做“共享缓存池”。这样当咱们每次写数据页的时候并不是间接把数据写到数据库上,而是写到了 shared buffer pool 中;这样会有不言而喻的劣势,首先读写效率会大大的进步,咱们每次写都不用期待数据页实在的写入磁盘,而能够异步的进行;其次如果数据库在事务未提交前回滚或者解体掉了,咱们只须要抛弃掉 shared buffer pool 中的数据,只有当数据库胜利提交时,它才能够真正的把数据刷到磁盘上,这样从可见性和解体恢复性上看,咱们看似曾经满足了要求。

然而上述计划还是有一个难以解决的问题,即数据落盘这件事并不像咱们设想的这么简略。比方 shared buffer pool 中有 10 个脏页,咱们能够通过存储技术来保障单个页面的刷盘是原子的,然而在这 10 个页面的两头任何时候数据库都可能解体。继而不管咱们何时决定数据落盘,只有数据落盘的过程中机器产生了解体,这个数据都可能在磁盘上产生一个 T +0.5 的版本,并且咱们在重启后还是没方法去重做或者回滚。

下面两个例子的论述仿佛注定了数据库没有方法通过不依赖其余构造的状况下保证数据的一致性(还有一种风行的计划是 SQLite 数据库的 Shadow Paging 技术,这里不探讨),所以如果想解决这些问题,咱们须要引入下一个重要的数据结构,数据日志。

2 预写日志 + Buffer IO 计划

计划总览

咱们在 Buffer IO 的根底上引入了数据日志这样的数据结构,用来解决数据不统一的问题。

在数据缓存的局部与之前的想法一样,不同的是咱们在写数据之前会额定记录一个 xlog buffer。这些 xlog buffer 是一个有序列的日志,他的序列号被称为 lsn,咱们会把这个数据对应的日志 lsn 记录在数据页面上。每一个数据页页面都记录了更新它最新的日志序号。这一个性是为了保障日志与数据的一致性。

构想一下,如果咱们可能引入的日志与数据版本是完全一致的,并且保证数据日志先于日志长久化,那么不管何时数据解体咱们都能够通过这个统一的日志页复原进去。这样就能够解决之前说的数据解体问题。不管事务提交前或者提交后解体,咱们都能够通过回放日志的计划来回放出正确的数据版本,这样就能够实现解体复原的原子性。另外对于可见性的局部咱们能够通过多版本快照的形式实现。保证数据日志和数据统一并不容易,上面咱们具体讲下如何保障,还有解体时数据如何复原。

事务提交与管制刷脏

WAL 日志被设计进去的目标是为了保证数据的可恢复性,而为了保障 WAL 日志与数据的一致性,当数据缓存被长久化到磁盘时,长久化的数据页对应的 WAL 日志必须先一步被长久化到磁盘中,这句话论述了管制刷脏的实质含意。

  1. 数据库后盾存在这样一个过程叫做 checkpoint 过程,其周期性的进行 checkpoint 操作。当 checkpoint 产生的时候,它会向 xlog 日志中写入一条 checkpoint 日志,这条 checkpoint 日志蕴含了以后的 REDO 位点。checkpoint 保障了以后所有脏数据曾经被刷到了磁盘当中。
  2. 进行第一次插入操作,此时共享内存找不到这个页面,它会把这个页面从磁盘加载到共享内存中,之后写入本次插入的输出,并且插入一条写数据的 xlog 到 xlog buffer 中,将这个表的日志标记从 LSN0 降级到 LSN1。
  3. 在事务提交的时刻,事务会写入一条事务提交日志,之后 wal buffer pool 上所有本次事务提交的 WAL 日志会一并被刷到磁盘上。
  4. 之后插入第二条数据 B,他会插入一条写数据的 xlog 到 xlog buffer 中,将这个表的日志标记从 LSN1 降级到 LSN2。
  5. 同 3 一样的操作。

之后如果数据库失常运行,接下来的 bgwriter/checkpoint 过程会把数据页异步的刷到磁盘上;而一旦数据库产生解体,因为 A、B 两条日志对应的数据日志与事务提交日志都曾经被刷到了磁盘上,所以能够通过日志回放在 shared buffer pool 中从新回放出这些数据,之后异步写入磁盘。

fullpage 机制保障可恢复性

WAL 日志的复原仿佛是白璧无瑕的,但可怜的是方才的计划还是存在一些瑕疵。构想当一个 bgwriter 过程在异步的写数据时遇到了数据库的 CRASH,这时一部分脏页写到了磁盘上,磁盘上可能存在坏页。(PolarDB 数据页是 8k,极其状况下磁盘的 4k 写是有可能写出坏页面的)然而 WAL 日志是没方法在坏页上回放数据的。这时就须要用到另外一个机制来保障极其状况下数据库可能找到原始数据,这就波及到了一个重要的机制 fullpage 机制。

在每一个 checkpoin 动作之后的第一次批改数据时,PolarDB 会将这条批改的数据连同整个数据页写入到 wal buffer 中之后再刷入磁盘,这种蕴含整个数据页的 WAL 日志被称为备份块。备份块的存在使得在任何状况下 WAL 日志都能够将残缺的数据页给回放进去。上面是一个残缺的过程。

  1. checkpoint 动作
  2. 进行第一次插入操作,此时共享内存找不到这个页面,它会把这个页面从磁盘加载到共享内存中,之后写入本次插入的输出。这时不同于上一节的操作,PolarDB 序号为 LSN1 的这条 WAL 日志会把从磁盘上读上来标记为 LSN 0 的整个页面写入到 wal buffer pool 中。
  3. 事务提交,此时整个 WAL 日志被强制刷入磁盘上的 WAL 段中。
  4. 同上节
  5. 同上节

这时如果数据库产生了解体,在数据库从新拉起复原时,一旦它遇到了坏掉的页面,便能够通过最后的 WAL 日志中记录的最后版本的页面一步一步的把正确的数据给回放进去。

基于 WAL 日志的解体复原机制

有了前两节的根底上,咱们能够持续演示如果数据库解体后,数据是如何被回放进去的。咱们演示一种数据页被写坏的回放。

  • 当数据库回放到写入数据 A 的这条 WAL 日志时,它会从磁盘中读出 TABLE A 这个页面。这里的这条 WAL 日志是一条备份日志,这是因为 CHECKPOINT 后,每个回放页面的第一条 WAL 日志都是备份日志。
  • 当这条日志被回放时,备份日志有非凡的回放规定:它总是将本人页面笼罩掉原来的页面,并将原来页面的 LSN 降级为这个页面的 LSN。(为了保证数据一致性,失常回放页面只会回放大于本人 LSN 号码的 WAL 日志)。在这个例子中,因为备份块的存在,写坏的页面被胜利复原了进去。
  • 接下来 PolarDB 会依照失常的回放规定去回放后续的日志。

最初数据回放胜利后,shared buffer pool 中的数据便能够异步的被刷到磁盘下来替换之前损坏的数据。

咱们花了很大的篇幅来阐明数据库是如何通过预写日志而进行解体复原的,这仿佛能够解释持久性位点的含意;上面我咱们还须要再解释可见性的问题。

3 可见性机制

因为咱们对于原子性的阐明中会波及可见性的概念,这个概念在 PolarDB 中由一套简单的 MVCC 机制来实现,且大多属于隔离性的领域。这里会对可见性进行一个简略的阐明,而更具体的说明会放到隔离性的文章中持续论述。

事务元组

第一个要说到的是事务元组。他是一条数据的最小单元,真正寄存了数据,这里咱们只关注几个字段就好了。

  • t\_xmin:生成该数据的事务 ID
  • t\_xmax:批改该事务数据的事务 ID(删除或锁定数据的事务 ID)
  • t\_cid:同一事务中对该元组操作的一个序号
  • t\_ctid:一个由段号 / 偏移量组成的指针,指向最新版本的数据

快照

第二个要说到的是快照。快照记录了某一个工夫点数据库中事务的状态。

对于快照咱们仍旧不开展,咱们晓得通过快照能够从 procArray 中获取到某一个工夫点数据库中所有可能事务的状态即可。

以后事务状态

第三点要说的到的是以后事务状态,事务状态是指数据库中决定事务运行状态的的机制。在并发的环境中,决定看到的事务状态是十分重要的一件事。

在查看一个 tuple 中的事务状态时,可能会波及到三个数据结构 t\_infomask、procArray、clog:

  • infomask:位于 tuple 头部的缓存标记位,标记了该元组 xmin/xmax 两个事务的运行状态,这个状态能够看作是 clog 的一层异步缓存,用来减速事务状态的获取;其状态设置是异步设置,在事务提交时并不将所有事务相干的元组都立刻降级,而是期待当第一个足够新的可能看到本次更新的快照设置时再去设置。
  • procArray 快照:快照中的事务状态,快照的获取实际上就是在 procArray 中拿到这一瞬间数据库中所有事务的状态,快照一旦获取状态恒定,除非再次获取(同一事务中获取内容是否扭转取决于事务隔离级别)。
  • clog:事务的理论状态,分为 clog buffer 和 clog 文件两局部。clog buffer 中实时的记录了所有的事务状态。

在一个可见性判断过程中,三者拜访的程序是[infomask -> 快照,clog],而三者的决定性程序是[快照 -> clog -> infomask]。

infomask 是最容易获取的信息,就记录在元组的头部,在局部条件下通过 infomask 就能够明确以后事务的可见性,不须要波及到前面的数据结构;快照领有最高级的决定权,最终决定 xmin/xmax 事务的状态是运行 / 未运行;而 clog 用来辅助可见性的判断,并且辅助设置 infomask 的值。举例而言,如果这个判断 xmin 事务可见性时发现在快照 /clog 中都曾经提交,那么会把 t\_infomask 置为已提交;而如果 xmin 事务可见性时发现在快照提交,而 clog 未提交,则零碎判断产生了解体或回滚,将 infomask 设置为事务非法。

事务快照可见性

在介绍元组和快照后,咱们就能够持续探讨快照可见性的话题。PolarDB 的可见性有一套简单的定义体系,须要通过许多信息组合定义进去,然而其中最间接的就是快照和元组头。上面通过一个数据插入和更新的示例来阐明元组头和快照的可见性。

本文不探讨隔离性,咱们假如隔离级别是可串行化:

  • Snapshot1 时刻:此时事务 1184/1187 都未开始,元组中也没有记录,student 表是一张空表;通过 Snapshot1 快照能够失去的数据是空,咱们把这个版本记做 T。
  • Snapshot1 – Snapshot2 时,此刻咱们获取快照那么拿到的还是 Snapshot1,那么他看到的数据应该还是 T。
  • Snapshot2 时刻:此时事务 1184 曾经完结,1187 还未开始。所以 1184 的批改对用户可见,1187 仍旧不可见。具体到元组中能够看到 (1184/0) 这样的元组头,所以看到的是数据版本 Tom,咱们把这个版本记做 T +1。
  • Snapshot2 – Snapshot3 时,此刻咱们获取快照那么拿到的还是 Snapshot2,那么他看到的数据应该还是 T +1。
  • Snapshot3 时刻:此刻事务 1184/1187 都曾经完结,二者都可见,所以咱们能够看到元组中 (1184,1187) 和(1187,1187)二者都不可见,而(1187,0)即 Susan 是可见的。咱们把这个版本记做 T +2。

通过上述剖析咱们能够失去一个简略的论断,数据库的可见性取决于快照的机会。咱们原子性中所谓的可见性版本不同其实是指拿到的快照不同,快照决定了一个正在执行中的事务是否曾经提交。这种提交与事务标记提交状态甚至是记录 clog 提交都没有关系,咱们能够通过这种办法来使得咱们拿到的快照与事务提交具备一致性。

事务原子性中的可见性

上文中咱们曾经简述了 PolarDB 快照可见性的问题,这里补充下事务提交时的具体实现问题。

咱们设计可见性机制的核心思想是:“事务只应该看到它应该看到的数据版本”。如何定义应该看到,这里只举一个简略的例子,如果一个元组的 xmin 事务没有提交,其余事务大概率是看不到的;而如果一个元组的 xmin 事务曾经提交,其余事务就可能会看到。如何晓得这个 xmin 有没有提交,上文曾经提到了咱们通过快照来决定,所以咱们事务提交时的要害机制就是新快照的更新机制。

可见性在事务提交时波及到两个重要的数据结构 clog buffer 和 procArray。二者的关系在上文曾经给出了解释,他们在判断事务可见性时施展肯定的作用,当然 procArray 起到了决定性的作用。这是因为快照的获取实际上就是一个遍历 ProcArray 的过程。

在理论第三步会将本事务提交的信息写入 clog buffer,此时事务标记 clog 是已提交,但实际上仍旧没有提交。之后事务标记 ProcArray 已提交,这一步事务实现了真正的提交,这个工夫点之后从新获取的快照会更新数据版本。

五 PolarDB 中原子性的实现

在实现了 PolarDB 解体复原及可见性实践的阐明之后,咱们能够晓得 PolarDB 能够通过这样一套预写日志 +BufferIO 的计划来保障事务的解体复原和可见一致性,从而实现原子性。上面咱们将针对事务提交中最重要的环节进行探索,找出咱们最后提到的原子性位点到底指什么。

1 事务解体复原统一——持久性位点

简略来说事务提交中有这样四个操作对于事务的原子性来说是最为外围和重要的。本节咱们先思考前两个操作。

  • 提交事务的 Commit 日志(即 Commit 的 WAL 日志)。
  • 将本次事务所有的提交的 WAL 日志全副强制刷盘,长久化到存储。

咱们标记这个 xlog(WAL 日志)落盘的位点,咱们构想两种状况:

  • 如果在这个位点前事务解体或者回滚了,那么不论数据日志有没有刷盘,Commit 日志肯定没有刷盘,因为 WAL 日志具备程序性,Commit 日志肯定是最初一个长久化到磁盘中。此时如果咱们对数据进行回放,咱们发现短少 Commit 日志的事务无奈被标记为已提交状态,而依据可见性这种状态相干的数据肯定是不可见。这些数据之后会被视为脏数据给清理掉。所以咱们能够得出结论,在这个节点前解体,事务实际上就是没有提交。数据库本质上是复原到了状态 T。
  • 如果在这个位点后解体或回滚了,此时咱们不管它在哪一步解体或回滚,咱们都能够确定 Commit 日志肯定刷到了磁盘上。而一旦 Commit 日志被刷到了磁盘上,那么这个事务所写的数据肯定能够被回放进去且标记为已提交。那么这个数据就是可见的。这个事务实际上曾经提交了,数据库被复原到了 T +1。

这个景象表明,2 号位点仿佛就是解体复原的临界点,它标注了数据库解体复原能够回到 T 或者 T + 1 状态。那么咱们如何称说这个位点?回忆持久性的概念:事务一旦提交,该事务对于数据库的批改就永恒的保留在了数据库中。二者实际上是吻合的。所以咱们将这个 2 号位点称为持久性位点。

另外对于 xlog 刷盘还有一点须要阐明的是 xlog 刷盘和回放具备单个文件的原子性;WAL 日志头部的 CRC 校验提供了单个 WAL 日志文件的合法性校验,如果 WAL 日志写磁盘损坏,这条 WAL 日志的内容有效,确保不会呈现数据的局部回放。

2 事务的可见性统一——原子性位点

接下来咱们持续看 3、4 号操作:

  • 将本次事务提交写入到 Clog buffer 中。
  • 将本次事务提交的后果写入到 ProcArray 中。

3 号操作是在 Clog buffer 中记录了事务的以后状态,能够看作是一层日志缓存。4 号操作将提交操作写入到了 ProcArray 中,这是十分重要的一步操作,通过方才的阐明咱们晓得快照判断事务状态是通过 ProcArray 进行的。即这一步决定了其余事务看到的该事务状态。

如果在 4 号操作前事务解体或回滚,那么数据库中所有其余事务看到的数据版本都是 T,相当于事务没有真正的提交。这个判断即通过可见性 -> 快照 -> Procarray 这个程序决定的。

而当 4 号操作后,针对所有观察者来说这个事务曾经提交了,因为所有在这个工夫点之后拿到的快照数据版本都是 T +1。

从这一点思考,4 号操作齐全切合原子性操作的含意。因为 4 号操作的进行与否影响了事务是否胜利提交。4 号操作前事务总是容许回滚的,因为没有其余事务看到该事务的 T + 1 状态;然而 4 号操作过后,事务便不容许回滚,不然一旦存在读到 T + 1 版本的其余事务就会造成数据的不统一。而原子性的概念即是,事务胜利提交或失败回滚。因为 4 号操作后不容许回滚,那 4 号操作就齐全能够作为事务胜利提交的标记。

综上所述,咱们能够将 4 号操作定义为事务的原子性位点。

3 持久性位点与原子性位点

原子性与持久性的要求

再次给出原子性与持久性的概念:

  • 原子性:一个事务要么执行胜利,要么执行失败。
  • 持久性:一个事务一旦执行胜利,就能够永恒的保留在数据库中。

咱们把 4 号操作标记为原子性位点,是因为在 4 号操作的时刻,主观上所有的观察者都认为这个事务曾经提交了,快照的版本从 T 降级为 T +1,事务不再可回滚。那么事务一旦提交,原子性是否就不失效了,我认为是的,原子性至少只保障事务胜利提交那一刻的数据一致性,事务曾经完结了咱们就没方法再说原子性。所以原子性在原子性位点前保障了事务的可见、可复原。

咱们把 2 号位点标记为持久性位点,是因为持久性认为事务胜利后就能够永恒的保留。根据上述的揣测,这个位点无疑就是 2 号这个持久性位点。所以从 2 号位点开始后的所有工夫咱们都应该保障持久性。

如何了解两个位点

在解释完 2、4 号两个位点之后,咱们最终能够把事务提交时波及到的两个最重要概念定义进去,咱们当初能够答复第一个问题,到底在哪个时刻事务真正的提交?答案是持久性位点后事务能够被残缺的复原进去;而原子性位点后事务真正的被其余事务视作提交。然而二者却并不是分离性的,这如何了解呢?

我认为这其实是原子性实现的一种斗争,因为咱们没有必要把二者对立,咱们只须要保障关键性的一点,只有两个位点的程序可能使得在不同状态下的数据具备一致性,那么就能够认为它合乎咱们原子性的定义。

  • 在持久性位点前解体或回滚,此时事务失败,解体前或复原后数据版本都是 T。
  • 在持久性位点后原子性位点间解体或回滚,此时事务的可见性版本是 T,也就是说对于数据库中的所有事务来说,咱们看到的都是 T。回滚后,数据被从新回放到了 T +1;而此时数据库重启后会发现,在数据库解体前的事务拿到快照看到的数据版本是 T,解体后重启拿到快照看到的数据版本是 T +1,好像事务被隐式的提交了。然而这并不违反数据的一致性。
  • 在原子位点后解体。这个事务曾经提交了,解体前解体后事务看到的都是 T + 1 版本的数据。

最初咱们思考两个位点为什么没有抉择合并。持久性位点的操作是 WAL 日志的刷盘,这个波及到了磁盘 IO 的问题;另一方面原子性位点做的事件是写 ProcArray,这就要拿到 ProcArray 上的一把争抢很重大的大锁,能够认为是一次高频的共享内存写行为;二者自身都关乎数据库事务的效率,如果绑定了二者成为一个原子操作,无疑会使得二者期待相当严重,可能会对事务的运行效率造成较大影响。从这个角度来说二者的行为拆散是一个效率上的思考。

二者程序是否能够颠倒?

显然不能够,通过上述的示意图咱们能够看到两头这一段时间可能呈现既不满足原子性要求,也不满足持久性要求的区域。

具体而言,如果先进行原子性位点,再进行持久性位点,则构想二者两头解体的事务情景。其余事务在解体前会看到 T + 1 版本的数据,解体后看到了 T 版本的数据,这样看到将来数据的行为显然是不被容许的。

如何定义真正的提交

真正的提交就是原子性位点提交。

还是最根本的情理,真正提交的标记就是数据版本从 T 降级为 T +1。这个位点就是原子性位点。在这个点之前,其余事务看到的数据版本都是 T,说真正的提交是不失当的;在这个点之后事务无奈被回滚。这足以阐明这就是事务真正的提交点。

其余操作

咱们最初关注 1 / 3 号操作:

  • 1 号操作是写 wal commit 日志到 xlog buffer,这个写日志对于事务提交来说并不要害;因为如果它写入了没有刷到磁盘上,那么它其实还是毫无作用。
  • 3 号操作是在 clog buffer 中标记本事务为已提交状态;这个操作对事务提交来说也不要害。因为如果数据库运行失常,它不影响本事务快照的可见性;如果数据库解体,这个 clog 状态不管是否曾经长久化,事务状态都能够被 xlog 中的 Commmit/Abort 日志给回放进去。

六 PolarDB 的原子性过程

1 事务提交

本节咱们回到事务提交函数中,看到这几个操作在函数调用栈中的地位。

  • 事务提交流程是带有事务 ID 的事务,不带事务 ID 的事务没有这个过程。因为不带事务 ID 的事务大概率是只读操作,不会对数据库中数据一致性造成任何影响。
  • 提交 xlog 前会开启严格模式,这个模式下任何谬误都会是致命谬误,数据库间接解体重启。
  • xlog 刷盘和 CLOG 写内存的程序是在同步模式下进行的,异步模式下不保障 xlog 刷盘,所以可能会解体后失落数据。
  • 3/ 4 两头有一步要害的操作,Replication 期待。实际上此时数据 xlog 曾经刷盘,然而还没有真正的提交,在同步模式下主库会期待被库将刷到磁盘上的 xlog 利用结束,之后再进行下一步。
  • 写 ProcArray 本事务提交,事务真正提交实现,事务不再可回滚。
  • 清理资源状态,此时工作已和本事务没有任何关系。

2 事务回滚

  • 没有事务 ID 的事务回滚会间接跳过。
  • 回滚前会首先判断事务是否已提交,这个判断是基于 CLOG 进行的。一个事务怎么能又提交又回滚呢?这就是咱们之前探讨的 3 - 4 之间的状态,如果 CLOG 记录了提交,那么遇到回滚命令数据库间接产生致命故障解体重启。
  • 回滚中也会相应的写入 xlog 回滚日志,不过是异步刷到磁盘。能够构想其实回滚日志即便不写入,数据也是不可见的。
  • 当事务在 ProcArray 中写入回滚日志后,事务在过程中真正的回滚了(其实这个状态对其余事务没有影响,之前后拿到的数据版本都是 T)。

七 总结与瞻望

最初对全文做一个总结,本文次要围绕着“如何实现事务原子性”这个话题开展,别离从数据库的解体复原个性和事务可见性来阐明了 PolarDB 数据库实现原子性的底层原理。在介绍预写日志 +buffer IO 原理的过程中还谈到了 shared buffer、WAL 日志、clog、ProcArray、这些对原子性来说重要的数据结构。在事务这个整体下数据库的各个模块奇妙的搭接起来,充分利用磁盘、缓存、IO 这些计算机资源组成了一套残缺的数据库系统。

联想到计算机科学其余的模型,如 ISO 网络模型中传输层 TCP 协定在一个不牢靠的信道上提供牢靠的通信服务。数据库事务实现了相似的思维,即在一个不牢靠的操作系统(随时可能解体)和磁盘存储(无奈大量数据的原子写)上牢靠的存储数据。这一简略而重要的思维堪称是数据库系统的基石,它如此重要以至于整个数据库中最外围的数据结构大多其无关。或者随着数据库的倒退将来技术更迭出更先进的数据库架构体系,然而咱们不能遗记是原子性、持久性仍旧该当是数据库设计的外围。

八 思考

到这里事务原子性的重点就完结了,最初针对本文提到的观点留下几个问题供大家思考。

  • 如何了解事务提交的原子性和持久性位点?
  • 思考单个事务原子性和多个事务原子性的关系?解体复原和可见性是否是一体的?
  • PolarDB 中存在异步提交的概念,即不要求事务提交时不要求 xlog 日志落盘。请思考在这个模式下可能违反事务的哪些个性?是否违反原子性和持久性?

参考资料
https://www.interdb.jp/pg/

版权申明:本文内容由阿里云实名注册用户自发奉献,版权归原作者所有,阿里云开发者社区不领有其著作权,亦不承当相应法律责任。具体规定请查看《阿里云开发者社区用户服务协定》和《阿里云开发者社区知识产权爱护指引》。如果您发现本社区中有涉嫌剽窃的内容,填写侵权投诉表单进行举报,一经查实,本社区将立即删除涉嫌侵权内容。

正文完
 0