Spanner论文分析——Lec13

Spanner是Google的分布式数据库系统,提供全球数据分布、扩展性和高可用性。它通过TrueTimeAPI实现时间同步,支持外部一致性的分布式事务。Spanner使用Paxos状态机进行数据复制和分区,并采用两阶段提交处理读写事务。其数据模型是半关系型的,支持查询语言和事务。通过TrueTime和Paxos,Spanner解决了分布式环境下的时钟同步和事务一致性问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、Spanner简介

1.Spanner 是Google的的分布式数据库 (Globally-Distributed Database),可扩展的,高可用的, 可全球部署的分布式数据库系统(谷歌号称它是世界上第一个可以在全球范围内进行数据分布式管理并且支持外部一致性分布式事务的数据库产品),Google称其为下一代Bigtable。

2.除了扩展性,同时通过同步复制多版本来满足外部一致性,可用性也很好。在CAP三者之间完美平衡。

3.Spanner能做到这些,离不开一个用GPS和原子钟实现的时间API。这个API能将数据中心之间的时间同步到10ms以内(暴露时间的不确定性)。因此有几个重要的功能:无锁读事务, 原子schema修改,读历史数据无block

4.对于读写事务,它使用基于 Paxos 复制容错的 2PC ;对于只读事务,它不使用锁机制,且允许从本地副本执行读操作,从而提高了只读事务处理效率。

二、Spanner背景

在这里插入图片描述
1.从上图可以看到。Spanner位于F1和GFS之间,承上启下。所以先提一提F1和GFS。

2.早期Google大量使用Mysql,但Mysql是单机,Google开发了一个可容错可扩展的RDBMS——F1。F1对应RDMS应有的功能,起初F1是基于Mysql的,不过会逐渐迁移到Spanner。

3.Google BigTable是NoSql产品,提供很好的扩展性,开源有HBase,为什么Google还需要F1,而不是都使用BigTable呢?
因为(1)BigTable提供的最终一致性,一些需要事务级别的应用无法使用。(2)BigTable还是NoSql,而大量的应用场景需要有关系模型
因此Google才有这个可扩展数据库的F1。而Spanner就是F1的至关重要的底层存储技术。

4.Colossus 第二代GFS,开源-新HDFS,初代GFS是为批处理设计的。对于大文件很友好,吞吐量很大,但是延迟较高。
Colossus的重要改进有:
· 优雅Master容错处理 (不再有2s的停止服务时间)
· Chunk大小只有1MB (对小文件很友好)
· Master可以存储更多的Metadata(当Chunk从64MB变为1MB后,Metadata会扩大64倍,但是Google也解决了)

5.Spanner主要致力于跨数据中心的数据复制上,同时也能提供数据库功能。与Google类似的BigTable, Megastore对比,区别:
(1)BigTable在Google得到了广泛的使用,但是他不能提供较为复杂的Schema,还有在跨数据中心环境下的强一致性。
(2)Megastore有类RDBMS的数据模型,同时也支持同步复制,但是他的吞吐量太差,不能适应应用要求。
(3)Spanner不再是类似BigTable的版本化 key-value存储,而是一个“临时多版本”的数据库。

何为“临时多版本”,数据是存储在一个版本化的关系表里面,存储的时间数据会根据其提交的时间打上时间戳,应用可以访问到较老的版本,另外老的版本也会被垃圾回收掉。

6.paper中提到他们一开始设计Spanner的原因
谷歌广告系统中的数据过去是存放在很多不同的MySQL和BigTable数据库中的,维护这些分片是一个非常费时费力的过程。而且这些数据库不支持跨多台服务器上使用事务。他们想要能够将数据分散到不同的服务器上以获得更好的性能,并且想具备在多个数据分片上使用事务的能力。
他们想要顺序地执行事务,他们也想要获得外部一致性(external consistency),这意味着,当它结束提交时另一个事务就会开始执行,这第二个事务需要去看到由第一个事务所做的任何修改。
如果我们想去获得外部一致性的能力,那么就要考虑分布式下数据复制相关的问题。

三、Spanner特性

1.从高层看Spanner是通过Paxos状态机将分区好的数据分布在全球的。数据复制全球化的,用户可以指定数据复制的份数和存储的地点。

2.应用可以细粒度的指定数据分布的位置。

