【论文阅读】内存数据库并发控制算法的实验研究

内存数据库并发控制算法的实验研究

原文链接jos.org.cn/jos/article/pdf/6454

摘要

并发控制算法的基本思想归纳为"先定序后检验”,基于该思想对现有各类并发控制算法进行 了重新描述和分类总结,于在开源内存型分布式事务测试床 3TS 上的实际对比实验。

  • 3TS:

    3TS: Tencent Transaction Processing Testbed System(简称3TS),是腾讯公司CynosDB(TDSQL)团队与中国人民大学数据工程与知识工程教育部重点实验室,联合研制的面向数据库事务处理的验证系统 (gitee.com) 目前,验证系统已集成13种主流的并发控制算法,提供了TPC-C、PPS、YCSB等常见基准测试。3TS还进一步提供了一致性级别的测试基准,针对现阶段分布式数据库系统的井喷式发展而造成的系统选择难问题,提供一致性级别判别与性能测试比较。


数据库中的事务需要具备 4 大特性: 原子性(A)、一致性©、隔离性(I)和持 久性(D). 原子性要求事务的操作要么全做, 要么全不做; 一致性要求事务的执行必须使数据库从一个一致性 状态变迁到另一个一致性状态, 不允许未成功提交事务的临时数据被访问; 隔离性要求事务的执行不能影响 其他事务也不能被其他事务影响; 持久性要求数据库持久化存储已提交事务写入的数据. 数据库系统为了保 证 ACID 特性, 引入故障恢复子系统来保证原子性和持久性, 并引入并发控制子系统来保证一致性和隔离性.

设计并发控制算法做到正确且高效仍面临以下两大挑战:

  • 在保证可串行化隔离级别的前提下, 保证事务处理的高效性是一个挑战;
  • 并发控制算法的处理逻辑需要针对基于内存的数据库架构进行调整。
    • 内存数据库相比磁盘数据库,降低了回滚率和磁盘I/O对算法整体性能的影响程度,CPU 的计算处理性能反而成为内存数据 库的主要瓶颈⇒ 主要取决于CPU的性能。以锁机制为 例, 磁盘数据库系统往往会使用细粒度的锁来减少锁争用, 而在内存数据库中就可以使用粗粒度的锁来减少维护锁的计算开销。
    • 磁盘数据库需要单独设计存储结构来在内存中维护元数据,然而内存数据库元数据直接放在数据项的头部。

采用内存数据库可以更直接地评估并发控制算法的效率,因为不需要考虑内外存 I/O 的影响。如何利用内存数据库具备的高速数据读取的优点, 设计并优化并发控制算法, 是当前的另一大挑战。

先定序后检验:即并发控制算法可以统一理解为先对事务进行定序——规定算法要求的事务执行顺序; 之后检测事务实际的执行是否满足这个定序要求, 如不符合, 则事务需要回滚。

在开源的内存型分布式事务测试床 3TS 上,在相同的分布式场景下进行了并发控制算法的实验测试:两阶段封锁、乐观并发控制、时间戳排序(timestamp ordering,T/O)、快照隔离(snapshot isolation, SI) 等比较传统的算法以及 MaaT、Calvin 等近年来提出的部分算法进行总结分析

并发控制算法 优缺点
2PC、T/O、MVTO、MV2PC、Silo 适合低冲突率下的应用场景
Calvin确定性算法 受冲突率以及写事务比例影响小,适合于高冲突和分布式事务
写快照隔离write snapshot isolation WSI 低中高冲突率场景下都有不错的表现
Sundial 回滚率低 适合中高冲突场景

【论文阅读】内存数据库并发控制算法的实验研究_第1张图片

1 数据库并发控制算法的基本思想

评估一种算法是否可以避免所有数据异常的黄金法则是可串行化, 即判断并发事务的执行结果是否与某 一串行执行这些事务的结果等价: 若等价, 则认为该算法调度后的并发事务序列是可以避免所有数据异常的。本文创新性地将并发控制算法的核心思想归纳为"先定序后检验"。

