yugabytedb架构之DocDB事务层

寄语天涯客,轻寒底用愁,春风来不远,只在屋东头

DocDB事务层

Yugabytedb的分布式ACID事务架构参考了Google Spanner

DocDB内的所有更新操作都认为是事务,包括如下两种情况:

只更新一行的操作和更新驻留在不同节点上的多行的操作。

设置了autocommit模式。

时间同步

YugabyteDB集群中的事务可能需要更新跨集群中节点的多行。为了与ACID兼容,此事务所做的各种更新应该在固定时间内立即可见,而与集群中读取更新的节点无关。为了实现这一点,集群中的节点必须就时间的全局概念达成一致。

要是集群中的不同节点在时间上达成一致,需要所有节点都能够访问一个高可用且全局同步的时钟,Google Spanner使用的是TrueTime,一个高可用全局同步的时钟示例,具有严格的错误界限。但是这种时钟在许多部署中都不可用,物理时钟无法在节点之间完全同步。因此,他们不能跨节点排序事件。

Hybrid Logical Clocks(HLCs)混合逻辑时钟

HLCs将使用NTP粗略同步的物理时钟与跟踪因果关系的Lamport时钟相结合来解决问题。每个节点先计算自己的HLC,HLC是一个二元组。由物理时间和逻辑时间组成。任何节点上生成的HLC都是严格单调的,比较两个HLC时,先比较物理时间再比较逻辑时间。

物理时间:使用节点的物理时钟(Linux中的clock_REALTIME)初始化HLC中的物理时间。一旦初始化完成,物理时间只能更新为更大的值。

逻辑时间:对于给定的物理时间,HLC的逻辑时间是一个单调递增的数字,提供了在同一物理时间内发生的事件的顺序。初始值为0,只要物理时间更新,逻辑时间就会被重置为0。

在两个节点之间的任何RPC通信中,都会交换HLC。HLC较低的节点会将其HLC更新为较大的值。如果节点上的物理时间超过其HLC的物理时间,则将后者更新为物理时间,并将逻辑时间设置为0,因此节点上的HLC是单调递增的。

HLC用来确定读取点,以确定哪些更新对最终客户端可见。如果已经根据Raft协议将更新安全的复制到多数派节点上,则可以向客户端回应更新操作成功,并且安全的为该HLC提供所有读取服务,这为YugabyteDB支持MVCC奠定了基础。

使用HLC的MVCC

每个key的最后一部分是时间戳,他可以快速定位到RocksDB中key-value存储的特定版本。

Yugabytedb在很多方面使用了混合逻辑时钟,如下所示:

分配给同一个tablet已提交Raft日志的混合逻辑时间戳,即使有Leader变更,也始终不断递增。因为新的Leader始终有来自前一个Leader的所有已提交日志条目,并确保在追加新条目之前使用最后提交条目的时间戳更新其混合逻辑时间戳,这种方式简化了为单个Tablet读取请求选择安全混合时间戳的处理逻辑。

尝试在特定混合时间从tablet读取数据的请求需要确保tablet中没有低于读取时间戳的更改,这样可能会导致结果集不一致。在跨多个tablet进行事务性读取操作时,需要在特定的时间戳读取tablet。由于读取时间戳被设置为处理读取请求的YB-Tserver上的当前混合时间,因此该条件更容易满足,所以我们读取的tablet上的混合时间立即被更新为至少与读取时间戳一样高的值。然后,读取请求只需等待Raft队列中时间戳低于读取时间戳的任何相关条目被复制并应用到RacksDB后,它可以继续处理读取请求。

事务执行路径

单行事务

事务管理器自动检测是否为单行事务。为了提升性能,对单行的更新直接更新该行,而无需使用单行事务路径与事务状态tablet交互。

分布式事务

执行分布式事务路径,需要使用事务管理器,该事务管理器可以协调作为事务的操作,并根据需要提交或终止事务。

 事务隔离级别

SQL92定义了四种事务隔离级别,分别为:串行化、重复读、读提交和读未提交。

YugabyteDB支持两种类型的隔离级别:串行化和快照(对应的隔离级别是重复读)

DocDB内的锁

为了支持上述两种隔离级别,锁管理器支持三种类型的锁

快照隔离级别的写锁

快照隔离事务根据其修改的值获取这种类型的锁。

串行化读锁

可序列化读-修改-写事务对其读取的值采用这种类型的锁,以确保在事务提交之前不会修改这些值。

串行化写锁

