首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

Linearizability和Serializability

引言

随着信息科技的在人类生活中的不断渗透, 我们数据规模和计算规模也是与日俱增, 某些问题已经无法在单机系统上进行处理, 只能寻求分布式系统的解决方案.

分布式系统用多个设备来处理特定问题, 但是, 如何保证这样的处理结果像单机系统那样正确?

这里所谓的正确的指的是: 以相同的顺序进行相同的一系列操作, 分布式系统能得到和单机系统相同的结果.

分布式系统中有个概念叫做Linearizability(线性), 可以用来衡量一个分布式系统的正确程度. 也就是本文主要介绍的内容.

本文主要介绍Linearizability概念, 实现Linearizability的意义, 实现Linearizability的难点. 以及简单说明它与Serializability的区别.

Linearizability的概念

如果一系列的操作并发交叉进行, 最终形成的history(可以理解为运行记录), 与顺序执行这些操作形成的sequential history相同, 而且这些操作的先后顺序仍然得到保留. 那么这个history我们就称之为是linearizable的[1].

这个说法是来自论文[1]的正式定义, 理解起来有点烧脑. 但是我有个直白的理解:

先是简单的情况, 假设操作都可以瞬间生效, 那write操作写入一个值之后, 后续的read操作都应该能读到这个值, 或者读到更加后面的write的值. 而且一个read操作读到某个值后, 它后面的read应该也读到这个值, 或者更加后面的write的值. 这样的一系列操作就是满足linearizable的.

复杂的情况来了(偏学术), 现实场景中, 一个操作从开始到结束普遍都是有时延的. 比如下文场景一中, 操作1和2的运行时间有一部分重叠, 我们无法区分两者的顺序, 但是可以明确的是: 3在1结束之后开始运行, 而且操作3也在2之后运行. 所以操作3必然能读到操作1的值, 或者读到操作2的值. 这样的话, 这一些列操作就是满足linearizable的.

现在我列举两个场景, 来形象说明这种偏学术的复杂情况.

我们假设现在有一个寄存器, 具有W和R两个操作, 比如W(0)A表示进程A往寄存器写入0这个值, R(0)B表示进程B从寄存器读到0这个值.

现实中, 操作从开始到结束是具有时延的, 我们用start[W(0)]表示发起操作, end[W(0)]表示请求结束.

场景一

假设现在有如下图所示的场景一:

h1

该场景共有三个操作:

A进程执行W(1).

操作1进行的过程中, B读取到0这个值, 用R(0)表示.

操作1和操作2都结束后, B进程再次读取, 得到了1这个值, 用R(1)表示.

注意这三个操作中隐含的先后顺序: 操作1先于操作3, 操作2先于操作3.

这个并发执行的场景对应的history如下:

这个history可以找到一个等价的sequential history,:

而且这个sequential history相当于依次执行: 操作2, 操作1, 操作3.

这个sequential history仍然保持了操作1--->操作3, 操作2--->操作3的先后顺序. 也即是等价于场景一.

因此, 根据论文[1], 场景一可以认为是linearizable的.

场景二

再看如下的场景二, 该场景并不满足linearizable的限制 :

h2

在这个场景中, 当B执行W(1)的同时, A也在读取, 而且已经读取到了1这个值, 也即是有以下关系成立:

之后C执行W(0), 也即是:

W(0)C结束之后, B读取这个值, 也就是有以下关系成立:

从中, 我们发现B竟然还读取到1这个值, 它其实是应该读到0这个值的.

所以这个场景是不满足linearizable.

分布式系统中的Linearizability

分布式系统中, 如果可以按照操作发生的先后顺序, 构造一个linearizable的运行记录, 那么这个特性就称之为的Linearizability(线性).

拿zookeeper来举例, 我们应该都知道zookeeper的follower read特性. 如果你从follower上读取数据, 你读到的数据可能是旧的(原因可能是新数据还没apply). 也就是t1时刻更新了zk节点, 在t1之后, 读到的数据可能还是t1之前的. 所以zookeeper是不满足线性的.

zookeeper不满足线性的原因是follower上的read操作和apply操作没有进行重排, 也就是后面的read操作先于t1那个事务的apply操作.

解决这个问题的办法, 可以参考tidb的做法, 将read操作也进行一次raft log, 相当于对read操作的顺序重排到了raftlog中, 排在apply之后.

不满足线性的另一种原因是, 无法感知/重排这些操作的先后顺序. 举例来说, 假设现在有一个全球部署的分布式存储系统,先后发生以下4个操作

按照直觉来想, 操作b读取到的x值肯定是1, 操作d读取到的x值肯定是2. 但这一切都是建立在系统能够感知这些操作的先后顺序之上, 比如在操作b中, 坐落于美国的server可以意识到操作a已经先发生.