1.1 定序

定序是指为并发事务确定一个等价可串行化调度中事务的先后顺序.

  • 静态定序:即按照事务进入某个处理阶段的先后顺序来确定顺序. 使用该方法的典型算法有T/O、OCC等算法. 太过于严格,可能执行的时候的事务顺序是另外一种串行序列,导致不必要的rollback。
  • 动态定序是指: 根据执行过程中的操作结果, 动态地调整事务在最终串行执行序列中的先后关系. 动态调整顺序的依据 就是数据库中的 3 种依赖: 读写依赖、写读依赖和写写依赖。如 MaaT、Sundial 等算法, 均采用了动态定序的方式

动态定序的方式必然需要增加监控事务之间依赖的开销, 但是更灵活的事务顺序会带来较低的回滚率. 静态定序的方式虽然无须增加监控事务依赖的开销, 但是会引入不必要的回滚.

【论文阅读】内存数据库并发控制算法的实验研究_第2张图片

1.2 检验

检验指检查事务的实际操作是否满足定序步骤中规定的事务先后顺序. 核心问题包括: (1) 如何检验; (2) 何时检验.

  • 如何检验

    【论文阅读】内存数据库并发控制算法的实验研究_第3张图片

    • 检验冲突:只关注事务执行过程中是否存在并发事务的冲突操作与自己访问了相同的数据项(是否有数据冲突)。如果有,则等待或者回滚。主流的 2PL 以及OCC 等算法均采用了检验冲突的方式. 2PL 算法在读写操作时, 通过加锁来检验是否存在读写、写写冲突. OCC 算法则是在事务提交之前检验是否存在读写冲突, 即检查全局是否有其他事务修改了自己的读集 Rset.

    • 检验依赖:通过检验事务执行操作产生的数据依赖关系和事先规定的顺序是否相同, 来判断是否需要回滚。

      • T1先开始,然后T2开始,但是在T1 WRITE(X) 发现X被T2修改过,所以T1依赖T2,写作T2→T1(ww),与事务开始的顺序相反所以依赖问题,回滚T1.

      【论文阅读】内存数据库并发控制算法的实验研究_第4张图片

      • 冲突只关注并发事务是否存在对相同数据项的冲突操作, 依赖则关注两个事务之间的先后顺序; 其检验思路不同, 检验冲突是通过规避和解决并发事务的冲突操作来做到可串行化,检查依赖则是根据 3 种依赖体现的事务先后关系来整理出一个可串行化序列, 若两个事务之间存在完全相反的依赖顺序,则两者之中必须回滚一个来达到可串行化.
    • 检验冲突和检验依赖相结合:

      • 对于写写冲突, 利用加锁来解决.
      • 对于写读依赖, 利用预写操作来解决. 预写使得事务只有提交时才会更新数据项, 因此写读依赖仅仅会发生在当前事务与已提交的写事务之间, 因此无须处理
      • 对于读写依赖, 利用数据项上维护的时间戳信息来检查读写依赖是否存在.
  • 何时检验?除了calvin之外可以将事务拆分成三个执行阶段

    • 读写操作阶段read-write phase:执行读写操作
    • 验证阶段validation phase:检验事务是否可以提交,一般针对OCC和SI
    • 结束阶段finish phase:执行事务的提交或者rollback
  • T/O 算法在读写操作阶段进行检验, 可以尽早检验出不可串行化的事务并将其回滚. OCC 算法则在验证阶段单独对冲突或者依赖进行检验, 提高了事务在读写时的并发度。Sundial 选择了在写操作时检验写写冲突, 在验证阶段 检验读写依赖。

1.3 定序方式与检验方式可能的组合