这种类型的锁由串行化事务对其写入的值以及纯写入快照隔离事务获取,因此,写入同一项的多个快照隔离事务可以并行进行。

yugabytedb架构之DocDB事务层

 

 细粒度锁

我们进一步区分了在DocDB节点上获取的锁(由任何事务写入或由读-修改-写可序列化事务读取)和在其父节点上获取的锁。我们把前者称为“强锁”,后者称为“弱锁”。例如,如果SI事务正在将col1设置为行1中的新值,则它将在行1上获取弱SI写锁,但在行1.col1上获取强SI写锁。基于这种区别,完成的冲突矩阵如下所示:

yugabytedb架构之DocDB事务层

 

 下面有几个示例来解释上述矩阵可能出现的并发场景:

多个SI事务可以同时修改同一行中的不同列。它们在行键上获得弱SI锁,在写入的各个列上获得强SI锁,行上的弱SI锁彼此不冲突。

多个只写事务可以写入同一列,并且它们在此列上获取的强可串行化写锁不会冲突。使用混合时间戳确定最终值(最新的混合时间戳获胜)。纯写SI和可串行化写操作使用相同的锁类型,因为它们与其他锁类型共享相同的冲突模式。

下面介绍yugabytedb中的锁机制

yugabytedb的事务层既支持乐观锁也支持悲观锁。

并发控制

DocDB拥有由查询层执行的写入临时记录的功能。临时记录用于对行上的持久锁进行排序,已检测冲突。临时记录有一个与之相关的优先级,这是一个数字,当两个事务冲突时,优先级较低的事务将终止。

Yugabytedb目前支持乐观并发控制,正在积极研究悲观并发控制。

行级锁

 yugabytedb和PostgreSQL类似,支持大多数行级锁。但是,一个区别是yugabytedb使用乐观并发控制,不阻塞/等待当前持有的锁,而是终止低优先级的事务。

显式行级锁使用事务优先级来确保两个事务永远不会在同一行上持有冲突的锁。这是由查询层在悲观并发控制下为运行的事务分配非常高的优先级来完成的。这会导致与当前事务冲突的其他事务失败,因为它们的事务优先级较低。

行级锁的类型

For Update

 FOR UPDATE锁会导致select语句检索到的行被锁定,就像用于更新一样,防止当前事务结束之前被其他事务锁定、修改或删除。在先前锁定的行上执行下述操作将会返回失败:UPDATE, DELETE, SELECT FOR UPDATE, SELECT FOR NO KEY UPDATE, SELECT FOR SHARE or SELECT FOR KEY SHARE。

在行上执行任何删除操作或者修改某些列上值都需要获取FOR UPDATE锁。

FOR NO KEY UPDATE

和FOR UPDATE锁的行为类似,只是获取的锁较弱:该锁不会阻塞同一行上的SELECT FOR KEY SHARE。该锁也用于未获取FOR UPDATE锁的UPDATE操作。

FOR SHARE

和FOR NO KEY UPDATE锁的行为类似,只是它在每个检索到的行上加共享锁而不是独占锁,会阻止其他事务执行UPDATE, DELETE, SELECT FOR UPDATE or SELECT FOR NO KEY UPDATE操作,但不会阻止他们执行SELECT FOR SHARE or SELECT FOR KEY SHARE。

FOR KEY SHARE

和FOR SHARE锁的行为类似,只是锁定较弱:会阻塞SELECT FOR UPDATE,不阻塞SELECT FOR NO KEY UPDATE。阻塞其他执行DELETE或任何更新特定值操作的事务,但不会阻塞UPDATE其他值的操作,也不会阻塞SELECT FOR NO KEY UPDATE, SELECT FOR SHARE, or SELECT FOR KEY SHARE。

单行事务

yugabytedb为同一个shard(partition,tablet)中的一行或多行修改提供了ACID语义,通过一次网络交互就能完成。即使是读修改操作,只要是单行或者在单个shard内,就可以在一次网络交互中完成,比如

update table set x=x+1 where ...

insert ... if not exists

update... if exists

这点与Apache Cassandra不同,Apache Casandra使用轻量级事务的概念来实现这些读修改写操作的正确性,并导致4个网络往返的延迟。

从Leader那读取最新数据