3.Spanner还有两个一般分布式数据库不具备的特性:读写的外部一致性,基于时间戳的全局的读一致
这两个特性可以让Spanner支持一致的备份,一致的MapReduce,还有原子的Schema修改。

4.这些特性都得益有Spanner有一个全球时间同步机制,可以在数据提交的时候给出一个时间戳。
这个全球时间同步机制是用一个具有GPS和原子钟的TrueTime API提供了。

5.使用事务、数据分散在网络上获得容错能力,以此确保在每个想要使用该数据的人的附近都有该数据的一份副本,为了做到这点,spanner使用了至少两个很巧妙的思想:
其中一个是它们用了两阶段提交(2PC),但为了避免二阶段提交中因为事务协调器崩溃而导致所有人都被阻塞这一情况,他们在Spanner中使用了Paxos
另一个令我们感兴趣的想法就是,他们通过同步时间来做到非常高效的只读事务
这使得大范围的事务成为可能,其中一个叫做CockroachDB的开源系统就很多地方都使用了这些设计。

6.Spanner让只读事务快——无锁和二阶段提交,本地replica读数据——性能改善了10倍左右。
读写事务使用的依然是两阶段提交和锁,因为安全等待时间和commit wait的原因,在很多时候Spanner还是会遇上阻塞的情况,但只要它们的时间足够准确,那么这些commit wait的时间就会变得相当小。
snapshot isolation以及时间戳可能是这篇paper中最令我们感兴趣的两个方面。

四、Spanner架构

1.整体架构

在这里插入图片描述
一个Spanner的整体架构如上图所示。一个Spanner整体系统/部署实例叫做一个universe。它主要有以下几部分组成:
(1)universemaster: 是一个控制台, 监控universe里所有zone状态信息, 用于debug。
(2)placement driver: 帮助维持特定副本数量,自动搬迁数据, 实现负载均衡。
(3)Zone是系统的主体,具体的数据访问和控制都在一个个zone里。一个zone内部物理上必须在一起,一个数据中心可能含有一个或多个zone。一个Zone可以理解为一个BigTable部署实例。一个zone 包含一个 zonemaster 和多个 spanserver,它又细分为以下几个部分:
①zonemaster:zone上的管理者,相当于BigTable的Master,用于管理spanserver上的数据
②a set of spanservers:相当于BigTable的ChunkServer,提供具体的数据存储和访问。
③location proxy:位置代理,用于给client提供数据的位置信息,client要先访问它来定位需要访问的spanserver。

2.spanserver软件架构
上面介绍的是整体的Spanner框架,下面介绍对于spanserver的软件架构。
在这里插入图片描述
(1)每一个replica group 里的副本为上图中的一列。如上图中红色标注的部分。 其逻辑数据主要是存储在一个叫做tablet的数据结构体中。可以简单的把看成一个map集合:
在这里插入图片描述
在底部,每个 spanserver 负载管理 100-1000 个称为 tablet 的数据结构的实例。一个 tablet 就类似于 BigTable 中的 tablet。
与 BigTable 不同的是,Spanner 会把时间戳分配给数据,这种非常重要的方式,使得Spanner 更像一个多版本数据库,而不是一个键值存储。

(2)一个 tablet 的状态是存储在类似于 B-树的文件集合写前(write-ahead)的日志中,所有这些都会被保存到一个分布式的文件系 统中,这个分布式文件系统被称为 Colossus,它继承自Google File System。

(3)每个replica都运行着paxos共识协议,用来在副本组之间保持数据的一致性。事实上,这里它所使用的是Paxos的一种变体,它里面有leader,这和我们所熟悉的raft非常相似。对于一个给定的数据分片,它都会由一个Paxos实例来管理该数据分片对应的所有replica,包含该数据分片的所有replica就组成了一个Paxos组。
这些Paxos实例都是彼此独立的,每个Paxos组都有属于自己的leader,各自维护着独立的数据版本协议。这样做可以让我们对这些数据并行加速处理。为了应对client端的海量请求,将这些请求分散到数据分片所在多个Paxos组中,使用分片是为了提高并行吞吐量
因为Paxos只需要将每个日志条目复制到大多数follower中即可,这意味着有少数replica中的数据可能会落后,如果我们允许client去读取当地replicas中的数据副本来提高速度的话那么它们所读取到的可能是过时的数据
它们需要这种外部一致性的思想,即让每次读操作看到的都是最新的数据,他们得通过某种方式来处理本地replica上数据的版本有点落后的情况。