【论文阅读】内存数据库并发控制算法的实验研究_第5张图片

  • 第一象限是静态定序与检验依赖的组合:预先设定一个静态的顺序, 然 后检验实际出现的事务依赖关系是否满足该顺序
  • 第二象限是静态定序与检验冲突的组合:这种组合只需要维护较少的数据结构, 但是会造成最多的假冲突,OCC Calvin。
  • 第三象限中, 动态定序与检验冲突的组合是不可能的, 因为动态定序的基础是事务之间的事务依赖,而检验冲突无法为动态定序提供顺序上的帮助.
  • 第四象限中动态定序方式与检验依赖方式:最直接的一种算法就是根据事务之间的依赖构建出事务依赖图, 并在其中检查是否存在环路。MaaT 通过计算时间戳的方式, 一定程度上减少了构建事务依赖的 开销.

2 两阶段封锁算法(2PL)

思路:根据冲突加锁的先后排定事务顺序, 通过加锁操作检查事务之间的冲突. 事务在读操作时添加读锁(RL)、写操作 时添加写锁(WL), 在提交或者回滚时释放所有锁.

为了预防或解除死锁, 2PL 提供了多种方式: (1) NO-WAIT; (2) WAIT-DIE ; (3) WOUND-WAIT; (4) 死锁检验

  • NO-WAIT 机制是指当加锁发生冲突时, 立刻回滚当前事务。优点:实现简单。缺点:是长事务和其他事务产生冲突的可能性较大, 有可能被饿死。
  • WAIT-DIE 机制是指在检测到冲突时, 根据事务之间的优先级选择处理方式, 即等待(WAIT)或回滚(DIE).可以使用开始时间戳T.start_ts来确定事务的优先级。优点:增加比较优先级的方式避免了事务饿死的情况。缺点:锁算法下的冲突率依然较高。
  • WOUND-WAIT:根据事务优先级来待(WAIT)或抢占锁(WOUND). 若当前事务的优先级较高, 则当前事务抢占锁并回滚之前的锁拥有者; 否 则, 当前事务需要等待.

整体上,2PL方法简单、节省内存、辕信息维护开销少,但是静态定序+检验冲突导致高回滚,不适用于冲突率较高的场景。

3. 乐观并发控制算法OCC

OCC思想:仅在提交之前进行验证, 检查当前事务是否与并发事务存在读写/写写冲突。

3.1 传统OCC

以事务进入验证阶段的时间排序, 并在验证阶段检测并发事务之间是否存在冲突. 为了检测并发事务之间的冲突, 在 OCC 中, 所有事务都需要维护 start_ts (事务的开始时间戳)和 end_ts (事务进入验证时的时间戳), 用于判断当前事务和另一个事务是否并发.还需要对每个事务单独维护一个 Rset(读集)和 Wset (写集): Rset 代表当前事务读取过的数据项 X 的集合, 其中每个元素 Rset[X]包含读到的数据 data; Wset 代表当前事务准备修改的数据项 X, 其中每个元素 Wset[X]包含准备写入的新数据 data.

缺点:1. 只读事务也需要进行验证。2. 额外维护大量写集进行检查,内存开销和验证开销大。

BOCC(back) FOCC(Forward):验证阶段只检查当前事务的写集是否与活跃事务(正在读写阶段的事务)的读集 ActiveRset 存在交集. 相较于 BOCC, FOCC的只读事务无须验证, 整体上减少了验证的开销.

【论文阅读】内存数据库并发控制算法的实验研究_第6张图片

难以应用到分布式系统,于是提出并发验证的OCC算法:把所有事务的写集分成History(已经提交事务的写集)和Active(验证阶段的事务的写集),拆分验证阶段变成几个临界区段来增加并发度,事务 T 验证前先在第 1 个临界区内将自己的写集存入 Active, 保证可以被后进入验 证的事务发现, 并拷贝一份现有 History 和 Active. 之后, 事务 T 通过与读集的对比, 验证两个结构内的写集.验证通过后, 进入第 2 个临界区, 将自己从 Active 中清除并写入 History.(好复杂)

3.2 Silo

