ITPub博客

首页 > 数据库 > > Spanner事务漫谈

Spanner事务漫谈

作者:u_9ff4c058a0e0 时间:2022-06-02 14:14:30 0 删除 编辑

Spanner是谷歌开发的一款高可用、分布式、支持全球部署的数据库,它可以被看做是NewSQL的开山之作(虽然NewSQL这个词越来越被业界认识抵触了),在它之后,一大批分布式数据库(CockroachDB、TiDB、YugabyteDB......)如雨后春笋般崛起。本文主要讨论Spanner的事务实现。

一:整体架构

在介绍讨论事务实现之前,先简单介绍一下Spanner的架构作为铺垫。

图1:Spanner 集群

如上图所示是一个Spanner集群(universe)。它包括一个universemaster服务、一个placement dirver服务和多个Zone(类似于Bigtable中的server)。在集群运行过程中,可以给集群加入新的zone或者从集群中退役现存的zone。一个zone包含一个zonemaster和成百上千个spanserver,每个spanserver负责管理100~1000个tablet,每个tablet都是一段kv map(其数据模型是[(key:string, timestamp:int64)→string]),这些kv map都是LSM tree。zonemaster将数据分配给spanner,其实就是在spanserer里构建(写入)tablet,客户端根据location proxy定位到确定的spanserver获取数据服务。universemaster用于监控集群中每个zone的状态信息。placement driver定期与每个spanserver交互,找到那些由于副本的约束信息更新或者为了平衡负载而需要移动的数据。

图2:

为了实现高可用,spanner给一份数据维护多个副本(通常3~5个副本),具体实现就是每个spanserver在每个tablet之上维护一个paxos复制状态机。paxos用于实现table的一致性复制,所有对table的写操作,都必须在这个paxos组的leader上发起,然后同步复制到这个paxos的法定数量的副本之后才表示这个写操作成功。一个读操作可以在任何足够新(up-to-date)的副本上进行。

先铺垫这么多了,虽然Spanner还有很多其它特性,但是本文主要探讨Spanner的事务实现,所以在此不做介绍了。

二:事务实现