集群正常运行时,当Leader追加和复制日志条目时,最新的复制到多数派节点上的日志就是要提交的。然而,在领导者切换之后,情况就复杂了。当tablet选出新的Leader时,按raft协议的描述它会追加一个空的日志条目到tablet的raft日志中并复制到其他节点。在这条空的日志条目被复制之前,我们认为该tablet对读取最新值和接收读修改写操作都是不可用的。这是因为新的tablet Leader需要保证之前raft leader提交的日志条目都已经应用于RocksDB和其他需要持久化的信息以及内存中的数据结构,并且只有在我们知道新的Leader日志中的所有条目都提交之后,才有可能。

Leader租约:在网络分区的情况下读取最新数据

Leader租约是tablet Leader在短时间内建立其权利的一种机制,以避免下述的不一致情况:

Leader与其他follower之间产生网络分区

新的Leader被选举出来

客户端写入新值,并且新Leader将新值复制到其他节点

客户端从旧的Leader那读取过时的值

yugabytedb架构之DocDB事务层

 

 yugabytedb中使用Leader租约机制来避免这类问题。工作机制如下所述:

对于每个leader-to-follower消息(Raft术语中的AppendEntries),无论是复制新条目还是空心跳消息,leader都会以一定的时间间隔发送一个“leader-lease”请求,例如“我想要一个2秒的租约”。租赁期限通常是一个系统范围的参数。对于每个peer,leader还跟踪对应于每个挂起请求的租约到期时间(即发送请求的时间+租约持续时间),该时间以本地单调时间(在Linux中为CLOCK_monotonic)的形式存储。为此,leader将自己视为“peer”的特例。然后,当它从follower那里接收响应时,就把这些复制水印保存下来。leader采用此多数复制水印作为其租约到期时间,并在决定它是否可以服务于一致的读取请求或接受写入时使用它。

当Follower收到上述Raft RPC时,它读取其当前单调时钟的值,并加上租约间隔,作为租约到期时间,也记住其局部单调时间。如果该Follower后来成为Leader,在任何潜在的旧Leader租约到期之前,该Leader不会提供一致性读或接收写入服务。

为了保证任何新Leader都知道任何旧Leader的租约到期,还需要一点逻辑。Raft组内的每个成员都会记录它所知道的旧Leader的最新过期时间(根据该服务器的本地单调时间)。每当节点响应RequestVote RPC时,它都会在其响应中包含已知旧Leader租约的最大剩余时间。这与接收节点上leader的AppendEntries请求中的租约期限类似:在接收节点成为leader时,必须经过至少一段时间才能处理最新的请求。算法的这一部分是需要的,这样我们就可以证明一个新的Leader总是知道任何旧Leader的租约。这类似于Raft的正确性证明:总有一个节点(“voter”)接收到旧Leader的租赁请求并投票给新Leader,因为两个多数派必须重叠。

请注意,对于这种leader租约的实现,我们不依赖任何类型的时钟同步,因为我们只在网络上发送时间间隔,并且每个节点根据其本地单调时钟运行。时钟实现的两个要求是:

不同服务器之间有界单调时钟漂移率。例如,如果我们使用低于每秒500µs漂移率的标准Linux假设,我们可以通过将上述所有延迟乘以1.001来解释。

单调的时钟不会冻结。例如,如果我们在一个暂时冻结的虚拟机上运行,当虚拟机再次开始运行时,虚拟机监控程序需要从硬件时钟中刷新虚拟机的时钟。

 leader lease机制保证在任何时间点,tablet的Raft组中最多有一个节点认为自己是最新的leader,可以为一致的读或写请求提供服务。

读请求的安全时间戳分配

每个读取请求都被分配一个特定的MVCC时间戳/混合时间(我们称之为ht_read),这允许对同一组key的写入操作与读取并行进行。然而,至关重要的是,截至此时间戳的数据库视图不能通过同时发生的写操作来更新。换句话说,一旦我们为读取请求选择了ht_read,就不能为同一组key 的并发写,分配小于或等于ht_read的时间戳。正如我们前面提到的,我们严格地为任何给定tablet的Raft日志条目分配递增的混合时间。因此,安全分配ht_read的一种方法是使用最后提交记录的混合时间。由于提交的Raft日志记录永远不会被未来的Leader覆盖,并且每个新Leader都会读取最后一个日志条目并更新其混合时间,因此所有未来记录的混合时间都将严格递增。

然而,使用这种保守的时间戳分配方法,在特定的tablet上没有写操作时,ht_read将一直不变。如果使用TTL(生存时间),将导致客户端观察到异常:就客户端而言,在将新纪录写入tablet之前,过期值不会消失,随着新纪录的写入,很多过期值可能会突然消失。为了防止这种异常,在分配读时间戳时,需要与当前混合时间接近(反过来又接近物理时间),以保留自然的TTL语义。因此,我们尝试选择ht_read作为最大的时间戳,以保证tablet后续写入操作的混合时间严格高于该时间戳,即使在领导者变更期间也是如此。