OCC 的优化算法 Silo:事务根据进入验证阶段的时间确定顺序, 在验证阶段通过检测自己的读集是否被其他事务修改进行检验. Silo 需要在数据项上维护数据项修改时间戳 wts 用于判断读写冲突, 并额外维护 txn_id 写锁用 于避免写写冲突. 在事务中则需要维护读集 Rset 和写集 Wset, 记录读写过的数据项.还需要额外记录读取时数据项的元信息。

验证阶段:第 1 步需要对写集中的数据加锁. 为了防止加锁过程中发生死锁, 加锁前需要对写集的数据进行排序。第 2步, 检查读集中数据是否被其他事务加锁或 wts 信息是否改变, 以此来判断是否存在读写冲突.在验证通过 后, 解锁并写入新数据. txn_id表示拥有写锁的事务id?

事务 T2 在提交时修改数据项的 wts, T1 验证时发现数据项 X 的 wts 与读操 作时不同, 从而回滚.Silo 通过数据项来检验冲突, 大大减少了验证阶段所需的操作; 其次, Silo 算法下多个事务并发验证, 提 高了事务执行的并发度。仍需维护 读写集

【论文阅读】内存数据库并发控制算法的实验研究_第7张图片

3.3 Maat

动态时间戳,动态定序+检验依赖关系;依据事务之间的依赖来确定事务的可串行化调度的先手顺序,在验证阶段调整事务的时间戳范围来体现?先后关系。

每个数据项X需维护:1. readers:读取该数据项X但是没提交的事务id;2. Writers:表准备写该数据项但未提交的事务;3. wts:最后一次写当前数据项X并且已提交的事务的时间戳。4. rts:表最后一次读取当前数据项的已提交事务时间戳

每个事务T需要维护:1. Rset 读集 2.Wset:写集 3.时间戳范围[lower,upper) 类似于生命周期?体现事务的先后顺序。4. before:表应在当前事务之前提交的未提交事务队列 5.after:应在当前事务之后提交的未提交事务队列 6.other_writes:代表和当前事务修改相同数据项的未提交事务队列 7.max_rts:访问过的数据项X中最大的 rts 8.max_wts:代表访问过的数据项中最大的 wts.

  1. T1读X 更新readers
  2. T2 WX,更新writers,根据readers发现T1→T2依赖关系
  3. T2验证阶段,调整事务信息,更新T1的upper 为1,也就是T1该在1的时候就commit才行?T2为[1,inf)表示T2 1之后开始?主要是在验证阶段通过更新事务的信息来定序。
  4. T1进入验证阶段,因为要让lower 大于数据项 X 的 wts,所以lower =2 > 1 = upper不对?T1 rollback。

【论文阅读】内存数据库并发控制算法的实验研究_第8张图片

3.4 Sundial

动态定序+混合检验,动态计算提交的时间戳来动态的确定顺序,结合OCC+2PL来检验十五是否满足先后顺序,在数据项上维护租约(数据项可以被访问的事务的时间戳范围 )来判断事务的先后顺序。

用OCC验证读写和写读依赖,用加锁解决写写冲突。Sundial中每个数据项需要维护元信息:1. txn_id,锁?2. 租约logic-lease→ wts+rts,如果满足wts≤T.commit_ts≤rts 事务可以读取该数据,commit_ts是事务维护的动态计算的提交时间戳,以及Rset、Wset。

【论文阅读】内存数据库并发控制算法的实验研究_第9张图片

过于简短没看懂,原文链接:people.csail.mit.edu/devadas/pubs/sundial.pdf

**总结:**Silo 算法的优点在 于检验和维护数据项元信息开销较小, 但也因此产生了回滚率较高的缺陷,适用于冲突率较低的场景Sundial 算法则兼顾了动态定序的低回滚和混合检验的高效 率, 在中高冲突率下性能依然较好.此低冲突率场景下性能依然比 Silo 算法差,推荐在低冲突率下使用 Silo 算法, 在中高冲突率下选择 Sundial 算法.

4 时间戳排序算法