(4)对于副本组的leader来说,其还需要维护一个lock table ,也就是一系列的KV: key—>lock-states 。 用来事务处理中的2PL。 同时,leader replica 上还实现了transaction manager, 这个模块是用来支持分布式事务的。即事务如果涉及多个replica groups ,replica groups 之间的leader 会相互之间进行协调。也就是上图中蓝色所圈出来的部分。
参考

(5)paper中还介绍了directory的概念。它只是抽象出来的数据的一种组织方式,directory可以理解为partition的一种逻辑上的表达。一个tablet分为多个partition/directory,一个directory分为多个fragment,每个fragment对应一个paxos组。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
(6)将数据的多个副本放在不同的数据中心,这样做是有两个原因:①故障容灾 ②可以让这些靠近该数据副本的所有不同client去使用该数据。

五、数据模型

1.spanner中数据模型有以下特点

(1)结构化的半关系型表
(2)支持查询语言
(3)支持通用事务

2.Spanner模型设计

传统的 RDBMS例如MySQL采用关系模型,有丰富的功能,支持SQL查询语句。
而NoSQL数据库多是在key-value存储之上增加有限的功能,如列索引、范围查询等,但具有良好的可扩展性。
Spanner继承了Megastore的设计,数据模型介于RDBMS和NoSQL之间,提供树形、层次化的数据库schema,一方面支持类SQL的查询语言,提供表连接等关系数据库的特性,功能上类似于RDBMS;另一方面整个数据库中的所有记录都存储在 同一个key-value大表中,实现上类似于BigTable,具有NoSQL系统的可扩展性。
Megastore的参考
参考

3.存储数据的格式

Spanner给存储在其中的记录带上了时间戳作为版本号
(key:string, timestamp:int64) → string

bigtable的行模型是 (row:string, column:string, time:int64) → string,
可以看到spanner跟big table的模型最大的不同是spanner强化了row的概念,不再突出column;这样spanner的timestamp是赋给整行数据的,是有物理意义的,这使得spanner更像一个实现多版本并发的数据库,而在big table中,timestamp仅仅用于保存多个版本的key-value,跟并发完全无关;这也是为什么spanner称自己为semi-relational 数据库,而big table只称自己是semi-structure 数据库的原因。
注:spanner是把时间戳打在行上,相当于保存了不同时刻的状态而不是单个属性的数据

4.Spanner中的表

在Spanner中,应用可以在一个数据库里创建多个表,同时需要指定这些表之间的层次关系。例如,下图中创建的两个表——用户表Users和相册表Albums,并且指定用户表是相册表的父节点。父节点和子节点间存在着一对多的关系,用户表中的一条记录一个用户对应着相册表中的多条记录多个相册。此外,要求子节点的主键必须以 父节点的主键作为前缀。例如,用户表的主键用户ID就是相册表主键用户ID+相册ID的前缀。
在这里插入图片描述
上面的两个SQL语句看似创建了两张表:Users和Albums,但实际上Spanner将它们合并存储。注意第二张表的建表语句使用了多主键:uid和aid,并且通过“INTERLEAVE IN PARENT …”语句表示uid与Users中的主键uid一致。
这意味着,Album中的多条uid相同的记录,是嵌套在Users表的一条uid记录之下的。同一个user之下的所有子数据,就构成了一个directory。

注:对于这种层次化、类似于树形结构的数据模型来说,关系相近的数据可以存储在一起,这样存取就会利用一些locality的优势,相应的提高性能。

六、Spanner的True Time与分布式事务

1.True Time API

Spanner创造性的采用了GPS + 原子钟的方式提供TrueTime API来解决时钟问题,去掉了中心化的授时中心。接下来解释Spanner如何利用TrueTime API来解决分布式事务,并提供外部一致性。
当应用程序通过这个库来查询时间时,它所拿到的是TT区间(TT interval),这个区间是由最earliest time和latest time所组成。我们要保证正确的时间是不早于earliest time,不晚于latest time。

MethodReturn
TT.now()返口一个范围区间[earliest,latest] ,当前的真实时间位于这个区间中
TT.after(t)如果确保当前真实时间已经超过t,则返回true
TT.before(t)如果确保当前真实时间还未到t,则返回true

