CockroachDB架构的事务层通过协调并发操作来实现对ACID事务的支持。

如果你还没阅读过架构概览,建议你先阅读该篇。

概览

最重要的是,CockroachDB认为一致性是数据库最重要的特性 ,如果没有它,开发人员无法构建可靠的工具,并且企业可能会遇到潜在的且难以检测到的异常情况。

为了提供一致性,CockroachDB在事务层中实现了对ACID事务语义的完全支持。 但是,重要的是要意识到所有语句都作为事务处理,包括单个语句 - 这有时被称为“自动提交模式”("autocommit mode"),因为它的行为就好像每个语句后跟一个COMMIT

有关在CockroachDB中使用事务的代码示例,请参阅有关事务的文档

由于CockroachDB支持可以跨越整个集群的事务(包括跨range和跨表事务),因此它通过两阶段提交过程优化了正确性。

写 & 读 (阶段1)

当事务层执行写操作时,它不会直接将值写入磁盘。 相反,它创建了两个有助于它协调分布式事务的东西:

当创建Write Intents时,CockroachDB会检查新的提交值(如果存在,则重新启动事务)以及相同键的现有Write Intents - 这将被作为事务冲突来解决。

如果由于其他原因导致事务失败,例如未能通过SQL约束检查,则事务将中止。

如果事务尚未中止,则事务层开始执行读取操作。 如果只读操作遇到标准的MVCC值,则一切正常,但是,如果遇到任何 Write Intents,则必须将操作作为事务冲突解决。

提交 (阶段2)

CockroachDB检查正在运行的事务的记录,看它是否被“中止”; 如果有,则重新启动事务。

如果事务通过了这些检查,它将进入到COMMITTED,并告知客户端事务成功。 此时,客户端可以自由开始向集群发送更多请求。

清理 (异步阶段3)

事务完成后,所有Write Intents都应该被清理掉。 要做到这一点,the coordinating node––which kept a track of all of the keys it wrote––reaches out to the values,并且:

不过,这只是一种优化。 如果将来的操作遇到Write Intents,它们总是检查它们的事务记录 -- 任何操作都可以通过检查Transaction Record的状态来解析或删除Write Intents。

与其他层的交互

与CockroachDB中的其他层的关系中,事务层:

技术细节和组成

时间和混合逻辑时钟(Hybrid Logical Clocks)

在分布式系统中,排序和因果关系(causality)是比较难解决的问题。尽管可以通过完全依赖于Raft来维持可串行化,但会使数据读取效率低下。

为了优化读取性能,CockroachDB实现了混合逻辑时钟(HLC),它由物理组件(总是接近本地现实时间)和逻辑组件(用于区分具有相同物理组件的事件)组成。 这意味着HLC时间总是大于或等于现实时间。更多细节查看:HLC paper.

在事务方面,网关节点使用HLC时间为事务选择时间戳。 每当提到事务的时间戳时,它就是HLC值。此时间戳用于跟踪值的版本(通过多版本并发控制)以及提供事务隔离保证。

当节点向其他节点发送请求时,它们包括由其本地HLC(包括物理和逻辑组件)生成的时间戳。当节点接收请求时,它们会通知发送方提供事件的时间戳。 这有助于保证在节点上读取/写入的所有数据的时间戳都小于下一个HLC时间。

然后,这使得主要负责range的节点(即Leaseholder)通过确保读取数据的事务处于大于其读取的MVCC值的HLC时间来读取其存储的数据(也就是“读取“始终发生在“写入“之后)。

Max Clock Offset Enforcement(强制最大时钟偏移)

CockroachDB需要适度级别的时钟同步以保持数据一致性。 因此,当节点检测到其时钟与集群中至少一半其他节点不同步时,即与其他节点的时钟偏移达到允许的最大偏移量(默认为500毫秒)的80%,它会立即崩溃

这样可以避免违反可序列化一致性并导致过时读取(stale reads)和写偏序(write skews)的风险,但通过在每个节点上运行NTP 或其他时钟同步软件来防止时钟过度偏移是非常重要的。

有关时钟偏移太大可能导致的风险的更多详细信息,请查看 What happens when node clocks are not properly synchronized?

时间戳缓存

为了提供可串行化,每当读取值时,我们将该操作的时间戳存储在时间戳缓存中,该时间戳显示正在读取的值的最新时间戳(high-water mark)。

每当发生写入操作时,都会根据缓存的时间戳来检查其时间戳。 如果时间戳小于缓存时间戳的最新值,我们会尝试将其事务的时间戳向前移动到以后的时间(move the timestamp for its transaction forward to a later time)。 在可序列化事务的情况下,这会导致它们在事务的第二阶段重新启动。

client.Txn 和 TxnCoordSender

正如我们在SQL层的架构概述中所提到的,CockroachDB将所有SQL语句转换为键值(KV)操作,这是最终存储和访问数据的方式。

从SQL层生成的所有KV操作都使用client.Txn,它是CockroachDB KV层的事务接口 - 但是,如上所述,所有语句都被视为事务,因此所有语句都使用此接口。