静态定序+检验依赖。在事务开始时, 为其分配开始时间戳, 并按照开始时间戳对事务进行排序. 在执行读写操作时, 检测当前事务的实际执行顺序是否违背预先规定的顺序: 如果与预定顺序不符, 当前事务需要回滚或者陷入等待.

在事务数据结构中维护:start_ts 开始时间戳,data数据项的值,wts 代表写入当前数据项的事务时间戳;rts代表最后一次读取当前数据项的事务时间戳. 维护wts rts来记录对数据项的更改,进而检查依赖。基础时间戳排序算法的问题:若事务 T1 修改了数据项 X, 之后, T2 在 T1 的基础上再次修改 X, 此时若 T2 提交而 T1 回滚, 将无法确定 X 应当回滚到的版本。

引入两阶段提交,增加预写操作。事务写操作时不将新数据写入数据库, 而是将新数据暂存, 待事务提交时再将新数据写 入. 预写操作解决了无法处理回滚的问题, 但会影响读取操作的正常执行: 假如一个时间戳更大的读事务在 读取数据时发现一个时间戳更小的写事务尚未提交, 为了满足时间戳顺序, 读事务必须等待.

额外维护min_pts 表示数据项上所有与写操作的最小时间戳;read_reqs 代表当前数据项上等待的读操作;pre_reqs 代表当前数据项上的预写操作;write_reqs表示数据项上等待的提交操作。

【论文阅读】内存数据库并发控制算法的实验研究_第10张图片

5 多版本并发控制

MVCC 可以将事务对数据 项的操作转化为对数据项版本的操作, 从而提高并发度, 提供读写互不阻塞的能力. 但是 MVCC 本身没有处 理事务冲突的机制, 所以一般需要结合 T/O、2PL、OCC 来做到可串行化.

5.1 MVTO 时间戳排序机制+多版本并发控制

将时间戳作为读取数据项某个版本的依据.通过事务的开始时间戳确定事务顺序, 在读写操作时, 检测实际执行产生的 3 种依赖是否违背时间戳规定的顺序.

start_ts 事务的开始时间戳。数据项版本要维护:data、wts:写入当前版本的事务时间戳(版本的提交时间戳);当事务的 start_ts 大于 wts 时, 该版本对事务可见; rts:成功读过该版本的所有事务 的时间戳的最大值,事务的 start_ts 大于所有版本的 rts 时, 该事务可对当前数据项进行修改。txn_id, 代表当 前正在写入该版本的事务号, 可以理解为写锁, 用于避免写写冲突。

【论文阅读】内存数据库并发控制算法的实验研究_第11张图片

5.2 MV2PL 两阶段封锁+多版本并发控制

通过加锁的先后顺序为事务定序, 在读写操作阶段,借助锁机制处理事务之间的冲突. 和2PL相比MV2PL下的读写锁等信息被维护在数据项的每一 个版本上, 包括 txn_id(写锁), read_cnt(读锁数量)以及 wts(当前版本写入时间戳).

【论文阅读】内存数据库并发控制算法的实验研究_第12张图片

5.3 乐观并发控制+多版本并发控制

Cicada是OCC(Silo) + MVCC,cicada要维护1. status 当前版本的状态(PENDING 代表当前版本尚未提交, COMMITTED 代表当前版本已经提交, ABORTED 代表当前版本 已被回滚); 2. wts代表当前版本的提交时间戳; 3. rts, 代表读取过当前版本的已提交事务的最大时间戳.

5.4 快照隔离Snapshot Isolation算法