注: Spanner系统中的TrueTime 是综合利用了硬件(主要是原子钟和GPS)和软件(API),来实现的一种系统时间机制,这种时间机制保证了机器间的时间差不超过ε,从某种程度上把各个节点的时钟拉到了同一个“时钟参考系”(其实确切的说应该是利用延时,来达到事务之间不重叠的目的),让不同的结点发生的事件可以进行因果比较(也就是谁发生在前、谁发生在后)。
参考

2.paxos group

Spanner将数据分片,每一片数据有多个副本(paxos达成一致),这些副本组成一个paxos group

3.时间戳

spanner的paxos算法并未公开,从论文中可以看出,它的paxos具有leader,并且leader带有租约,需要定期向各个follower进行续租(看起来和raft有一定相似)。leader在每一个租约有效期内有一个大时间戳Smax,leader可以进行时间戳分配,spanner保证leader在当前租约内分配的任意时间戳不会超过Smax并且是单调递增的,同时两个连续的租约之间的时间戳不会有重合
(注:Smax相当于paxos的leader有效期的截止日期)

4.选择时间戳

Spanner的读写事务会将时间戳作为版本号按照前面所说的格式写到记录中,那么现在问题在于:拥有TrueTime的情况下如何为事务Ti选择时间戳Si使得事务之间能满足外部一致?下面是Spanner做法:

  • 在事务Ti决定提交的时候,选择TT.now().latest作为Si。
  • 提交时进行等待,直到TT.after(Si)为true,这被称为cond wait。
    (注:事务决定提交的最晚时刻为事务的时间,提交后等待到时间Si之后)

5.Spanner外部一致性的两条规则

(1)其中一条规则是start rule,另一条规则是commit wait rule
start rule告诉我们事务所选择的时间戳是什么,一个事务所选择的时间戳(TS)等于TT.now().latest,对于一个只读事务来说,它的时间戳就是它开始执行时的时间戳,对于一个读写型事务来说,我们会将事务提交时的latest time作为它的时间戳分配给它。
commit wait rule只适用于读写型事务,事务协调器会去收集投票信息,检查是否能够提交该事务,并为该事务选择一个提交时间戳,当它选完时间戳后,在实际提交该事务(执行完写操作并释放锁)之前,它需要延迟等待一段时间进行事务提交,这个提交时间戳得小于下一个读事务当前时间的最早开始时间。(下文六-7的例子是两个读写事务的一致性)

6.系统是如何通过这两条规则对只读事务做到强制外部一致性的?

例子:假设这里有两个读写型事务,即T0和T1,它们都对x进行了写入操作。还有一个只读型事务T2,它要去读取x,因为这里在时间戳上使用了快照隔离,所以我们想确保T2看到最新写入的值。
假设T0将x的值设定为1,提交事务。然后T1将x设置为2,然后提交事务。假设T2是在T1完成后才开始执行,我们想确保它看到的x值为2。
假设T0所选的时间戳是1,它在这个时间点将写操作落地到数据库。假设T1开始执行,此时它选择了一个时间戳,它从True time系统那里拿到了一个数字范围(时间范围)而不是一个数字,它所拿到的earliest time值为1,latest time的值为10,它必须选择latest time 10作为它的时间戳,所以T1的提交时间就是10。现在我们还不能提交这个事务,因为commit wait规则表示,我们得等到这个时间点才能进行提交。T1会一直去询问时间,直到它所拿到的时间范围里面不包括10为止。在某一时间点,它拿到的earliest time为11,latest time为20。现在就可以说,我知道我的时间戳(10)肯定是过时的了,此处就是T1的commit wait范围。
假设T2是在T1结束后开始执行,假设T2是在11点后去询问时间的,它会拿到一个包含时间11的时间范围,假设所拿到的earliest time是10,latest time是12,我们知道这个latest time至少也得是11,因为T2是在T1结束后开始执行的,11必然小于latest time,T2会选择12作为它的时间戳。
只要T2是在T1提交后开始执行,那么就能保证这是奏效的。这里的理由是,commit wait会导致只有到了提交时间后,T1才会去提交事务。T1选择了一个时间戳,这就保证了当T1在这个时间点提交后,T2就会开始执行,我们对earliest time的值是什么并不清楚,但我们知道当前时间是在T1的提交时间之后,这里保证了T2的latest time(即它所选择的时间戳)是在T1的提交时间之后,因为如果T2在T1结束后开始执行,那么这就保证了T2会获得一个更高的时间戳。
快照隔离机制(数据对象会有多个版本)会导致T2能够看到所有比它时间戳来得低的事务中的所有写操作所做的修改,这意味着T2会看到T1所做的修改,简单来讲,这意味着,这就是Spanner如何强制让它的事务保持外部一致性的方式了。
总结:两条规则保证读事务能够看到它之前提交事务修改的值。读写事务完成写操作后确定时间戳,要等到选择的时间戳不在当前获取的时间范围时,才能提交事务。之后的只读事务选择的时间戳一定大于之前的读写操作的提交时间戳,所以修改的数据一定可见。
在这里插入图片描述