但是在现实的分布式场景中, 我们的系统并不能像上帝一样, 可以清楚的知道世界各角落中, 每个操作的先后顺序, 除非我们为每一个操作都标记上统一的, 单调递增的id, 这个id的功能一般由timestamp来充当.

但是, 对于Spanner[2][3]这样全球部署, 或者跨地域部署的系统, 如何来为事务分配timestamp, 才能保证系统的响应时间在可接受的范围内? 如果整个系统采用一个中心节点来分配timestamp, 那么系统的响应时间就变得非常不可控, 对于离中心节点隔了半圈地球的用户来说, 响应时间估计会是100ms级别.

所以, 我有一篇文章着重介绍Spanner分配timestamp的原理, 也即是TrueTime API.

Linearizability与Serializability

需要注意的是Linearizability其实是无关于并发控制的, 它只是关于操作顺序的一种限制. 与Linearizability很容易混淆的一个术语是Serializability, 这个特性才是关于并发控制的一个限制. 如果一个系统, 可以将并发的事务按照某种调度, 达到的效果和某种串行执行的效果一样(也就是各事务的操作不会相互交错), 那么这个特性就叫Serializability.

Serializability着重于保证编程模型中的一些约束, 这些约束大部分是人为规定的. MySQL的默认RR隔离级别是没有保证Serializability, 因为RR并不能防止Lost Update和Write Skew的发生, 这两种情况都会破坏一些约束, 具体请参考Weak Isolation in Relational Databases[4].

对于单机数据库系统, 单机数据库系统维持一个统一的, 递增的事务id是轻而易举的事情, 所以可以认为它们天生就是具有Linearizability特性. 另外, 有些数据库系统, 比如MySQL, 它所支持的Serializable隔离级别便是本文所说的Serializability的特性.

所以, 我们可以看到, 所谓的Serializability便是ACID中的I, 至于Linearizability, 是针对于分布式系统而言的一个难点.

Linearizability的意义

现在的数据库系统, 都具有Snapshot read的概念, 所谓Snapshot read指的是对于系统的读操作, 只是过去某一时刻的一个快照, 也就是对应于该事务id所能看到的一份历史数据, 在Spanner的场景来说, 也就一个指定的timestamp所能看到的一份历史数据.

如果一个分布式系统能够对"历史"有所感知的话, 也就意味着能够感知操作的前后顺序, 也就是说这个系统支持Linearizability.

如果一个分布式系统无法支持Linearizability, 那么对于指定一个timestamp所能看到的历史数据, 每次可能是不一样的, 举例来说, 假设现在有一个全球部署的分布式存储系统,先后发生以下3个操作:

假设s2 > tabs(a), 意思是s2是在操作a之后一个timestamp, 按照直觉来想, 肯定满足s2>s1的关系式, 但是对于不支持Linearizability的系统来说, 可能存在s2

类似, 还假设s2 s3, 导致操作d这个Snapshot read看到了c操作的结果.

注意, 操作b和d的都指定同一个timestamp=s2进行Snapshot read, 但是b和d看到的历史数据却是不一样的, 也就是说, 对于同一个timestamp对应的快照竟然不一样.

到这里可以看出来, 不支持Linearizability的分布式系统, Snapshot read便无从谈起, Snapshot Isolation也就是不可能的事情.

有些系统实现了"部分顺序", 也就是在单个节点(或者说单个分区)内的Linearizability, 也就是能做到单机Snapshot, 但是没有全局的Snapshot.

实现Linearizability的难点

从上述描述中, 可以看到, 实现Linearizability首先需要对系统中每个事务分配一个统一的,单调递增的id, 一般来说是timestamp. 那么, 在分布式系统中, 如何协调这个timestamp呢?

对于这个问题, 我们可能会很迅速的想到一个方案: 引入一个专门生成timestamp的中心节点, 每次事务提交时, 访问该中心节点来获得timestamp. TiDB使用的便是这种方案, 它利用TimeStamp Oracle模块来提供授时服务. 但是这种方案引入了中心节点, 也意味着整套系统的部署不可能在地理位置上过于分散, 否则事务延时将会令人难以忍受.

需要指出的是, 逻辑时钟(比如Lamport时钟和Vector时钟)无法实现Linearizability, 因为它只能区分有"因果关系(也就是有通信)"的事件间的顺序. 所以, 类似于"事务T2在事务T1提交后才开始提交"这样的场景, 当节点间没有发生通信时, 逻辑时钟无法区分这两个事务的顺序.

那么, Spanner是如何在这种全球部署的分布式系统中保证Linearizability的. 在全球分布式系统中, 真实世界里两个事务, 先后在不同的地方commit, 系统怎样去如实反映事务的commit顺序? 具体详情可以参考这篇文章:关于Spanner中的TrueTime和Linearizability

参考

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180319G19G7600?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券