Spanner 实现允许一个 Paxos 领导者通过把 slave 从租约投票中释放出来这种方式,实现领导者的退位。为了保持这种彼此隔离的不连贯性,Spanner 会对什么时候退位做出限制。把 smax 定义为一个领导者可以使用的最大的时间戳。在退位之前,一个领导者必须等到 TT.after(smax)是真。
4.1.2 为读写事务分配时间戳事务读和写采用两段锁协议。当所有的锁都已经获得以后,在任何锁被释放之前,就可以给事务分配时间戳。对于一个给定的事务,Spanner 会为事务分配时间戳,这个时间戳是 Paxos 分配给 Paxos 写操作的,它代表了事务提交的时间。
Spanner 依赖下面这些单调性:在每个 Paxos 组内,Spanner 会以单调增加的顺序给每个 Paxos 写操作分配时间戳,即使在跨越多个领导者时也是如此。一个单个的领导者副本,可以很容易地以单调增加的方式分配时间戳。在多个领导者之间就会强制实现彼此隔离的不连 贯:一个领导者必须只能分配属于它自己租约时间区间内的时间戳。要注意到,一旦一个时间戳 s 被分配,smax 就会被增加到 s,从而保证彼此隔离性(不连贯性)。
Spanner 也会实现下面的外部一致性:如果一个事务 T2 在事务 T1 提交以后开始执行, 那么,事务 T2 的时间戳一定比事务 T1 的时间戳大。对于一个事务 Ti 而言,定义开始和提交事件eistart和eicommit,事务提交时间为si。对外部一致性的要求就变成了:
tabs(e1commit )<tabs(e2start ) s1<s2。执行事务的协议和分配时间戳的协议,遵守两条规则,二者一起保证外部一致性。对于一个写操作 Ti 而言,担任协调者的领导者发出的提交请求的事件为eiserver 。
Start. 为一个事务 Ti 担任协调者的领导者分配一个提交时间戳 si,不会小于 TT.now().latest 的值,TT.now().latest的值是在esierver事件之后计算得到的。要注意,担任参与者的领导者, 在这里不起作用。第 4.2.1 节描述了这些担任参与者的领导者是如何参与下一条规则的实现的。
Commit Wait. 担任协调者的领导者,必须确保客户端不能看到任何被 Ti 提交的数据,直到 TT.after(si)为真。提交等待,就是要确保 si 会比 Ti 的绝对提交时间小。提交等待的实现在 4.2.1 节中描述。证明如下:
4.1.3 在某个时间戳下的读操作第 4.1.2 节中描述的单调性,使得 Spanner 可以正确地确定一个副本是否足够新,从而能够满足一个读操作的要求。每个副本都会跟踪记录一个值,这个值被称为安全时间 tsafe,它是一个副本最近更新后的最大时间戳。如果一个读操作的时间戳是 t,当满足 t<=tsafe 时, 这个副本就可以被这个读操作读取。
。。。
4.1.4 为只读事务分配时间戳一个只读事务分成两个阶段执行:分配一个时间戳 sread[8],然后当成 sread 时刻的快照读来执行事务读操作。快照读可以在任何足够新的副本上面执行。
在一个事务开始后的任意时刻,可以简单地分配 sread=TT.now().latest,通过第 4.1.2 节中描述过的类似的方式来维护外部一致性。但是,对于时间戳 sread 而言,如果 tsafe 没有增加到足够大,可能需要对 sread 时刻的读操作进行阻塞。除此以外还要注意,选择一个 sread 的值可 能也会增加 smax 的值,从而保证不连贯性。为了减少阻塞的概率,Spanner 应该分配可以保持外部一致性的最老的时间戳。第 4.2.2 节描述了如何选择这种时间戳。
4.2 细节这部分内容介绍一些读写操作和只读操作的实践细节,以及用来实现原子模式变更的特定事务的实现方法。然后,描述一些基本模式的细化。
4.2.1 读写事务就像 Bigtable 一样,发生在一个事务中的写操作会在客户端进行缓存,直到提交。由此导致的结果是,在一个事务中的读操作,不会看到这个事务的写操作的结果。这种设计在 Spanner 中可以很好地工作,因为一个读操作可以返回任何数据读的时间戳,未提交的写操作还没有被分配时间戳。
在读写事务内部的读操作,使用伤停等待(wound-wait)[33]来避免死锁。客户端对位于合适组内的领导者副本发起读操作,需要首先获得读锁,然后读取最新的数据。当一个客户端事务保持活跃的时候,它会发送“保持活跃”信息,防止那些参与的领导者让该事务过时。当一个客户端已经完成了所有的读操作,并且缓冲了所有的写操作,它就开始两阶段提交。客户端选择一个协调者组,并且发送一个提交信息给每个参与的、具有协调者标识的领导者,并发送提交信息给任何缓冲的写操作。让客户端发起两阶段提交操作,可以避免在大范围连接内发送两次数据。