7.举例分析TrueTime和分布式事务的关系

(1)本质:阻塞直到自己的置信区间不会跟别人重叠,才能继续接受新事务。
(2)分布式环境下无法保证不同机器时钟总是有相同值。当使用时间戳来记录事务的提交时间,需要保证同一个partition的先后两个事务T1和T2, T2的提交时间戳比T1大,这样在之后的操作中才能正确取到对应版本的值。
(3)例子:有两个事务T1和T2,3个partition A B C 其上各有一个值X Y Z可以更改。(每个partiton是一个paxos集群,为简化问题假设通信瞬间完成)。分布式事务会选择不同分片最大的为commit时间戳,如下图示例分布式事务的时钟偏移会造成问题:
在这里插入图片描述
(4)Spanner的TrueTime利用原子钟保证了机群里的任意机器的时间戳差距不会超过一个上限值:ε,且当coordinator选择好时间戳之后,会等待一个ε的时间才告诉参与transaction的partition这个时间戳来commit。
(5)如上面的例子,如果选择TrueTime,假设ε=60个单位时间,则当T1的Coordinator选中104作为时间戳后,等待60个单位提交。此时B中实际时间戳已经到达120,在此刻记录Y = 1的commit时间戳为104。由于T1在B持有写锁,所以T2最快在B也只能在120之后提交,这样Y = 2的时间戳必定大于Y =1,保证了正确的顺序。
(6)总结:原子钟保证机器间的时间差不超过ε,不同事务的提交时间强制等待到ε过期,来保证分布式事务的一致性。
参考

8. 快照隔离

快照隔离意味着,我们通过使用时间戳(等同于执行顺序)和安全等待时间来确保只读事务能够看到它们开始执行前的所有读写型事务所做的修改,并且它无法看到它(只读事务)开始时间之后的读写型事务所做的修改。
快照隔离一般无法保证外部一致性。为了使用快照隔离,Spanner也得对时间戳进行同步,同步时间戳加上commit wait规则这才允许Spanner去保证外部一致性以及线性一致性。

七、Spanner事务的实现

1.读写事务

spanner的读写事务采用了2PC+2PL的方式实现。首先,client的任何读写操作都必须访问一个paxos group的leader并且在leader的lock table中获取锁,对于读而言应当读取新的数据,而写入的数据则先全部缓存在客户端
(1)首先,spanner会选出一个paxos group作为coordinator,然后客户端向参与分布式事务各个paxos group的leader发送commit指令和数据,对于participant会带上coordinator的信息。
(2)participant的leader获取写锁,获取一个prepare的时间戳Sprepare,通过paxos协议在自己的group写入一条prepare的记录,写入完成后通知coordinator。coordinator同样需要获取写锁,但是不进行prepare。
(3)coordinator在收到所有participant的回应后按照前面所说的方式选择一个时间戳Si作为整个事务的提交时间戳,并通过paxos协议写入整个事务的提交message,coordinator的各个副本只有在确保cond wait完成后才能apply应用日志。
(4)通知各个participant提交,并且在并回复客户端(原文中没有提,但按理说这两个步骤是可以并行的)
在这里插入图片描述

2.只读事务