但是,client.Txn实际上只是TxnCoordSender的装饰器(wrapper),它在我们的代码库中扮演着至关重要的角色:

此后,请求将传递到Distribution中的DistSender

事务记录

当事务开始时,TxnCoordSender将事务记录写入包含事务中修改的第一个key的range。 如上所述,事务记录为系统提供了关于事务状态的可信的来源。

事务记录表达了以下事务的状态之一:

已提交事务的事务记录将保留,直到其所有Write Intents转换为MVCC值。 对于中止的事务,可以随时删除事务记录,这也意味着CockroachDB将丢失被视为属于中止的事务的事务记录。

Write Intents

CockroachDB中的值不直接写入存储层; 相反,所有东西都是以临时状态写成的,称为“Write Intent”。 它们本质上是多版本的并发控制值(也称为MVCC,在存储层中有更深入的解释),并添加了一个附加值,用于标识值所属的事务记录。

每当操作遇到Write Intent(而不是MVCC值)时,它会查找事务记录的状态以了解它应如何处理Write Intent值。

解析Write Intent

每当操作遇到key的Write Intent时,它会尝试“解析”它,其结果取决于Write Intent的事务记录:

隔离级别

隔离是ACID事务的一个元素,它决定了并发性的控制方式,并最终保证了一致性。

CockroachDB有效地支持最强的ANSI事务隔离级别:SERIALIZABLE。 所有其他ANSI事务isolaton级别(例如,READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READ)将自动升级为SERIALIZABLE。历史上较弱的隔离级别用于最大化事务吞吐量。 但是,最近的研究 证明,使用弱隔离级别会导致基于并发的攻击的严重漏洞。 CockroachDB继续支持额外的非ANSI隔离级别SNAPSHOT,尽管它已被弃用。 客户端可以在启动事务时显式设置事务的隔离:

事务冲突

CockroachDB的事务允许以下类型的冲突:

为了使这更容易理解,我们将调用第一个事务TxnA和遇到其 Write Intents 的事务TxnB

CockroachDB继续执行以下步骤,直到其中一个事务中止,将其时间戳向前推移或进入TxnWaitQueue队列(has its timestamp pushed, or enters the TxnWaitQueue)。

  1. 如果事务具有明确的优先级设置(即HIGHLOW),则具有较低优先级的事务被中止(在Write/Write情况下)或者将其时间戳向前推移(在Write/Read情况下)。

  2. TxnB试图向前推送TxnA的时间戳。

    只有在TxnA是snapshot isolation隔离级别并且TxnB的操作是读取的情况下,才能成功。 在这种情况下,会出现写偏序异常。

  3. TxnB进入TxnWaitQueue等待TxnA完成。

此外,可能会出现以下类型的冲突(that don't involve running into intents):

TxnWaitQueue(Txn等待队列)

TxnWaitQueue追踪所有他们遇到的无法推动(push)写操作的事务,并且必须等待阻塞事务完成才能继续。

TxnWaitQueue是记录阻塞事务ID的map,例如

txnA -> txn1, txn2
txnB -> txn3, txn4, txn5

重要的是,所有这些活动都发生在单个节点上,该节点是包含事务记录的range的Raft组的leader。

一旦事务确实解决了---通过提交或中止---一个信号被发送到TxnWaitQueue,它允许被该事务阻止的所有事务开始执行。

被阻塞的事务会检查自己事务的状态,以确保它们仍处于活动状态。 如果被阻止的事务被中止,则只需将其删除。

如果事务之间存在死锁(即它们各自被彼此的Write Intents阻塞),则其中一个事务被随机中止。 在上面的例子中,如果txnAkey1上阻塞了TxnB,而TxnBkey2上阻塞了TxnA,就会发生这种情况。

Timestamp Cache(时间戳缓存)

为了提供可串行化,每当一个操作读取值时,我们将操作的时间戳存储在时间戳缓存中,该时间戳显示正在读取的值的高水位线(shows the high-water mark for values being read.)。

每当发生写入时,都会根据时间戳缓存检查其时间戳。 如果时间戳小于Timestamp Cache的最新值,我们会尝试将其事务的时间戳推送到以后的时间。在可序列化事务的情况下,这可能导致它们在事务的第二阶段重新启动 (查看read refreshing部分)。

Read Refreshing

每当事务的时间戳被push时(has been pushed),在允许可序列化事务在该时间戳下提交前,都需要进行额外的检查:必须检查该事务先前读取的所有值,以验证在原始事务时间戳和push的事务时间戳之间没有发生写入。 此检查可防止破坏可串行化。检查是通过使用专门的RefreshRequest来跟踪所有读取来完成的。 如果成功,则允许事务提交(如果该事务被不同的事务或时间戳缓存push,或者遇到ReadWithinUncertaintyIntervalError时会执行检查,然后再继续执行)。

如果Refreshing失败,则必须用push的时间戳重试该事务。

与其他层的交互

事务 & SQL Layer

事务层从SQL层执行的planNodes接收KV操作。

事务 & Distribution层

TxnCoordSender将其KV请求发送到Distribution层中的DistSender

What's Next?

了解CockroachDB如何在Distribution Layer中呈现集群数据的统一视图。