2021-06-01

九、分布式数据库之隔离性

 

1、读写冲突的处理

 

2021-06-01_第1张图片

 

图中事务 T1、T2 先后启动,分别对数据库执行写操作和读操作。写操作是一个过程,在过程中任意一点,数据的变更都是不完整的,所以 T2 必须在数据写入完成后才能读取,也就形成了读写阻塞(通过锁机制)。反之,如果 T2 先启动,T1 也要等待 T2 将数据完全读取后,才能执行写入。

 

  • 先执行写事务时,阻塞时间除了磁盘写入时间,还包括主备同步网络通讯的开销;
  • 先执行读事务时,虽然不用考虑主备之间的数据复制,但是一方面读操作往往涉及多行数据,更多的数据行会被加锁;另一方面读事务的计算更加复杂,所以读事务阻塞的时间就更长。

 

相比之下,用 MVCC 来解决读写冲突(写写冲突还是要依赖锁,见11节“广义乐观和悲观协议”),就不存在阻塞问题,要优雅得多了。多版本并发控制(Multi-Version Concurrency Control,MVCC)就是通过记录数据项历史版本的方式,来提升系统应对多事务访问的并发处理能力

 

2、单体数据库的 MVCC

 

 

在 PGXC 架构中,因为数据节点就是单体数据库,所以 PGXC MVCC 实现方式其实就是单体数据库的实现方式

 

2.1、MVCC 的存储方式

 

MVCC 有三类存储方式:

 

  • 一类是将历史版本直接存在数据表中的,称为 Appane-Only,典型代表是 PostgreSQL。
  • 另外两类都是在独立的表空间存储历史版本,它们区别在于存储的方式是全量还是增量。
    • 增量存储就是只存储与版本间变更的部分,这种方式称为 Delta,也就是数学中常作为增量符号的那个 Delta,典型代表是 MySQL 和 Oracle。
    • 全量存储则是将每个版本的数据全部存储下来,这种方式称为 Time-Travle,典型代表是 HANA。

 

2021-06-01_第2张图片

 

2.1.1、Append-Only 方式

 

优点:

 

  • 在事务包含大量更新操作时也能保持较高效率。因为更新操作被转换为 Delete + Insert,数据并未被迁移,只是有当前版本被标记为历史版本,磁盘操作的开销较小。
  • 可以追溯更多的历史版本,不必担心回滚段被用完。
  • 因为执行更新操作时,历史版本仍然留在数据表中,所以如果出现问题,事务能够快速完成回滚操作。

 

缺点:

 

  • 新老数据放在一起,会增加磁盘寻址的开销,随着历史版本增多,会导致查询速度变慢。

 

2.1.2、Delta 方式

 

优点:

 

  • 因为历史版本独立存储,所以不会影响当前读的执行效率。
  • 因为存储的只是变化的增量部分,所以占用存储空间较小。

 

缺点:

 

  • 历史版本存储在回滚段中,而回滚段由所有事务共享,并且还是循环使用的。如果一个事务执行持续的时间较长,历史版本可能会被其他数据覆盖,无法查询。
  • 这个模式下读取的历史版本,实际上是基于当前版本和多个增量版本计算追溯回来的,那么计算开销自然就比较大。

 

ORACLE回滚段

 

回滚段概述  

 

回滚段用于存放数据修改之前的值(包括数据修改之前的位置和值)。回滚段的头部包含正在使用的该回滚段事务的信息。一个事务只能使用一个回滚段来存放它的回滚信息,而一个回滚段可以存放多个事务的回滚信息。  

 