之前已知我们在进行只读事务的时候会获取一个读取时间戳Sread,那么自然的想法就是直接通过这个Sread来进行读取,读取时间戳小于Sread的版本的数据,而spanner是支持在任意副本上进行读取的,我们能直接在任意一个副本上直接用Sread进行读取吗?考虑遇到的下面两个问题:
①在某个节点上有处于prepare状态的事务,这时候如果需要读取被这个事务修改的数据,那么这些数据是否可见?对于一个participant,处于prepare状态的事务可能完成了上面2PC的步骤2,也有可能coordinator已经决定commit完成了步骤4,但是由于延迟这个participant还没有在本地提交。这个时候如果直接读取已经提交的数据,可能Sread大于已提交事务的提交时间戳Si,但却没能读取到这个事务写入的数据。
(注:因为分布式事务participant节点应用日志的延迟性,读不到小于Sread的时间戳的事务修改数据)
②一个paxos group有多个副本,从follower上进去读取的时候是否能保证读到新的数据?显然从leader到follower的复制有延迟,直接从任意一个follower读取的话并不能保证读取到新的数据。
(注:因为多副本复制延迟)

3.安全时间

paper中表明,leader会严格按照时间戳增加的顺序来发送日志记录。每个replica会去记录它从它的Paxos leader处所收到的日志记录,并使用它从leader处所拿到的最后一个日志记录来作为安全时间
如果一个事务要去读取时间戳15所对应的值,但replica只从Paxos leader处拿到的最后一个日志记录时间戳是13,那么replica就会推退给我们返回数据(直到它从leader处拿到了时间戳大于等于15所对应的日志条目时,它才会对我们进行响应),这样做就确保了,对于一个给定时间点的请求来说,直到replica从leader那里知道了该时间点前所发生的一切事情,它才会对该请求进行响应,对于读请求来说,这可能会造成延时。
总之,为了解决上面的问题spanner的每一个副本都维护了一个tsafe,这个值代表这个副本足够新,使得读取能满足线性一致,只读事务的Sread只要小于等于tsafe,就可以进行读取。tsafe的定义为
在这里插入图片描述
上标paxos的tsafe, 即上面说的应用的最后一条日志的时间戳,解决七-2-②多副本延迟的问题。上标TM的tsafe即paxos组中所有处于prepare状态的事务prepare的时间戳
Sprepare小的那个值-1,解决七-2-①分布式事务participant节点应用日志的延迟问题。
参考

4.Spanner实现读写型事务的示例

(1)银行转账的例子

在这里插入图片描述
假设XY保存在不同的分片,这两个数据分片会被复制到3个不同的数据中心中。
Spanner想要对两阶段提交和两阶段锁完全支持,这和
另外一个博客总结的几乎完全一样。其中一个很大的不同在于,这里我们不会使用单独一个服务器来作为参与者和事务协调器,我们是使用由多台服务器所组成的Paxos组来充当参与者和事务协调器的角色,以此提高容错能力。
每三台服务器中,有一台会被作为leader使用。这里假设数据中心2的服务器是数据分片X这个Paxos组中的leader,数据中心1的服务器是数据分片Y这个Paxos组中的leader。

(2)事务执行顺序

a.client会选择一个唯一的事务id来给它所要发送的所有消息打上标记
b.根据事务的代码,client首先得去执行所有的读操作,然后在最后同时去执行所有的写操作
正如上一篇博客中说的那样,当你每次要对一个数据项data item进行读取或写入时,负责处理该item的服务器得将一把和这个data item关联起来。Spanner会在Paxos组的leader处对这些锁进行维护。
当client想执行事务时,该数据分片所在的Paxos组中的leader会返回X的当前值,并对X加锁(锁被占用时等待锁释放)。同样的操作读取Y并加锁。
在这里插入图片描述
c. client计算出要对X和Y写入的值,发送给leader。client会在事务的最后将所有这些写操作一次性提交给Paxos组,会选择其中一个Paxos组来作为事务协调器使用,它会将作为事务协调器来使用的那个Paxos组的id发送出去。下图用了两个方框标记的这个Paxos组中的这个服务器,它不仅是该Paxos组的leader,它同时也扮演了该事务的事务协调器的角色。
client将它想写入的更新值以及该事务协调器的id发送出去,即client会发送一个关于x的写请求给x的leader,当每个Paxos组中的leader收到携带写入值的这个写请求时,它会发送一条prepare消息给它的follower,并将它写入到Paxos的日志中(下图用P表示prepare消息)。如果在它没有发生崩溃和丢失锁的情况下就能够去执行这个事务。当这个Paxos组中的leader收到了大多数follower的响应后,r就会发送一个Yes给事务协调器,表示我能够去执行事务中我负责的这部分任务。
同样,client也会发送一个值给Y所属的Paxos组中的leader,这个服务器扮演着Paxos leader的角色,它会将prepare消息发送给该Paxos组中它的follower,并等待来自大多数follower的确认信息。数据分片Y所对应的这个Paxos leader向与X所使用的同一个事务协调器组(当下指的就是自己这台服务器)发送了一个Yes,表示它可以去提交事务,当事务协调器收到了来自所有涉及该事务的数据分片所属的Paxos组中leader的响应后。如果它们回复的都是Yes,那么事务协调器就会去提交这个事务。否则,它就不会去提交这个事务。
在这里插入图片描述
d.假设事务协调器会去提交该事务。事务协调器发给数据分片Y所属的Paxos组中的follower一条commit消息,请将这个事务落地到日志。然后,它也会告诉该事务中所涉及的其他Paxos组的leader,你们可以提交了。
只有当commit安全落地到日志后,事务协调器才会将commit消息发送给其他数据分片的Paxos组。我们会保证事务协调器不会忘记它所做的决定,一旦这些commit消息都被提交到了不同分片中的Paxos log日志中,每个分片就可以去执行这些写操作,并释放数据项的锁。
在这里插入图片描述
(3)在该设计中,锁是用来确保事务执行的有序性。如果有两个事务冲突了,因为它们在操作同一个数据对象,它只能等另一个事务将锁释放。Spanner使用了完全标准的两阶段锁来获取有序性,并且它也使用了完全标准的两阶段提交来获得使用分布式事务的能力。