为此,我们需要引入“混合时间Leader租约”的概念,类似于上一节讨论的“绝对时间Leader租约”。对于每个发送给Follower的Raft AppendEntries请求,无论是常规请求或者是空的/心跳请求,tablet的Leader都会计算“混合时间租约到期时间”,简称ht_lease_exp,并将其发送给Follower,ht_lease_exp通常计算为当前混合时间加上固定配置的持续时间(eg 2秒)。在回复中,Follower承认旧Leader有权分配混合时间(包括ht_lease_exp)。与普通租约类似,这些混合时间租约通过投票传播。Leader维护多数派的复制水印,在它发送混合时间Leader租约时,它认为自己已经复制了该值。

安全时间的定义

假设当前多数派节点复制的混合时间Leader租约到期时间是replicated_ht_lease_exp,则读请求的安全时间戳可计算为最大值:

最后一次Raft日志提交的混合时间

或者下面的其中之一:

如果Raft日志中有未提交的日志条目:第一个未提交日志条目的混合时间最小值-ε(混合时间中的最小差异)和replicated_ht_lease_exp。

如果Raft日志中没有未提交的日志条目:则取当前混合时间和replicated_ht_lease_exp的最小值。

换句话说,最后提交的日志条目的混合时间始终可以安全读取,但对于较高的混合时间,多数派节点的混合时间Leader租约是一个上限。这是因为我们只能保证,如果ht<replicated_ht_lease_exp,未来的Leader将不会提交混合时间小于ht的条目。

请注意,当从单个tablet上读取时,我们永远不必等待所选的ht_读取变得安全,因为它已经被选择为安全读取。但是,如果我们决定在多个tablet上读取一致的数据视图,可以在其中一个tablet上选择ht_read,我们必须等待时间戳在第二个tablet上安全读取。这通常会很快发生,因为第二个tablet的Leader上的混合时间将立即用第一个tablet的Leader传播的混合时间进行更新,在常见情况下,我们只需等待混合时间小于ht_read的未决Raft日志条目提交即可。

将安全时间从Leader传播到Follower,以便在Follower上读取

yugabytedb支持Follower读,以满足需要极低读取延迟的场景,而这种延迟只能通过在离客户端最近的数据中心提供读取请求来实现。这个特性的代价是可能读取到旧一点的数据,这是应用程序开发人员必须做出的权衡,与强一致的Leader读取类似,Follower端的读取也必须选择一个安全的读取时间戳。

如前所述,“安全读取时间”意味着在读取时间戳之前,未来的写入不会改变数据的视图。但是,只有Leadeer能够使用上一节中描述的算法计算安全读取时间。因此,我们在RPC上将最新的安全时间从Leader传播给Follower。这意味着,例如,从处于网络分区的Follower端读取将看到数据的“冻结”快照,包括指定了TTL但未超时的值。当网络分区修复后,Follower将开始从Leader那里获得更新,并且能够返回非常接近最新的读取结果。

分布式ACID事务

临时记录

正如YugabyteDB将单个分区ACID事务写入的值存储到DocDB中一样,它需要将分布式事务写入的未提交值存储在类似的持久数据结构中。但是,我们不能将它们作为常规值写入DocDB,因为它们将在不同的时间对通过不同tablet读取的客户端可见,从而允许客户端看到部分应用的事务,从而打破原子性。因此,YugabyteDB将临时记录写入负责事务试图修改的key的所有tablet。我们称它们为“临时”记录,而不是“常规”(“永久”)记录,因为在事务提交之前,读不到它们。

临时记录存储在同一tablet peer中的单独RocksDB实例中。与其他可能的设计选项(例如,将临时记录与常规记录内联存储或将它们与常规记录一起放在同一RocksDB实例中)相比,我们选择的方法具有以下优点:

扫描所有临时记录很容易。正如我们将看到的,这对于清理中止/放弃的事务非常有用。

在读取数据期间,我们需要与常规记录非常不同的方式处理临时记录,并且将它们放在RocksDB key空间的单独部分可以简化读取数据。

将临时记录存储在一个单独的RocksDB实例中允许我们对其采用不同的存储、压缩和刷新策略。

临时记录的编码细节

有三种类型的RocksDB键值对与临时记录相对应,省略了将这些记录置于RocksDB中所有常规记录之前的单字节前缀。