回滚段的作用 

 

  • 事务回滚 :当事务修改表中数据的时候,该数据修改前的值(即前影像)会存放在回滚段中,当用户回滚事务(ROLLBACK)时,ORACLE将会利用回滚段中的数据前影像来将修改的数据恢复到原来的值。  

 

  • 事务恢复 :当事务正在处理的时候,实例宕机,回滚段的信息保存在重做日志文件中,ORACLE将在下次打开数据库时利用回滚来恢复未提交的数据。 (ORACLE的CRASH-SAFE能力

 

  • 读一致性 :当一个会话正在修改数据时,其他的会话将看不到该会话未提交的修改。而且,当一个语句正在执行时,该语句将看不到从该语句开始执行后的未提交的修改(语句级读一致性)。当ORACLE执行SELECT语句时,ORACLE依照当前的系统改变号(SYSTEM CHANGE NUMBER-SCN)来保证任何前于当前SCN的未提交的改变不被该语句处理。可以想象:当一个长时间的查询正在执行时,若其他会话改变了该查询要查询的某个数据块,ORACLE将利用回滚段的数据前影像来构造一个读一致性视图。ORACLE的可重复读

 

 

2.1.3、Time-Travel 方式

 

优点:

 

  • 同样是将历史版本独立存储,所以不会影响当前读的执行效率。
  • 相对 Delta 方式,历史版本是全量独立存储的,直接访问即可,计算开销小。

 

缺点:

 

  • 相对 Delta 方式,需要占用更大的存储空间。

 

3、PGXC 的MVCC

 

PGXC 底层使用单体数据库存储,所以PGXC 就是用单体数据库MVCC的方式。

 

4、NewSQL的MVCC

 

NewSQL 底层使用分布式键值系统来存储数据,MVCC 的存储方式与 PostgreSQL 类似,采用 Append 方式追加新版本。

 

5、MVCC存储方式小结

 

2021-06-01_第3张图片

 

6、MVCC 的工作过程

 

MVCC是要解决多事务的并发控制问题,也就是保证事务的隔离性。本质也是在解决读写冲突:如果某个数据正在被某个事务处理,如果用锁来处理,结果就是等待或者阻塞;如果用MVCC来处理,就是取消冲突,但是只能看到上一个版本的数据

 

6.1、“已提交读”(Read Committed,RC)隔离级别下 MVCC 的工作过程

 

按照 RC 隔离级别的要求,事务只能看到的两类数据:

 

  • 当前事务的更新所产生的数据。
  • 当前事务启动前,已经提交事务更新的数据。

 

6.2、例子

 

2021-06-01_第4张图片

 

T1 到 T7 是七个数据库事务,它们先后运行,分别操作数据库表的记录 R1 到 R7。事务 T6 要读取 R1 到 R6 这六条记录,在 T6 启动时(T6-1)会向系统申请一个活动事务列表,活动事务就是已经启动但尚未提交的事务,这个列表中会看到 T3、T4、T5 等三个事务。

 

T6 查询到 R3、R4、R5 时,看到它们最新版本的事务 ID 刚好在活动事务列表里,就会读取它们的上一版本。而 R1、R2 最新版本的事务 ID 小于活动事务列表中的最小事务 ID(即 T3),所以 T6 可以看到 R1、R2 的最新版本。

 

MVCC相比锁的最大优势就是减少了可能造成阻塞的事物的数量。在这个例子中,T6 不会被正在执行写入操作的三个事务阻塞,而如果按照原来的锁方式,T6 要在 T3、T4、T5 三个事务都结束后,才能执行。

 

6.2、“可重复读”(Repeated Read,RR)隔离级别下 MVCC 的工作过程

 

6.1节的方法可以用于RR级别么?

 

RR级别就是在一个事务的执行期间,读取到的数据都是一样的,比如到了T6-2时刻

 

2021-06-01_第5张图片

 

此时,T1 到 T4 等 4 个事务都已经提交,此时 T6 再次向系统申请活动事务列表,列表包含 T5 和 T7。遵循同样的规则,这次 T6 可以新增地看到 R1 到 R4 等四条记录的最新版本,同时看到 R5 的上一版本。

 

T6 刚才和现在这两次查询得到了不同的结果集,这是不符合 RR 要求的。

 

实现 RR 的办法也很简单,我们只需要记录下 T6-1 时刻的活动事务列表,在 T6-2 时再次使用就行了。那么,这个反复使用的活动事务列表就被称为快照Snapshot

 

RC 与 RR 的区别在于 RC 下每个 SQL 语句会有一个自己的快照,所以看到的数据库是不同的,而 RR 下,所有 SQL 语句使用同一个快照,所以会看到同样的数据库。

 

为了提升效率,快照不是单纯的事务 ID 列表,它会统计最小活动事务 ID,还有最大已提交事务 ID。这样,多数事务 ID 通过比较边界值就能被快速排除掉,如果事务 ID 恰好在边界范围内,再进一步查找是否与活跃事务 ID 匹配。

 

在 MVCC 出现前读写操作是相互阻塞的,并行能力受到很大影响。而使用 MVCC 可以实现读写无阻塞,并能够达到 RC(读已提交)隔离级别。基于 MVCC 还可以构建快照,使用快照则能够更容易地实现 RR(可重复读)和 SI(快照隔离)两个隔离级别。

 

此外,MVCC 机制是用时间戳作为重要依据来判别哪个数据版本是可读取的。但是,如果这个时间戳本身有误差,就需要特定的机制来管理这个误差,从而读取到正确的数据版本。参见第10节。

 

7、PGXC 读写冲突处理

 

在 PGXC 架构中,实现 RC 隔离级的处理过程与单体数据库差异并不大。在实现 RR 时遇到两个挑战,也就是实现快照的两个挑战。

 

  • 一是如何保证产生单调递增事务 ID。每个数据节点自行处理显然不行,这就需要由一个集中点来统一生成。
  • 二是如何提供全局快照。每个事务要把自己的状态发送给一个集中点,由它维护一个全局事务列表,并向所有事务提供快照。

 

所以,PGXC 风格的分布式数据库都有这样一个集中点,通常称为全局事务管理器(GTM)。又因为事务 ID 是单调递增的,用来衡量事务发生的先后顺序,和时间戳作用相近,所以全局事务管理器也被称为“全局时钟”。

 

8、NewSQL读写冲突处理

 

8.1、TiDB

 

2021-06-01_第6张图片

 

TiDB使用了Percolator模型,底层是分布式键值系统,假设两个事务操作同一个数据项。其中,事务 T1 执行写操作,由 Prewrite 和 Commit 两个阶段构成,T2 在这两个阶段之间试图执行读操作,但是 T2 会被阻塞,直到 T1 完成后,T2 才能继续执行。

 

即TiDB就是用MVCC 出现前的读写阻塞来处理读写冲突,TiDB 根本没有设计全局事务列表。事实上,PGXC 中的全局事务管理器就是一个单点,很容易成为性能的瓶颈,而分布式系统一个普遍的设计思想就是要避免对单点的依赖。

 

 

8.1、CockroachDB

 

CockroachDB有全局事务列表。但是没有照搬单体数据库的“快照”。

 

2021-06-01_第7张图片

 

依旧是 T1 事务先执行写操作,中途 T2 事务启动,执行读操作,此时 T2 会被优先执行。待 T2 完成后,T1 事务被重启。重启的意思是 T1 获得一个新的时间戳(等同于事务 ID)并重新执行。

 

即CockroachDB还是会产生读写阻塞。CockroachDB 没有使用快照,不是因为没有全局事务列表,而是因为它的隔离级别目标不是 RR,而是 SSI,也就是可串行化。

 

事实上,被重启的事务并不一定是执行写操作的事务。CockroachDB 的每个事务都有一个优先级,出现事务冲突时会比较两个事务的优先级,高优先级的事务继续执行,低优先级的事务则被重启。而被重启事务的优先级也会提升,避免总是在竞争中失败,最终被“饿死”。

 

类似于多线程中,线程被中断,但是可以重入。

 

9、小结

 

 

10、读写操作与时间误差

 

在MVCC中,使用了时间戳作为重要依据来判别哪个数据版本是可读取的。但时间是不精确的,存在一个置信区间

 

2021-06-01_第8张图片

 

因为时间误差的存在,T2-C 时间点附近会形成一个不确定时间窗口,也称为置信区间或可信区间。严格来说,我们只能确定 T2-C 在这个时间窗口内,但无法更准确地判断具体时间点。同样,T6-S 也只是一个时间窗口。时间误差不能消除,但可以通过工程方式控制在一定范围内,例如在 Spanner 中这个不确定时间窗口(记为ɛ)最大不超过 7 毫秒,平均是 4 毫秒。

 

因为时间窗口的存在,T2的结束和T6的开始存在重叠,所以无法判断 T2-C 与 T6-S 的先后关系。如何避免呢?答案是等待。“waiting out the uncertainty”,用等待来消除不确定性。

 

“等待”是为了让事件先后关系明确,消除模糊的边界。

 

10.1、写等待:Spanner

 

Spanner 采用了写等待方案,也就是 Commit Wait,理论上每个写事务都要等待一个时间置信区间。对 Spanner 来说这个区间最大是 7 毫秒,均值是 4 毫秒。但是,由于 Spanner 的 2PC 设计,需要再增加一个时间置信区间,来确保提交时间戳晚于预备时间戳。所以,实际上 Spanner 的写等待时间就是两倍时间置信区间,均值达到了 8 毫秒。

 

2021-06-01_第9张图片

 

10.2、读等待:CockroachDB

 

CockroachDB 采用了读等待方式,就是在所有的读操作执行前处理时间置信区间。读等待的优点是偶发,只有读操作落入写操作的置信区间才需要重启,进行等待。但是,重启后的读操作可能继续落入其他写操作的置信区间,引发多次重启。所以,读等待的缺点是等待时间可能比较长。

 

2021-06-01_第10张图片

 

上图中,事务 T6 启动获得了一个时间戳 T6-S1,此时虽然事务 T2 已经在 T2-C 提交,但 T2-C 与 T6-S1 的间隔小于集群的时间偏移量,所以无法判断 T6 的提交是否真的早于 T2。

 

这时,CockroachDB 的办法是重启(Restart)读操作的事务,就是让 T6 获得一个更晚的时间戳 T6-S2,使得 T6-S2 与 T2-C 的间隔大于 offset,那么就能读取 T2 的写入了。如下图

 

2021-06-01_第11张图片

 

不过,接下来又出现更复杂的情况, T6-S2 与 T3 的提交时间戳 T3-C 间隔太近,又落入了 T3 的不确定时间窗口,所以 T6 事务还需要再次重启。而 T3 之后,T6 还要重启越过 T4 的不确定时间窗口。见下图

 

2021-06-01_第12张图片

 

10.3、在什么情况下,不用“等待”也能达到线性一致性或因果一致性呢?”

 

读等待和写等待都是通过等待的方式,度过不确定的时间误差,从而给出确定性的读写顺序,但性能会明显下降。那么在什么情况下,不用“等待”也能达到线性一致性或因果一致性呢?”

 

10.3.1、如果分布式数据库使用了 TSO,保证全局时钟的单向递增,那么就不再需要等待了,因为在事件发生时已经按照全序排列并进行了记录。

 

10.3.2、假设两个事件发生地的距离除以光速得到一个时间 X,两个事件的时间戳间隔是 Y,时钟误差是 Z。如果 X>Y+Z,那么可以确定两个事件是并行发生的,事件 2 就不用读等待了。这是因为既然事件是并行的,事件 2 看不到事件 1 的结果也就是正常的了。

 

11、广义乐观和狭义乐观

 

之前讨论的MVCC主要处理的是读写冲突,但是,读写冲突只是事务冲突的部分情况,更多时候我们要面对的是写写冲突,甚至后者还要更重要些。

 

11.1、并发控制技术的分类

 

并发控制分为乐观协议悲观协议两大类,单体数据库大多使用悲观协议。TiDB 和 CockroachDB 都在早期版本中提供了乐观协议,但在后来的产品演进又改回了悲观协议,其主要原因是事务竞争激烈和对遗留应用系统的兼容。

 

悲观协议是使用锁的,而乐观协议是不使用锁的。所以两个名词分别是:悲观锁和乐观协议(注意:没有“锁”字!)。

 

 

11.2、并发控制的三个阶段

 

在经典理论教材“Principles of Distributed Database Systems”中,作者将乐观协议和悲观协议的操作,都统一成四个阶段,分别是有效性验证(V)、读(R)、计算(C)和写(W)

 

两者的区别就是这四个阶段的顺序不同:

 

  • 悲观协议的操作顺序是 VRCW;
  • 乐观协议的操作顺序则是 RCVW。

 

因为在比较两种协议时,计算(C)这个阶段没有实质影响,可以忽略掉。那么简化后,悲观协议的顺序是 VRW,而乐观协议的顺序就是 RVW。

 

RVW的图示如下:

 

2021-06-01_第13张图片

 

11.2.1、读阶段(Read Phase)

 

每个事务对数据项的局部拷贝进行更新。要注意,此时的更新结果对于其他事务是不可见的。这个阶段的命名特别容易让人误解,明明做了写操作,却叫做“读阶段”。我想它大概是讲,那些后面要写入的内容,先要暂时加载到一个仅自己可见的临时空间内。

 

11.2.2、有效性确认阶段(Validation Phase)

 

验证准备提交的事务。这个验证就是指检查这些更新是否可以保证数据库的一致性,如果检查通过进入下一个阶段,否则取消事务。

 

首先这里提到的检查与隔离性目标有直接联系;其次就是检查可以有不同的手段,也就是不同的并发控制技术,比如可以是基于锁的检查,也可以是基于时间戳排序

 

在分布式架构下,有效性验证又分为局部有效性验证全局有效性验证。因此,乐观又分为狭义乐观和广义乐观,而狭义乐观就是学术领域常说的 OCC。TiDB 的乐观锁,因为没有全局有效性验证,不严格符合 VRW 悲观协议排序,所以是广义乐观(或者说相对乐观)。而 TiDB 后来增加的悲观锁,增加了全局有效性验证,是严格的 VRW,所以是悲观协议。

 

相对乐观和局部悲观是一体两面的关系,识别它的要点就在于是否有全局有效性验证

 

11.2.3、写阶段(Write Pharse)

 

将读阶段的更新结果写入到数据库中,接受事务的提交结果。就是完成最终的事务提交操作

 

  • 乐观,重在事后检测,在事务提交时检查是否满足隔离级别,如果满足则提交,否则回滚并自动重新执行。
  • 悲观,重在事前预防,在事务执行时检查是否满足隔离级别,如果满足则继续执行,否则等待或回滚。

 

11.3、TiDB的乐观锁

 

但是TiDB却宣城自己使用了乐观锁!这是怎么回事?

 

TiDB 的乐观锁基本上就是 Percolator 模型(参见《原子性》4.1节),它的运行过程可以分为三个阶段。

 

  • 选择 Primary Row:收集所有参与修改的行,从中随机选择一行,作为这个事务的 Primary Row,这一行是拥有锁的,称为 Primary Lock,而且这个锁会负责标记整个事务的完成状态。所有其他修改行也有锁,称为 Secondary Lock,都会保留指向 Primary Row 的指针。
  • 写入阶段:按照两阶段提交的顺序,执行第一阶段。每个修改行都会执行上锁(这里不一定是真正的锁,有可能使用了MVCC。只要能达到让其它事务不可见的目的就行)并执行“prewrite”,prewrite 就是将数据写入私有版本,其他事务不可见。注意这时候,每个修改行都可能碰到锁冲突的情况,如果冲突了,就终止事务,返回给 TiDB,那么整个事务也就终止了。如果所有修改行都顺利上锁,完成 prewrite,第一阶段结束。
  • 提交阶段:这是两阶段提交的第二阶段,提交 Primary Row,也就是写入新版本的提交记录并清除 Primary Lock,如果顺利完成,那么这个事务整体也就完成了,反之就是失败。而 Secondary Rows 上的锁,则会交给异步线程根据 Primary Lock 的状态去清理。

 

之所以是乐观的锁,是因为虽然对于每一个修改行来说,TiDB 都做了有效性验证,而且顺序是 VRW,可以说是悲观的,但这只是局部的有效性验证;从整体看,TiDB 没有做全局有效性验证,不符合 VRW 顺序,所以不能称为悲观锁,只能还是相对乐观的。

 

上述第2个阶段,对私有版本的操作也执行了有效性检查,规避锁冲突的情况。为啥私有版本还需要规避呢?不是有MVCC的控制,其它事务不可见么?

 

这是因为虽然对其它事务不可见,可是如果是两个新的事物都需要给某个数值(比如余额)增加100,那就必须都进行上锁才行,即遇到了“写写冲突”。处理这个写写冲突就是在做有效性检查V。

 

TiDB是局部悲观,从另外的角度来说就是相对乐观

 

11.4、乐观协议的挑战

 

一是事务冲突少是使用乐观协议的前提,但这个前提是否普遍成立要看具体情况,二是现有应用系统使用的单体数据库多是悲观协议,兼容性上的挑战。

 

11.4.1、事务频繁冲突

 

首先,事务冲突少这个前提,随着分布式数据库的适用场景越来越广泛,显得不那么有通用性了。

 

比如,金融行业就经常会有一些事务冲突多又要保证严格事务性的业务场景,一个简单的例子就是银行的代发工资。代发工资这个过程,其实就是从企业账户给一大批个人账户转账的过程,是一个批量操作。在这个大的转账事务中可能涉及到成千上万的更新,那么事务持续的时间就会比较长。如果使用乐观协议,在这段时间内,只要有一个人的账户余额发生变化,事务就要回滚。

 

11.4.2、遗留应用的兼容性需求

 

回到悲观协议还有一个重要的原因,那就是保证对遗留应用系统的兼容性。这个很容易理解,因为单体数据库都是悲观协议,甚至多数都是基于锁的悲观协议,所以在 SQL 运行效果上与乐观协议有直接的区别。

 

一个非常典型的例子就是 select for update。这是一个显式的加锁操作,或者说是显式的方式进行有效性确认,广义的乐观协议都不提供严格的 RVW,所以也就无法支持这个操作。

 

11.4.3、TiDB 的新的悲观锁

 

TiDB 悲观锁的理论基础很简单,就是在原有的局部有效性确认前,增加一轮全局有效性确认。这样就是严格的 VRW,自然就是标准的悲观协议了。具体采用的方式就是增加了悲观锁,这个锁是实际存在的,表现为一个占位符,随着 SQL 的执行即时向存储系统(TiKV)发出,这样事务就可以在第一时间发现是否有其他事务与自己冲突。

 

2021-06-01_第14张图片

 

12、悲观协议

 

这里主要讨论了可串行化的实现方案。

 

12.1、悲观协议的分类

 

首先要看看并发控制技术整体的分类。

 

  • 有的是按乐观协议和悲观协议进行分类;
  • 有的是按狭义乐观协议和其他悲观协议进行分类。

 

作者认为狭义乐观协议和其他悲观协议这种分类方式更清晰些。按照这个分类,并发控制协议分类如下图(图里的乐观协议是指狭义乐观并发控制);

 

2021-06-01_第15张图片

 

图里的几个名词:

 

  • 有序共享(Ordered Sharing 2PL, O2PL)
  • 利他锁(Altruistic Locking, AL)
  • 只写封锁树(Write-only Tree Locking, WTL)
  • 读写封锁树(Read/Write Tree Locking, RWTL)

 

基于锁的协议,数据库系统主要使用的 2PL。

 

12.2、两阶段封锁(Two-Phase Locking,2PL)

 

2PL 就是事务具备两阶段特点的并发控制协议,这里的两个阶段指加锁阶段释放锁阶段,并且加锁阶段严格区别于紧接着的释放锁阶段。

 

2021-06-01_第16张图片

 

在 t1 时刻之前是加锁阶段,在 t1 之后则是释放锁阶段,可以从时间上明确地把事务执行过程划分为两个阶段。2PL 的关键点就是释放锁之后不能再加锁

 

根据加锁和释放锁时机的不同,2PL 又有一些变体。

 

12.2.1、保守两阶段封锁协议(Conservative 2PL,C2PL)

 

事务在开始时设置它需要的所有锁(即读锁和写锁

 

2021-06-01_第17张图片

 

12.2.2、严格两阶段封锁协议(Strict 2PL,S2PL)

 

事务一直持有已经获得的所有写锁,直到事务终止。

 

2021-06-01_第18张图片

 

S2PL 模式下,事务持有锁的时间过长,会导致系统并发性能较差。

 

12.2.3、强两阶段封锁协议(Strong Strict 2PL,SS2PL)

 

事务一直持有已经获得的所有锁,包括写锁和读锁,直到事务终止。SS2PL 与 S2PL 差别只在于一直持有的锁的类型,所以它们的图形是相同的。

 

Percolator 模型:当主锁(Primary Lock)没有释放前,所有的记录上的从锁(Secondary Lock)实质上都没有释放,在主锁释放后,所有从锁自然释放。所以,Percolator 也属于 S2PL。TiDB 的乐观锁机制是基于 Percolator 的,那么 TiDB 就也是 S2PL。

 

S2PL 模式下,事务持有锁的时间过长,导致系统并发性能较差,所以实际使用中往往不会配置到可串行化级别。这就意味着我们还是没有生产级技术方案。

 

一种可能是性能更优的可串行化工程化实现,这就是 CockroachDB 的串行化快照隔离(SSI)。而 SSI 的核心,就是串行化图检测(SGT)。

 

12.3、串行化图检测(SGT,Serializable Graph TEST)

 

串行化图(Serializable Graph,SG)。这个图用来分析数据库事务操作的冲突情况。每个事务是一个节点,事务之间的关系则表示为一条有向边。

 

串行化图的构建规则是这样的,事务作为节点,当一个操作与另一个操作冲突时,在两个事务节点之间就可以画上一条有向边。

 

具体来说,事务之间的边又分为三类情况:

 

  • 写读依赖(WR-Dependencies),第二个操作读取了第一个操作写入的值。
  • 写写依赖(WW-Dependencies),第二个操作覆盖了第一个操作写入的值。
  • 读写反依赖(RW-Antidependencies),第二个操作覆盖了第一个操作读取的值,可能导致读取值过期。

 

串行化图检测的原理是,如果途中形成了环,那么就是不可串行化的。

 

比如下面的事物

 

2021-06-01_第19张图片

对应的SG就没有环:

 

2021-06-01_第20张图片

 

 

2021-06-01_第21张图片

 

就有环

2021-06-01_第22张图片

 

SGT通过检测环, SGT 没有锁的管理成本,所以性能比 S2PL 更好。但它和传统的 S2PL 一样属于悲观协议。

 

12.4、MVCC与乐观协议、悲观协议的区别

 

MVCC 已经是数据库的底层技术,与乐观协议、悲观协议下的各项技术是两个不同的维度,最后形成了 MVTO、MV2PL、MVSGT 等技术。这些技术考虑了多版本情况下的处理,但遵循的基本原理还是一样的。

你可能感兴趣的:(分布式算法,分布式)