(4)两阶段提交所带来的问题:即如果事务协调器在持有锁的情况下发生故障导致阻塞的问题,因为这个问题基本上使两阶段提交对于其他任何可能遇上很多部分故障的大型系统来说都是完全不可接受的。
Spanner通过对事务协调器进行复制解决了这个问题,事务协调器自身就是一个基于Paxos的replicated state machine。不管这个事务有没有被提交,它都会被复制到Paxos log日志中,如果这里的leader崩溃了(指用双方框圈起来的Y),虽然它之前管理着事务,因为这里是使用的是和raft一样的复制行为,那么这两个replica中的任意一个都能去接手leader的工作,同时也能去接手事务协调器的工作。
如果事务协调器决定去提交事务,那么它们在它们自己的log日志中就能看到这条commit消息,不管该Paxos组中剩下的哪个服务器接手了leader的工作,它就会在它的日志中看到这条提交信息,并能够告诉其他参与者该事务已经被提交。这有效地消除了上面两阶段提交所带来的问题。

5.Spanner实现只读型事务的示例

(1)上图存在大量跨数据中心的消息往来,花费很多时间,这些读写型事务的执行速度很慢。对于执行只读事务Spanner可以用更加精简的方案。
Spanner中只读事务的设计消除了读写型事务中存在的两种成本消耗问题,首先,它是从本地replica中读取数据。只读事务的设计所解决的另一个问题是:它井没有使用锁,也没有使用两阶段提交。它不需要使用事务管理器进行管理。
避免将读请求发送给跨数据中心的Paxos组leader来处理这个事务,这就可以避免跨数据中心读取数据,也可以避免降低读写型事务的执行速度(因为没有用锁,不需要去等待由只读事务所持有的锁)。

(2)在只读事务中主要引入两个正确性约束,他们想让所有事务的执行依然是有序的(因为我们要考虑前面是否有读写型事务,其实这和指令重排序一个道理)

对于只读事务来说,这意味着,一个只读事务的所有读操作可以看到在它执行之前的那个事务中的所有写操作所执行的结果,它必然无法看到任何在它之后所执行事务中的任何写操作所执行的结果。我们需要通过一种方法来将一个夹在两个读写事务之间的只读事务的所有读操作都放在这两个读写事务中间。

paper中所讨论的另一个主要约束就是他们想要获取外部一致性的能力,这实际上等同于我们以前看过的线性一致性。

