通过大量研究,我们找到了最佳的并发控制机制,结论为:基于SILO的OCC算法是MOT中最符合ACID特性的OCC算法。SILO为满足MOT的挑战性需求提供了最好的基础。
说明: MOT完全符合原子性、一致性、隔离性、持久性(ACID)特性,如MOT简介所述。
下面介绍MOT的并发控制机制。
MOT本地内存和全局内存
SILO管理本地内存和全局内存,如所示。
- 全局内存是所有核共享的长期内存,主要用于存储所有的表数据和索引。
- 本地内存是短期内存,主要由会话使用,用于处理事务及将数据更改存储到事务内存中,直到提交阶段。
当事务需要更改时,SILO将该事务的所有数据从全局内存复制到本地内存。使用OCC方法,全局内存中放置的是最小的锁,因此争用时间极短。事务更改完成后,该数据从本地内存回推到全局内存中。
本地内存与SILO增强并发控制的基本交互式事务流如下所示:
图 1 私有(本地)内存(每个事务)和全局内存(所有核的所有事务)
具体请参见Industrial-Strength OLTP Using Main Memory and Many-cores[对比:磁盘与MOT]。
MOT SILO增强特性
SILO凭借其基本算法流程,优于我们在研究实验中测试的许多其他符合ACID的OCC算法。然而,为了使SILO成为产品级机制,我们必须用许多在最初设计中缺失的基本功能来增强它,例如:
- 新增对交互式事务的支持,其中事务的SQL运行在客户端实现,而不是作为服务器端的单个步骤运行。
- 新增乐观插入
- 新增对非唯一索引的支持
- 新增对事务中写后读校验(RAW)的支持,使用户能够在提交之前查看更改
- 新增对无锁协同垃圾回收的支持
- 新增对无锁检查点的支持
- 新增对快速恢复的支持
- 新增对分布式部署两阶段提交的支持
在不破坏原始SILO的可扩展特性的前提下添加这些增强是非常具有挑战性的。
MOT隔离级别
即使MOT完全兼容ACID,MogDB 2.0.0并非支持所有的隔离级别。下表介绍了各隔离级别,以及MOT支持和不支持的内容。
表 1 隔离级别
隔离级别 | 说明 |
---|---|
READ UNCOMMITTED | MOT不支持 |
READ COMMITTED | MOT支持 READ COMMITTED(读已提交)隔离级别保证任何正在读取的数据在上一次读取时都已提交。它只是限制读者看到任何中间数据、未提交数据,或脏读。数据被读取后可以自由更改,因此,读已提交隔离级别并不保证事务再次读取时能找到相同的数据。 |
SNAPSHOT | MOT不支持 SNAPSHOT(快照)隔离级别提供与SERIALIZABLE(可序列化)相同的保证,同时支持并发事务修改数据。相反,它迫使每个读者看到自己的世界版本(自己的快照)。不阻止并发更新使得编程非常容易,且可扩展性很强。然而,在许多实现中,这种隔离级别需要更高的服务器资源。 |
REPEATABLE READ | MOT支持 REPEATABLE READ(可重复读)是一个更高的隔离级别,除了READ COMMITTED隔离级别的保证之外,它还保证任何读取的数据都不能更改。如果一个事务再次读取相同的数据,它将找出该数据,不做更改,并且保证它可读取。乐观模型使得并发事务能更新该事务读取的行。在提交时,该事务将验证REPEATABLE READ隔离级别是否被违反。若违反,则回滚该事务,必须重试。 |
SERIALIZABLE | MOT不支持 SERIALIZABLE(可序列化)隔离提供了更强的保证。除了REPEATABLE READ隔离级别保证的所有内容外,它还保证后续读取不会看到新数据。它之所以被命名为SERIALIZABLE,是因为隔离非常严格,几乎有点像事务串行运行,而不是并行运行。 |
下表显示了不同隔离级别启用的并发副作用。
表 2 隔离级别启用的并发副作用
隔离级别 | 说明 | 不可重复读 | 幻影 |
---|---|---|---|
READ UNCOMMITTED | 是 | 是 | 是 |
READ COMMITTED | 否 | 是 | 是 |
REPEATABLE READ | 否 | 否 | 是 |
SNAPSHOT | 否 | 否 | 否 |
SERIALIZABLE | 否 | 否 | 否 |
在不久后将发布的版本中,MogDB MOT还将支持SNAPSHOT和SERIALIZABLE隔离级别。
MOT乐观并发控制
并发控制模块(简称CC模块)提供了主内存引擎的所有事务性需求。CC模块的主要目标是为主内存引擎提供各种隔离级别的支持。
乐观OCC与悲观2PL
悲观2PL(2阶段锁定)和乐观并发控制(OCC)的功能差异在于对事务完整性分别采用悲观和乐观方法。
基于磁盘的表使用悲观方法,这是最常用的数据库方法。MOT引擎使用的是乐观方法。
悲观方法和乐观方法的主要功能区别在于,如果冲突发生,
- 悲观的方法会导致客户端等待;
- 而乐观方法会导致其中一个事务失败,使得客户端必须重试失败的事务。
乐观并发控制方法(MOT使用)
乐观并发控制(OCC)方法在冲突发生时检测冲突,并在提交时执行验证检查。
乐观方法开销较小,而且通常效率更高,原因之一是事务冲突在大多数应用程序中并不常见。
当强制执行REPEATABLE READ隔离级别时,乐观方法与悲观方法之间的函数差异更大,而当强制执行SERIALIZABLE隔离级别时,函数差异最大。
悲观方法(MOT未使用)
悲观并发控制(2PL,或称2阶段锁定)方法使用锁阻止在潜在冲突的发生。执行语句时应用锁,提交事务时释放锁。基于磁盘的行存储使用这种方法,并且添加了多版本并发控制(Multi-version Concurrency Control,MVCC)。
在2PL算法中,当一个事务正在写入行时,其他事务不能访问该行;当一个行正在读取时,其他事务不能覆盖该行。在访问时锁定每个行,以进行读写;在提交时释放锁。这些算法需要一个处理和避免死锁的方案。死锁可以通过计算等待图中的周期来检测。死锁可以通过使用TSO保持时序或使用某种回退方案来避免。
遇时锁定(ETL)
另一种方法是遇时锁定(ETL),它以乐观的方式处理读取,但写入操作锁定它们访问的数据。因此,来自不同ETL事务的写入操作相互感知,并可以决定中止。实验证明,ETL通过两种方式提高OCC的性能:
- 首先,ETL会在早期检测冲突,并通常能增加事务吞吐量。这是因为事务不会执行无用的操作。(通常)在提交时发现的冲突无法在不中止至少一个事务的情况下解决。
- 其次,ETL写后读校验(RAW)运行高效,无需昂贵或复杂的机制。
结论:
OCC是大多数工作负载最快的选项。这一点我们在初步研究阶段已经发现。
其中一个原因是,当每个核执行多个线程时,锁很可能被交换线程持有,特别是在交互模式下。另一个原因是悲观算法涉及死锁检测(产生开销),并通常使用读写锁(比标准自旋锁效率低)。
我们选择Silo是因为它比其他现有选项(如TicToc)简单,同时对大多数工作负载保持相同的性能。ETL有时比OCC更快,但它引入了假中止,可能会使用户混淆,而OCC则只在提交时中止。
OCC与2PL的区别举例
下面是会话同时更新同一个表时,两种用户体验的区别:悲观(针对基于磁盘的表)和乐观(针对MOT表)。
本例中,使用如下表测试命令:
table “TEST” – create table test (x int, y int, z int, primary key(x));
复制
本示例描述同一测试的两个方面:用户体验(本示例中的操作)和重试要求。
悲观方法示例——用于基于磁盘的表
下面是一个悲观方法例子(非MOT)。任何隔离级别都可能适用。
以下两个会话执行尝试更新单个表的事务。
WAIT LOCK操作发生,客户端体验是:会话2卡住,直到会话1完成COMMIT,会话2才能进行。
但是,使用这种方法时,两个会话都成功,并且不会发生异常中止(除非应用了SERIALIZABLE或REPEATABLE-READ隔离级别),这会导致整个事务需要重试。
表 3 悲观方法代码示例
会话1 | 会话2 | |
---|---|---|
t0 | Begin | Begin |
t1 | update test set y=200 where x=1; | |
t2 | y=200 | Update test set y=300 where x=1; – Wait on lock |
t4 | Commit | |
Unlock | ||
Commit(in READ-COMMITTED this will succeed, in SERIALIZABLE it will fail) | ||
y = 300 |
乐观方法示例——用于MOT
下面是一个乐观方法的例子。
它描述了创建一个MOT表,然后有两个并发会话同时更新同一个MOT表的情况。
create foreign table test (x int, y int, z int, primary key(x));
复制
- OCC的优点是,在COMMIT之前没有锁。
- OCC的缺点是,如果另一个会话更新了相同的记录,则更新可能会失败。如果更新失败(在所有支持的隔离级别中),则必须重试整个会话#2事务。
- 更新冲突由内核在提交时通过版本检查机制检测。
- 会话2将不会等待其更新操作,并且由于在提交阶段检测到冲突而中止。
表 4 乐观方法代码示例——用于MOT
会话1 | 会话2 | |
---|---|---|
t0 | Begin | Begin |
t1 | update test set y=200 where x=1; | |
t2 | y=200 | Update test set y=300 where x=1; |
t4 | Commit | y = 300 |
Commit | ||
ABORT | ||
y = 200 |