yugabytedb架构之DocDB事务层

 

事务IO路径

写路径

让我们了解单个分布式只写事务的生命周期。假设我们试图修改带有键k1和k2的行。如果它们属于同一个tablet,我们可以将此事务作为单个shard事务执行,在这种情况下,原子性将得到保证,因为两个更新都将作为同一个Raft日志记录的一部分进行复制。然而,在最一般的情况下,这些key将属于不同的tablet,从现在开始我们将假设这一点。

下图显示了分布式只写事务的关键步骤,不包括任何冲突解决。

yugabytedb架构之DocDB事务层

 

 1.客户端请求事务

客户端向需要分布式事务的YugabyteDB tablet服务器发送请求。下面是一个使用我们对CQL的扩展的示例:

START TRANSACTION;

UPDATE t1 SET v = 'v1' WHERE k = 'k1';

UPDATE t2 SET v = 'v2' WHERE k = 'k2';

COMMIT;

接收事务性写入请求的tablet服务器将负责驱动此事务中涉及的所有步骤,如下所述。事务步骤的编排由我们称为事务管理器的组件执行。每个事务都由一个事务管理器处理。

2.创建事务记录

我们分配一个事务id并选择一个事务状态tablet,该tablet将跟踪具有以下字段的事务状态记录:

Status(pending,committed,或者aborted)

Commit hybrid timestamp(如果已提交)

List of ids of participating tablets(如果已提交)

选择事务状态tablet时,需要确保事务管理器的tablet也是其Raft组的Leader,因为这样可以减少查询和更新事务状态时的RPC延迟。但通常情况,事务状态tablet可能和启动事务的tablet不是同一个。

3.写临时记录

我们开始将临时记录写入包含需要修改的行的tablet。这些临时记录包含事务id、我们尝试写入的值和临时混合时间戳。此临时混合时间戳不是最终提交时间戳,并且对于同一事务中的不同临时记录,它通常是不同的。相反,整个事务只有一个提交混合时间戳。

在编写临时记录时,我们可能会遇到与其他交易的冲突。在这种情况下,我们必须中止并重新启动事务。在一定的重试次数内,这些重新启动对客户端仍然是透明的。

4.提交事务

当事务管理器写入所有临时记录时,它通过向事务状态tablet发送RPC请求来提交事务。只有当事务没有因为冲突而终止时,提交操作才会成功。提交操作的原子性和持久性由事务状态tablet的Raft组保证。提交操作完成后,所有临时记录立即对客户端可见。

事务管理器向事务状态tablet发送的提交请求包含所有参与事务的tabletid。显然,到目前为止,没有新的tablet可以添加到该集合中。事务状态tablet需要这些信息来协调清理参与的tablet中的临时记录。

5.将响应发送回客户端

YQL引擎将响应发送回客户端。如果任何客户端(相同或不同的客户端)发送对已写入key的读取请求,则能读到新值,因为事务已提交。数据库的这个属性有时称为“读自己写”保证。

6.异步应用和清理临时记录

此步骤由事务状态tablet在接收到事务的提交消息并成功复制对其Raft组中事务状态的更改后进行协调。事务状态tablet已经知道哪些tablet正在参与此事务,因此它会向它们发送清理请求。每个参与tablet在其Raft日志中记录一条特殊的“应用”记录,其中包含事务id和提交时间戳。在参与的tablet中复制此记录时,tablet会删除属于该事务的临时记录,并将具有正确提交时间戳的常规记录写入其RocksDB数据库。现在,这些记录与常规单行操作编写的记录几乎无法区分。

一旦所有参与的tablet成功处理了这些“应用”请求,事务状态tablet就可以删除交易状态记录,因为尚未清理临时记录的tablet的所有副本(如慢Follower)都将根据这些tablet中本地可用的信息进行删除。状态记录的删除是通过在事务状态tablet的Raft日志中写入一个特殊的“applied everywhere”条目实现的。属于此事务的Raft日志条目将在此后不久从事务状态tablet的Raft日志中清除,作为旧Raft日志的定期垃圾收集的一部分。

读取路径

YugabyteDB是一个MVCC数据库,这意味着它在内部跟踪相同值的多个版本。读取操作不带任何锁,并且依赖MVCC时间戳来读取数据的一致快照。一个长时间运行的读操作(单分片或交叉分片)可以与修改同一key的写操作同时进行。