静态定序 + 检验依赖,读写混合事务根据提交时间戳,只读事务通过开始时间戳确定顺序。检验写写冲突和写读依赖来并发控制。对写写冲突:SI 规定: 数据项不能被两个事务同时修改, 并遵循"先提交者获胜策略”.先提交的事务成功, 后提交的事务回滚. . SI 在 MVCC 方法的基础上, 利用快照读(读取事务开始时符合一致性状态的数据)事务的弱隔离级别之快照隔离 - 知乎 (zhihu.com)处理写读依赖.没有对读写依赖进行限定, SI 算法并不能达到可 串行化.

  • SSI
    • 在 SI 的基础上检测连续的读写依赖(只要在 SI 基础上避免出现 T i 读写依赖于 T j、T j 读写依赖于 T k 的情况, 就能达到可串行化)。
    • (1) txn_id, 用于基础 SI 算法处理写写冲突(2) SIRead-Lock, 记录 读取过该数据项的所有事务, 用于协助事务判断是否存在读写依赖.每个版本需要记录提交时间戳 wts 和写 入该版本的事务号 creator.每个事务 T 上需要维护开始时间戳 start_ts 和提交时间戳 commit_ts为了检 测读写依赖, 还需要额外维护 inConflict 和 outConflict, 分别代表读写依赖于 T 的事务和被 T 读写依赖的事务.

【论文阅读】内存数据库并发控制算法的实验研究_第13张图片

  • WSI
    • WSI 将对写写冲突的检测转化为对读写依赖的检测, 并通过处理读写依赖来达到可串行化.

6 确定性并发控制算法——Calvin

应用在预先知道事务的全部 SQL 语句的场景中。主要思想:预先为事务确定顺序, 之后强制按照确定的顺序执行事务, 以避免分布式协调的开销.额外两个模块:定序器(sequencer)和调度器(scheduler)定序器用于拦截事务并且为这些事务确定顺序,即事务进入定序器的顺序; 调度器负责按照事务顺序为事务加锁, 保证事务的执行严格按照确定顺序。

一个事务T的执行流程:事务 T 被定序器截获并放入一个 batch 中, 根据事务的截获顺 序排序. 在经过一个固定周期后, 定序器将事务 T 所在的 batch 发送给对应的参与者节点. 参与者节点的调度 器接收 batch, 根据 batch 中事务的顺序加锁.

【论文阅读】内存数据库并发控制算法的实验研究_第14张图片

7. 并发控制算法实验评价

评估并发控制算法的标准:吞吐量和回滚率

负载:YCSB + TPCC,其中YCSB事务访问服从Zipf分布,即长尾分布(少量数据获得大量访问)

  • 长尾分布

    长尾分布数据是一种偏态分布,是指几个类别(亦叫头类)包含大量的样本,而大多数类别(亦叫尾类)只有非常少量的样本。

    【论文阅读】内存数据库并发控制算法的实验研究_第15张图片

    7.2 冲突率

【论文阅读】内存数据库并发控制算法的实验研究_第16张图片

  • 低冲突率下性能:WSI > MVTO > Wait die
  • 中冲突率下性能:MVTO > WSI > Sundial
  • 高冲突率下性能:Calvin

7.3 写操作比例

除 Calvin 和 Sundial 以外的算法, 吞吐量都随着写事务比例的升高而下降.

【论文阅读】内存数据库并发控制算法的实验研究_第17张图片

7.4 事务涉及节点数

Calvin 各个节点上子事务可以并发执行, 而且子 事务之间的网络开销和调度开销仅有 1 次, 因此在跨节点数较多的情况下, Calvin 效果较好.

【论文阅读】内存数据库并发控制算法的实验研究_第18张图片

7.5 可扩展性

固定事务的最大涉及节点数, 调整分布环境中的总节点数 从 2 到 16 来分析不同算法的性能表现,4 种场景, 分别是 YCSB 负载下只读事务、YCSB 负载下 中冲突事务(分布比例为 0.5)、TPCC 负载的 NewOrder 事务以及 TPCC 负载的 Payment 事务.

【论文阅读】内存数据库并发控制算法的实验研究_第19张图片

【论文阅读】内存数据库并发控制算法的实验研究_第20张图片

【论文阅读】内存数据库并发控制算法的实验研究_第21张图片

【论文阅读】内存数据库并发控制算法的实验研究_第22张图片

你可能感兴趣的:(论文阅读,数据库)