Spanner是谷歌公司研发的、可扩展的、多版本、全球分布式、同步复制数据库。它支持外部一致性(external consistency)的分布式事务,通过 Paxos 协议使得多个数据库副本达成共识。
Spanner 借助一个能保证时钟误差在一定范围(bounded time uncertainty)的 TrueTime API,实现了分布式事务的外部一致性。
TT.now()
返回的时间区间 [earliest, latest]
中包含了调用时的绝对时间。
TT.after(t)
和 TT.before(t)
则是对 TT.now()
的简单封装。因为调用时的绝对时间在 [earliest, latest]
区间内,TT.after(t)
只需返回 latest
就可以保证某一时间点 t
已经过去,TT.before(t)
只需返回 earliest
就可以保证某一时间点 t
还没到。
Spanner 中非 leader replica 也可以处理读请求。
只有一个 replica 足够新,才可以处理读请求。
各 replica 维护了一个它可以读取数据的时间戳上限:“安全时间”(safe time),不能从该 replica 读出高于此“安全时间”的数据。“安全时间”通过以下两者的最小值确定:
t_paxos
)对于 Paxos write 时间戳,取最近一次 applied 的 Paxos write 的时间戳即可;
对于 prepared 状态的事务,需要从它们中取出时间戳最小者。这是因为这些事务处于 prepared 状态,还不知道它们最终会不会提交,所以不能确定该时间戳下数据的最终结果。所以“安全时间”需要严格小于 prepared transactions 中的最小时间戳。
上述确定 safe time 的方法有以下缺点:
t_paxos
同样会阻碍 safe time 的前进。Spanner 则通过 leader-lease 区间的不相交性解决这个问题。每个 Paxos leader 维护一个时间戳门槛,表明可能会出现高于此门槛的写操作出现。它维护了一个从 Paxos 序列号 n到可能分配给 Paxos 序列号 n+1 的最小时间戳的映射 MinNextTS(n)
,当一个副本 apply 了序列号为 n 的 log 时,可以将 t_paxos
设为 MinNextTS(n) - 1
,leader 默认每8秒推进 MinNextTS()。读写事务都用 2PL,他们在获取锁后分配时间戳。对于一个事务,设代表事务提交的 Paxos write 的时间戳为 t_paxos
,Spanner 会将 t_paxos
分配给该事务。
Spanner 依赖以下单调性:在一个 Paxos group 中,Spanner 分配的时间戳单调递增。即使 leader 变更,这个单调性也成立,这是通过 leader-lease 区间的不相交性确保的。
Spanner 保证外部一致性:如果一个事务 T2 在 T1 提交后开始,则 T2 的时间戳一定大于 T1 的时间戳,这是通过 commit-wait 保证的。
对于事务 Ti,coordinator leader 等待 Ti 的提交时间戳 si 满足 TT.after(si)
后,才让 Ti 的修改对 client 可见。
在事务提交前,事务中发生的写是缓存在客户端中的。当一个事务的所有读、写操作都完成后(即 client 读出了所需的所有数据、缓存了所有的写操作)开始 2PC,client 先选择一个 coordinator group,给每个参与方的 leader 发送一个 commit message,并附带 coordinator 的身份、缓存的写操作。
一个非 coordinator 参与方 leader 收到来自 client 的 commit message 后,它选择一个 prepared timestamp 时间戳(这个时间戳必须大于它之前分配的所有时间戳),在 Paxos 中 log 一个 prepared record,然后告知 coordinator 其 prepared timestamp.
coordinator 听取所有 leader 的 prepared timestamp 后,commit timestamp 必须满足以下提交:
确定 commit timestamp 后,在 Paxos 中 log 一个 commit record.(或者因为没收到参与方的 prepared timestamp 而 timeout)
根据前述 commit-wait 规则,在 coordinator applies commit record 之前,coordinator 需要等待 TT.after(S) 为 true。在 commit-wait 后,coordinator 将 commit timestamp 发给 client 和其他参与方 leader. 各参与方 leader 将事务结果 log 到 Paxos 中,然后 apply commit record.
当 RO 事务所需的键都在同一个 Paxos group 时,client 将 RO 事务请求发送到该 Paxos group 的 leader。定义 LastTs() 为该 Paxos group 最近一个已提交的写操作的时间戳,对于只需一个 Paxos group 的 RO 事务,当没有 prepared transaction 时,给事务分配读时间戳 s_read = lastTS()
即可满足外部一致性(external consistency)。
如果 RO 事务需要多个 Paxos group 时有多种选择。
s_read = TT.now().latest
。LastTS()
的缺点:
当一个事务刚刚提交,一个与之不冲突的 RO 事务仍需将读时间戳确定为 LastTS()
. 可以增强 LastTS()
,添加一个 key ranges 到 commit timestamp 的映射,当一个 RO 事务到达时,只需根据 key 取出对应 key ranges 的 LastTS()
。
Spanner 中的 schema-change 作为一个事务执行。在其 prepare phase 中给该事务分配一个未来的时间戳 t。对于需要依赖 schema 的读写操作,如果它们的时间戳在 t 之前,则它们可以顺利执行;如果它们时间戳在 t 之后,则它们需要阻塞直至 schema-change 完成。
RW 事务中的读操作用 wound-wait 来避免死锁。
这里分别介绍一下 wait-die 和 wound-wait。
这是一种非抢占式死锁预防策略。当事务 Tn 请求一个当前被 Tk 占有的数据时,只有 Tn 的时间戳小于 Tk 的时间戳时,Tn 可以等待,否则 Tn 死亡(die)。
换言之,一个老事务请求一个被新事务占有的锁时,老事务将会等待;一个新事务请求一个老事务占有的锁时,新事务将会中止。
假设 T1, T2 两个事务满足 T(T1) < T(T2),wait-die 规则如下:
T1 | T2 |
---|---|
acquired the lock | |
try to acquire | |
get killed, restart later | |
release the lock |
T1 | T2 |
---|---|
acquired the lock | |
try to acquire | |
wait util acquired | |
release | |
acquired the lock |
这是一种抢占式死锁预防策略。它和 wait-die 相反,当 Tn 请求一个被 Tk 占有的数据时,只有 Tn 的时间戳大于 Tk 的时间戳时,Tn 可以等待,否则 Tk 中止(Tk is wounded by Tn)。
换言之,一个老事务请求一个被新事务占有的锁时,老事务会将新事务中止;一个新事务请求一个老事务占有的锁时,新事务将会等待。
假设 T1, T2 两个事务满足 T(T1) < T(T2),wound-wait 规则如下:
T1 | T2 |
---|---|
acquired the lock | |
try to acquire | |
kill T2 | |
get killed, restart later | |
acquired the lock |
T1 | T2 |
---|---|
acquired the lock | |
try to acquire | |
wait, util acquired | |
release | |
acquired the lock |
两种方法中,只有时间戳较大的事务会被杀死。
因为一个事务涉及的不同 key 可能属于不同的 key range,进而属于不同的 Paxos group。当多个事务执行时,可能一个新事务的修改被旧事务的修改覆盖了,进而导致外部不一致性,所以需要提供额外的手段对事务进行排序。
Spanner 借助其 TrueTime API,通过对事务的时间戳大小进行排序,实现了外部一致性。
deadlock in DBMS