(3)这里有一个例子,只读事务读取到的最新值并不是正确的结果,它和顺序执行所读取到的值并不相同。
正在执行三个事务,T1,T2,T3,T3是一个只读事务,T1和T2是读写型事务。
T1中,我们对x和y进行写入操作,然后提交。这里将钱从x的账户转移到y的账户
T2也执行了另一次x和y间的转账操作,然后提交事务
这里我们要打印出x和y的值,因为我们得对账户进行审计以确保银行没有丢钱
事务T3需要去读取x和y的值,T3中对x的读取操作是发生在T1结束后T2开始前这段时间内,T3是在很后面的时候才对y进行读取。T3会看到由T1所写入的x值,但它所读取到的y值则是T2写入的。假设它通过这个很简单的过程来读取数据库中最新的值,这些执行是无序的。
在这里插入图片描述
(4)Spanner解决这个问题所采用的方式有点复杂,首先,第一个重要的思路就是快照隔离(snapshot isolation),假设所有参与的机器上都有一个同步时钟,给每个事务都分配了一个特定的时间戳(这个时间戳是由它们上面的同步时钟那里拿到的)。
对于只读事务来说,它的时间戳就是事务开始的时间。
每个事务中所涉及的数据都会有它自己的时间戳。当每个replica保存数据时,实际上它保存了该数据的多个版本每个副本都是和写入这些副本的事务的时间戳相关联的。
这里的基本策略是,当只读事务开始执行读操作时,它就已经给它自己分配了一个时间戳。当它发送读请求时,它会让读请求携带一个时间戳,不管是哪台服务器保存了该事务所涉及数据的副本,它都会去这个多版本数据库中查看并找到它所要的那条记录,这条记录要是比该只读事务所指定的时间戳小最新时间戳。

(5)Spanner使用这种snapshot isolation的思路来解决只读事务所存在的问题,在读写型事务方面,Spanner依然用的是两阶段锁和两阶段提交来解决问题,读写型事务会给它们自己分配一个时间戳,这个时间戳就是该事务的提交时间
在这里插入图片描述
(6)snapshot isolation该如何解决我们之前的问题呢?即内部操作在要符合事务间执行顺序的问题。
这个例子来展示快照隔离技术是如何解决只读事务的顺序执行问题
两个读写型事务T1和T2,另一个只读事务T3。
当T1在10点执行完它的写操作时,Spanner存储系统并不会将旧值用新值覆盖,而是去添加该记录的一个新副本,上面还会携带一个时间戳( x@10=9 y@10=11)。
当T2在20点执行完写操作,添加一个带时间戳的记录(x@20=8,y@20=12)。
当T3开始执行的时候,它会去根据当前时间来选择一个时间戳,假设为15。所以T3的整个读取流程不会读到T2写入的数据(因为T2时间戳为20在15之后),只会读到T1写入的数据。保证了只读事务的顺序性。
这个例子模拟了以时间戳顺序来执行事务,执行顺序是先执行T1,接着执行T3,最后再执行T2,这个执行顺序所产生的结果和我按照10 15 20这些时间戳顺序所产生的结果是相同的,这就是Spanner处理事务时的简化版本。
在这里插入图片描述
(7)这个例子中,为什么T3读取y的旧值是可以的?
T2和T3是并发执行的,时间上会有重叠它们执行的时候。对于线性一致性和外部一致性来说,它们的规则是,如果两个事务并发执行,那么数据库所允许使用的执行顺序可以是任意的。
外部一致性所强加的唯一一条规则是,如果一个事务已经完成了,那么在它之后开始执行的事务必须看到自己这个事务中所有写操作做的修改

(8)老版本记录的丢弃策略
paper中并没有说丢弃老的记录的策略是怎么样的,但基于记录多个数据副本的磁盘存储空间和内存空间的双重开销,Spanner肯定会将老的记录丢掉。
之所以对每条记录使用多版本,是为了实现这些事务的快照隔离,你不需要去记住太过久远的值,因为你只需要记住可以追到最早已经执行事务的时间点所对应的值。

(9)如果快照隔离的时间并不同步的话会产生什么影响?
如果时间不同步,对于Spanner中的读写型事务来说没有什么影响,因为读写型事务使用了锁和二阶段提交,它们实际上并没有使用快照隔离,基于两阶段锁机制,读写型事务依然是有序执行的。
对于只读事务,分为两种情况:
①一个事务选择的时间戳太大,影响不会很严重,需要等待访问的replica从Paxos leader拿到的日志条目所对应的时间戳追上了事务所携带的时间戳时,replica才能进行响应,速度很慢。
②只读事务的时间戳过小,这是一个正确性问题,这会违反外部一致性。replica会返回很长时间以前的一个版本的数据,这个版本的数据可能忽略了很多最近对它所做的修改,导致我们丢失最近已经提交落地的写操作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值