在ACID事务部分,我们讨论了如何从单个shard(tablet)执行最新读取。在这种情况下,一个key的最新值就是raft知道的最后一个提交的raft日志记录所写的值。但是,为了从不同的tablet读取多个key,我们必须确保读取的值来自数据库最近的一致快照。下面是我们必须选择的快照的这两个属性的说明:

Consistent snapshot:快照必须完全显示任何事务的记录,或者根本不显示它们。它不能包含事务写入的值的一半,而忽略另一半。我们通过在特定的混合时间执行所有读取(我们称之为ht_read)并忽略任何混合时间较高的记录来确保快照的一致性。

Recent snapshot:快照包括任何客户端可能已经看到的任何值,这意味着在启动此读取操作之前写入或读取的所有值。这还包括客户端应用程序的其他组件可能已写入或从数据库读取的所有以前写入的值。执行当前读取的客户机可能依赖于结果集中这些值的存在,因为客户机应用程序的那些其他组件可能已经通过异步通信通道将此数据传递给当前客户机。当确定选择的混合时间太低时,我们通过重新启动读取操作来确保快照是最新的,也就是说,在启动读取操作之前可能已经写入了一些记录,但混合时间高于当前选择的ht_read时间。

yugabytedb架构之DocDB事务层

 

 1.客户端的请求处理和读取事务初始化

客户端对YQL或YEDIS或YSQL API的请求到达tablet的YQL引擎。YQL引擎检测到查询从多个tablet请求行,并启动只读事务。为请求选择混合时间ht_read,可以是YQL引擎tablet上的当前混合时间,也可以是其中一个tablet的安全时间。后一种情况至少可以减少tablet的安全等待时间,因此性能更好。通常,由于我们的负载平衡策略,接收请求的YQL引擎还将承载请求正在读取的一些tablet,从而允许实现更高性能的第二个选项,而无需额外的RPC往返。

我们还选择一个时间点,我们称之为global_limit,计算为物理时间+最大时钟偏移(max_clock_skew),它允许我们确定在读取请求开始后是否确实写入了特定记录。max_clock_skew是在不同YugabyteDB服务器之间全局配置的时钟偏差绑定。

2.根据选择的混合时间读取所有tablet

YQL引擎向事务涉及到的所有tablet发送读取请求。根据我们对安全时间的定义,每个tablet都会等待ht_read成为一个安全的读取时间,然后从其本地DocDB开始执行其部分读取请求。

当tablet看到具有混合时间ht_read的相关记录时,它会执行以下逻辑:

如果hr_record<=ht_read,结果中包含该记录

如果ht_record>definitely_future_ht,则从结果中删除该记录。definitely_future_ht的含义是,它是一个混合时间,在我们的读请求开始后,有更大混合时间的记录被写入。你可以假设definitely_future_ht是当前global_limit的上限。

如果ht_reads<ht_record<=definitely_future_ht,我们不知道这个记录是在我们读取请求之前还是之后写入的,但我们不能从结果中忽略它,因为如果它是在读取请求之前写入的,这可能会导致客户端观察到的不一致性。因此,我们必须用更新的ht_read=ht_record来重启整个读操作。

为了防止这些读取重新启动的无限循环,我们还向YQL引擎返回一个依赖于tablet的混合时间值local_limittablet,计算为该tablet中的当前安全时间。我们现在知道,在我们的读取请求开始之前,不可能写入任何混合时间高于本地tablet的记录(常规或临时)。因此,如果我们在以后尝试在同一事务中从该tablet读取时看到混合时间高于本地限制tablet的记录,我们就不必重新启动读取事务,并且我们在将来的尝试中明确设置了definitely_future_ht = min(global_limit, local_limittablet)。

3、查询事务状态

当每个参与的tablet从其本地DocDB读取数据时,可能会遇到临时记录,但它还不知道这些记录的最终事务状态和提交时间。在这些情况下,它将向transaction status tablet发送事务状态请求。如果事务已提交,则将其视为DocDB已包含混合时间等于事务提交时间的永久记录。临时记录的清理是独立和异步进行的。

4、tablets回应YQL

每个tablet给YQL的响应包含以下内容:

是否需要读取重新启动

local_limit tablet to be used to restrict future read restarts caused by this tablet.

从tablet中读取的实际值

5、YQL发送响应给客户端

一旦来自所有参与tablet的所有读取操作成功,并且确定无需重新启动读取事务,将使用适当的wire协议向客户端发送响应。

上一篇:技术管理进阶——Leader的模型、手段及思维


下一篇:Zookeeper 技术内幕