作为一款支持全球部署的分布式数据库,Spanner的最大的特点之一就是它提供了支持外部一致性的通用事务。在Spanner中,事务分为以下几种类型:Read-Write Transaction、Read-Only Transaction(包括Snapshot Read,它们的底层原理是相似的)和Schema-Change Transaction。实现这一切的基础,是Spanner实现了一个基于硬件的TrueTime时间戳接口,通过这个接口,使得Spanner能将集群的时钟偏移控制在10ms(下文一律用 [公式] 表示系统的最大时钟偏移)以内,这样再加上并发控制时的特殊处理,最终让Spanner给用户提供了支持外部一致性的通用事务(关键在于事务的延迟在用户可接受范围之内,如果不考虑事务延迟,那么即是时钟偏移有数百毫秒,也能通过特殊手段实现外部一致性,但是这样的事务由于高延迟是没有实用价值的)。在这里我们也不深究TrueTime接口是如何实现的,只需要知道通过TrueTime,系统的时钟偏移下降到 [公式] 。TrueTime提供三个主要的方法,TT.now()方法返回一个时间戳区间[earliest, latest],如果假设此时的绝对时间是t,那么这个区间就是[t- [公式] , t+ [公式] ],TT.after(t)返回true如果TT.now()的时间戳区间肯定已经过了时间戳t(其实也就是TT.now().earliest > t,TT.before(t)返回true如果TT.now()的时间戳区间肯定还没有到达时间戳t(即TT.now().latest < t)。

1:Spanner的外部一致性

Spanner在论文中并没有给出外部一致性(externally consistent)的严格定义,而只是说明了外部一致性的表现:如果一个事务T1在事务T2开始之前提交了,那么可以肯定T1的提交时间戳小于T2的开始时间戳。这其实也正是分布式系统中线性一致性(linearizability)的表现,他们也说明了外部一致性等价于线性一致性。这里会介绍一下什么是线性一致性。

在介绍线性一致性之前,首先介绍一下什么是Serializability。Serializability是数据库中的一个概念,表示的是数据库的隔离级别,在这种隔离级别下,数据库对多个并发事务的一个调度和将这些事务以某种顺序逐个串行产生的结果相同。比如如下场景:

#txn1           #txn2                     #txn1         #txn2
read(A);                                  read(A);
write(A);                                 write(A);
                read(A);                  read(B);
                write(B);      ====>      write(B);
read(B);                                                read(A);
write(B);                                               write(A);
                read(B);                                read(B);
                write(B);                               write(B);

左边的一个调度等价于右边txn1和txn2串行,这就是Serializability。它解决的事务之间的隔离性,因为这个概念形成于单机时代,这时事务由于都在同一机器上运行,拥有相同的时间参考系,所以是天然具有linearizability属性的。

图3

比如上图中,随着时间的推移,系统中发生了5个事务,其中txn2分别合txn3、txn4并行运行。在这样的场景中,我们虽然无法断定txn2与txn3、txn2与txn4的先后关系,但是我们可以肯定txn1的commit时间戳小于其余所有四个事务的begin时间戳,txn3的commit时间戳小于txn4、txn5的begin时间戳,同理txn2、txn4的commit时间戳都小于txn5的时间戳。也就是说txn1对数据库造成的影响肯定可以被剩余的四个事务都感受到,txn5也能感受到其他所有事务给数据库造成的影响。也就是说系统中所有事物存在全序关系。

但是,当系统进入分布式时代,由于时钟偏移问题,各个节点的时间参考系都不相同,仅能保证各个节点内部所有操作保持偏序关系,但是节点之间的的操作无法进行先后顺序的比较:

图3

比如上面的场景,在一个分布式数据库中,在node1节点执行了txn1(启动时间戳node1-t1,提交时间戳node1-t2),在node2节点执行了txn2(启动时间戳node2-t3,结束时间戳node2-t4),如果把这两个事务的开始、提交事件的发生时刻投影到绝对时间参考系(或者一个独立于node1、node2的第三方参考系),那么分别对应t1、t2、t3和t4。如果以单机的思维来看,因为txn2开始晚于txn1的提交,所以txn2必定能感受到txn1对数据库的影响(假设txn1、txn2存在相交的操作集)。但是在分布式系统中,情况不再如此简单,如果txn1、txn2都分别使用本地的时间戳(也就是分别使用本地时间参考系),由于各个机器的时钟存在偏移,可能往左偏(小),也可能往右偏(大)。一种可能的情况是,node1的时间相对于绝对(第三方)时间参考系向右(大)偏移了 [公式] ,而node2的时间向左(小)偏移了 [公式] ,也就是说txn的开始和提交时间戳变为了[t1+ [公式] , t2+ [公式] ],而txn2的开始和结束时间戳变为了[t3- [公式] , t4- [公式] ]。可能就会出现如下情况:

图4

也就是虽然在客户端看来txn1发生在txn2之前,但是系统内部由于时间戳偏移导致txn2虽然晚于txn1的commit启动,但是其实际的commit时间戳竟然比txn1的启动时间戳都小。这会导致在现实生活中出现一种令人难以置信的情况:某天你用手机给某人转了100元钱,系统明确提示你转账成功(这时手机连接node1完成了txn1)。然后你用电脑查询自己的账户余额(这时电脑连接上了集群的node2并完成了txn2)竟然发现和昨天的余额完全一样。你开始激动起来自己的钱并没有减少。于是你立刻使用手机又转了100(还是连接node1),同样提时转账成功。接着你又查了一次账户余额,但是这次电脑连接到了node1,然后你惊奇地发现账户一下子少了200元钱。于是你自闭了。

而线性一致性,就是要求系统中所有操作(事件)都保持全序关系,也就是说任何事件都能进行先后顺序的比较。比如上面的场景中,即使是node1上的txn1的commit和node2上的txn2的begin也是有先后顺序的。当你看到txn1 commit之后,再开启txn2,那么txn2的begin 时间戳肯定大于txn1的commit时间戳,即使这两个事务发生在不同的节点。这怎么样才能实现呢?当然如果没有时钟偏移,集群中所有的节点的时间都相同,那么肯定就是linearizability的,但是目前由于物理原理的限制不可能实现。我们应该意识到,上面场景中,txn1和txn2无法进行时间比较的原因是什么,那就是各个机器的时钟偏移都不同(比如node1右偏 [公式] ,而node2左偏 [公式] ),这导致各个机器上的事务的时间参考系都不相同,因此直接用它们自己的时间戳进行比较是没有意义的。如果能让各个节点上的事务都在同一个时间参考系中,那么它们也就可以直接比较了,这就是TSO中央授时技术,所有的事务都在一个专用的中心节点获取begin和commit时间戳,这样就绕开了各个节点的时钟偏移问题。但是它会给集群引入单点,这个单点容易成为性能瓶颈,同时也降低了系统的高可用性。

Spanner并没有采用这种技术,因为它要实现全球分布,通过TSO获取时间戳的延迟太高。它通过两方面的处理,实现了等价于linearizability的externally consistent。具体做法是:

1:控制时钟偏移

Spanner实现了一个基于硬件的(GPS和原子钟)TrueTime时间戳接口,用Spanner的话来说就是“这个接口直接暴露了时间的不确定性”,其实就是通过这个接口,可以获取一个时间区间[earliest, latest],尽管存在时钟偏移,但是偏移最早不会早于earliest,最晚不会晚于latest,如果假设绝对时间是t的话,那么这个区间就是[t- [公式] , t+ [公式] ]。通常Spanner能将集群中最大的时钟偏移 [公式] 控制在10ms以内。正是由于 [公式] 足够小,所以才能通过下面的方法实现外部一致性。

2:wait

因为通过TrueTime已经将时钟偏移控制地足够小了,所以Spanner实现外部一致性(externally consistent)的方式十分简单粗暴,那就是等待(wait)直到通过TrueTime获取的now时间区间的左边界也大于这个事务的提交时间戳,这样就不会由于时钟偏移导致事务乱序了。具体实现下面描述。

2:Paxos lease interval的不相交性保证

Spanner的paxos组中,通常有3~5个副本。 由leader提供写服务,读操作可以在任何足够新(up-to-date)的副本上进行。Spanner的paxos使用租期机制,leader的租期(lease)到期之后,就需要重新投票选举新的leader。在每次成功完成一次paxos写操作后,会隐式地延长leader的租期,如果没有写操作并且租期快要到期时,leader会自动发送lease request来延长租期,如果租期续租失败,那么就会重新选举leader。因此,在存在时钟偏移的情况下,如果不做特殊处理,那么可能存在的一种情况是,在同一时刻在同一个paxos组中出现了两个leader,如下:

图5

在上帝视角来看,在t时刻发生了leader的转移,replica1成为follower,replica2成为新的leader,并开始自己的leader 租期lease2。但真实的情况是,replica1所在的节点的时钟发生了偏移,它比绝对时钟(或者第三方时钟)快,实际上就replica1的视角来看,它是在t2时刻lease1到期的。同样由于时钟偏移,replica2所在的时钟比绝对时钟慢,在绝对时间为t时,它的时钟实际上才走到t1,这个时候它开始了自己的任期。这样就其它节点而言,在t1~t2这个时间区间([t1, t2]的最大跨度为2* [公式] ),它发现这个paxos组有两个leader,那它到底该和那个leader交互呢?

Spanner通过主动wait解决这个问题。每个leader记录它分配过的最大的时间戳 [公式] ,当自己的lease到期时,它不会立刻交出自己的权利,二是进行wait,直到TT.after([公式])==true,实际上也是TT.now().earliest > [公式] 。这样新节点的lease的下界最小也不会小于 [公式] 。从而保证了两个lease不会有重叠部分。

3:并发控制

Spanner的并发控制使用的都是比较成熟的技术,可以说它实现的就是标准的多版本两阶段封锁协议(应该是强两阶段封锁,即事务持有的read lock和write lock都必须持有到事务提交才释放),并通过两阶段提交实现分布式事务。它给事务进行了分类:ReadWrite事务、ReadOnly事务、Snapshot Read、SchemaChange事务。但是就本质而言,ReadOnly事务和Snapshot事务是一样的,只是给它们提供的时间戳不同,ReadOnly使用的是能读到它要读取的这个数据范围内所有数据项最新版本的时间戳,而Snapshot使用的是用户提供的时间戳,它根据这个时间戳读取能读到的数据版本。但是如果用户提供了一个最新的时间戳,那Snapshot其实就是ReadOnly,所以可以把它们都理解成是Snapshot Read,只不过ReadOnly读取的快照是这个事务启动时的数据库快照;其次,SchemaChange其实就是一个发生在未来的ReadWrite事务,在它的prepare阶段就会注册事务的提交时间戳,不过这个时间戳是主动选择在future的,所以它做schema change不用阻塞时间戳比它小的事务的执行,但是如果事务的时间戳比schema change事务的时间戳大,它还是要阻塞。

1:Spanner的分布式事务

为了进行并发控制,Spanner在每个paxos组的leader上都维护一个locktable,它用于管理本tablet的read lock和write lock。同时,每个leader还都维护着一个transaction manager,用于进行分布式事务的管理。如果一个事务的所有写操作都发生在同一个tablet,那么它就不需要通过transaction manager进行两阶段提交,直接通过locktable即可完成并发控制,这样这种事务就变成了“单机事务”,从而获得更高的性能。

Spanner将写操作在概念上做了区分,用户发起的写操作,在事务进入提交流程之前,都是缓存在client的,称之为client write,在进入提交流程之后,事务会将缓存起来的写操作发起paxos共识真正写入稳定存储,称之为paxos write。后续的讨论,其实基本上讨论的都是paxos write。

2:ReadWrite事务的实现

其面已经提到,Spanner的并发控制协议是两阶段封锁,所以ReadWrite事务的所有读操作需要申请read lock然后读取最新数据版本,所有写操作需要申请write lock。因为通过加锁进行并发调度,所以必然会引起死锁,Spanner通过wound-wait机制进行死锁处理(wound-wait在之前的死锁概述有过介绍)。如果一个事务的数据访问范围超过了一个tablet,必然会在多个tablet都产生锁,那么如果维护这些锁的活性呢?换句话说,当其它事务看到一个和它冲突的lock时,它怎么能确定持有这个lock的事务还处与pendding状态呢?Spanner对于这个问题的处理思路和之前介绍过的Percolator正好相反,它选择了悲观的处理方式,当事务产生lock后,client便会开启心跳定期给这些lock发送信息来告知自己还“活着”。这种处理措施在悲观高冲突场景优势更大,如果应用场景的冲突较少,那么这些心跳其实是在白白浪费带宽资源。

当事务执行完所有读操作并缓存了所有的写操作后,便进入两阶段提交流程。首先client会从所有写操作参与的paxos组中选择一个作为coordinator group,然后在它的leader上的transaction manager进行两阶段提交的处理。然后client会把缓存的所有写操作根据操作数据分发到对应的paxos组(当然是在leader)执行,在发送写操作的时候会携带coordiantor的身份信息,这样,每个participant都会知道coordinator在哪,以便和它交互。可以发现,这个两阶段提交流程是有client发起的,而不是coordinator,这样就不用先由client把所有信息发送给coordinator,再由coordinator发送给所有participate,可以减少一次中间的网络跳转。

图6

当participate paxos组的leader接收到两阶段提交的prepare请求后,它首先在locktable中申请相关写操作的write lock,然后选择一个时间戳称之为prepare timestamp,prepare timestamp需要大于这个paxos组(之所以是整个paxos而不是仅仅是leader,因为一个paxos组的时间戳是严格单调递增的)已经分配过的所有时间戳,然后记录下这个时间戳,本次记录需要发起一次paxos共识,从而把这个时间戳同步到这个组的所有follower。完了之后把这个时间戳发送给coordinator。

coordinator paxos组收到client的prepare请求后,它同样首先申请对应的write lock,但是不需要再分配prepare timestamp(因为最终需要再这里分配commit timestamp,在决定commit timestamp时有更严格的限制),而是等待所有的participate发来prepare timestamp。如果有任何一个participate paxos发来了错误信息或者在规定时间内没有发来prepare response,那么这个事务abort。在coordinator接收到所有的participate发来的prepare timestamp后,它会根据情况分配一个时间戳作为这个事务的commit timestamp,在此称之为s,s必须 大于等于所有participate的prepare timestamp、必须大于leadler接收到client发来的事务提交请求时的TT.now().latest、还必须大于leader所在的paxos之前分配过的所有时间戳(这就是为什么前面leader不需要分配prepare timestamp)。在选定了s之后,便是上面说过的wait了,leader必须等待直到TT.now().latest < s后才能通知所有participate和client事务提交。但是在commit wait的过程中,其实leader并行发起了一次paxos共识来把s同步到coordinator paxos组的所有follower,也就是说其实这个commit wait期间事务并没有闲着,如果给follower同步s花费的时间比s大,那么commit wait机制其实就不会产生额外的事务延迟,当然如果同步的时间更小,那么多出来的时间确确实实就是commit wait机制的导致的了。在完成commit wait之后,coordinator把事务提交时间戳s告知所有的participate和client,participate paxos组的leader把s通过一次paxos共识同步到所有的follower。至此完成了两阶段提交的所有流程。


3:ReadOnly事务的实现

如果一个事务中所有的操作都是读取操作,由于它不会对数据作出任何更新,因此可以充分利用多版本的优点,通过让该事务读取特定的数据库快照从而省略掉加共享锁的流程,但是一个事务在运行过程中,谁也没有办法保证它的下一条语句会不会是写操作,因此对于这种事务必须要在事务开始时明确标记它是ReadOnly的。这样标记之后,这种事务就不允许再执行写操作了。实现ReadOnly事务的关键是获取到合适的数据库快照,也就是read timestamp,只有时间戳选的好,才能达到真正的快照隔离级别的读性能。

Spanner提出了一种read timestamp(后续称之为[公式] )的获取方式。它把ReadOnly事务同样分为两阶段来实现。第一阶段,称之为negotiation(谈判)阶段,这个阶段的目的是确定 [公式] 。Spanner根据这个ReadOnly事务要访问的所有数据确定一个范围scope。如果这个scope只属于一个paxos组,那么Spanner直接把读操作发送到这个Paxos组的leader,然后把这个paxos组的LastTS(LastTS如何确定我们后面讨论)确定为 [公式] ,接着便用这个时间戳执行读取即可。如果这个scope跨越了多个paxos组,那么在执行读取之前要和所有参与的paxos进行一轮negotiation,从这些paxos组的所有leader获取各个组的LastTS,然后根据所有LastTS取定 [公式] ,然后再用这个时检戳在各个paxos的leader上进行读取。

LastTS如何确定呢?Spanner定义LastTS是上一次提交的事务的commit timestamp。这个paxos组的leader的transaction manager没有prepared事务,那么它能立刻返回,但是当存在prepared事务时,只能等待直到这些事务commit或者abort。那么存在的问题就是,即使处与prepared的事务和ReadOnly事务不冲突,它还是会delay ReadOnly事务获取LastTS。Spanner给出的思路是,在locktable中维护(key-ranges)—>(commit timestamp)这样的map,当ReadOnly事务到达时只确认跟它冲突的事务是否处与prepared状态,如果没有即可立刻返回,如果有,也只是被和它冲突的事务阻塞了,而不是其它无关的事务。

但是,对于scope涉及多个paxos组的ReadOnly事务,Spanenr并没有按照上面介绍的negotiation方法确定 [公式] ,因为这中方法会产生多次额外的网络交互(和多个leader进行negotiation),其次read操作还只能在leader上进行(因为要根据LastTS确定 [公式] ,而follower的LastTS可能落后于leader的,因为log是异步apply的),这就丧失了Spanner的follower read的优势(或者说是在任何up-to-date的副本上读)。因此,对于这种情况,Spanenr直接选择TT.now().latest作为 [公式] 。当然这种做法可能由于访问的那个副本还不够新(也就是还不满足up-to-date)而需要等待直到它up-to-date了。如何判断一个副本是不是up-to-date,我们会在下面介绍。


4:SchemaChange事务的实现

SchemaChange也是数据库中的一个难题,如果用通用事务实现SchemaChange,必然会阻塞所有也其它事务,如果表很大,SchemaChange会花费数小时甚至数天时间,难道正常业务也需要停这么长时间?

Spanner的SchemaChange事务是通用事务的变体,通过一个巧妙的设计,实现了“non-blocking”特性。具体实现就是,SchemaChange事务会在prepare阶段之前先选定事务的commit时间戳,记为t。这个t是在选择在未来的,任何通用的读写事务都是隐式依赖于Schema信息的,如果发现加锁,则根据加锁确定SchemaChange事务的t,通过和自己的时间戳比较,如果自己的时间戳小,则可以正常执行,否则阻塞直到SchemaChange事务提交。

后来的CockroachDB根据F1的论文通过分阶段的思路也实现了Online Schema Change,之后有时间了我们详细讨论。


5:follower read

Spanner是支持在paxos组的follower也执行读操作的,用Spanner的话来说就是可以在任何up-to-date的副本执行读取操作,不过对执行follower read的操作有限制,具体来说:因为ReadWrite事务的读操作读取的是最新版本的数据并且需要申请read lock,follower的数据还没有追到和leader同样的版本(因为log是异步apply的),同是locktable只存在于leader,所以ReadWrite事务的读操作只能在leader执行;ReadOnly事务,如果访问的scope是在一个paxos组中,这时[公式] 完全由这个paxos组的leader的LastTS决定(同样follower的数据可能还没有追到和leader相同),因此这种情况也只能在leader执行读取。剩余的情况,包括ReadOnly事务跨多paxos读取(时间戳直接通过TT.now().latest方式获取)和Snapshot read都可以根据提供的时间戳在任何up-to-date的副本执行读操作。

那么如何判断一个副本是up-to-date的呢?Spanner在每个replica都记录一个时间戳称之为 [公式] ,任何读操作的 [公式] 只要小于等与[公式],那么对于这个读操作来说,这个副本便是up-to-date的,便可顺利读取。[公式]由两方面确定,首先,每个副本它最近一次paxos write完成的时间,我们称之为 [公式] ,理所当然的,任何读操作都必须等到数据写入了才能读取,所以就需要[公式]<= [公式] ;其次,想象这样一个场景:一个ReadOnly事务执行过程中,由一个ReadWrite事务和它在同一paxos组并行推进。那么对于这个ReadOnly事务而言,它该怎么确定这个ReadWrite事务是不是会影响它呢(也就是说,这个ReadWrite更新的数据集到底再不再它的读取scope之内呢)?因此,采取了一个简单粗暴的处理措施,那就是排除掉paxos组所有处与prepared阶段的事务,具体做法是:定义一个时间戳叫 [公式] ,如果它比这个paxos组中的所有处与prepared的事务的commit timestamp都小,那么就可以排除掉这些事务了,但是对于participate paxos组,它们是无法知道事务最终的commit timestamp的,回忆上面对于ReadWrite事务的介绍,commit timestamp是在coordinator paxos组的leader通过所有的participate发来的prepare timestamp最终确定的,也就是说,对于participate,它虽然无法知道事务的commit timestamp,但是知道这个事务在这个paxos组最早能提交的时间戳prepare timestamp,因此 [公式] 就退而求其次,确定为一个paxos组的leader的transaction manager中所有处与prepared阶段的事务的prepare timestamp的最小值-1,因为每个事务记录prepare timestamp都会发起paxos共识,所以paxos组的follower也是能得到 [公式] 的。在确定了 [公式] 和 [公式] 后,便可以将 [公式] 确定为min( [公式] , [公式] ),当然如果transaction manager中没有prepared的事务,那么就可以不用考虑prepared事务的影响,所以 [公式]就可以是 [公式] 。

通过这种方式确定的 [公式] 存在两个问题:

(1)如果这个paxos组很长时间都没有发生过写操作了,那么 [公式] 就会很小,这个时候发生这种读操作,可能会被阻塞很长时间直到再发生一次paxos write把 [公式] 更新得更大。很明显这种阻塞是没有必要的。Spanner处理这个问题的思路也很巧妙,在paxos write更新 [公式] 的同时,如果没有paxos write发生,随着时间的推移,这个paxos分配给下一个paxos write的最小时间戳也在变大(因为在paxos组中时间保持严格单调递增),比如在t=1时刻发生过一次paxos write,然后过了100s都没有发生过paxos write,虽然write 没有发生,但是我们可以确定,即使发生了,它的时间戳最小也是101了,所以我们就可以将 [公式] 更新到100了。Spanner会每隔8s更新一次这个paxos组的MinNextTS,所以即使没有paxos write发生,一个快照读操作在任何副本最多阻塞8s就可以执行读操作了。

(2) [公式] 也可能会造成不必要的阻塞,比如一个快照读要读数据项a,它的读时间戳是3,但是在transaction manager中有多个(这里假设3个)处与prepared的ReadWrite事务分别影响了b、c、d,它们的prepare timestamp分别是1、2、1。按理来说,本次读操作是不会被阻塞的(我们假设 [公式] 是5),但是但是由于存在prepared事务得到的 [公式] =1,最终 [公式] =1,小于本次快照读的时间戳,所以本次读只能阻塞。和处理LastTS问题的思路类似,Spanner通过一个(key ranges)—>(prepare timestamp)的map在更细的粒度上跟踪时间戳,当快照读到来时,只检查和它冲突的数据的prepare timestamp,这样便能解决这个问题。

三:疑问

Spanner说locktable只存在于leader,那么如果发生leader迁移,旧leader上的locktable如何处理呢?如果不同步迁移,那岂不是会在造成一些事务的lcok丢失?这大概率会引发一致性问题;如果在leader迁移到其它节点时同步迁移locktable,会造成leader迁移的性能问题吗?



参考文献:

[1] 《Spanner: Google’s Globally-Distributed Database》

来源 https://zhuanlan.zhihu.com/p/449151546

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69999831/viewspace-3159975/ ,如需转载,请注明出处,否则将追究法律责任。

下一篇: TIBCO Rendezvous
请登录后发表评论 登录
全部评论

注册时间:2022-02-11

  • 博文量
    1828
  • 访问量
    20573