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)]表示请求结束.

场景一

假设现在有如下图所示的场景一:
在这里插入图片描述
该场景共有三个操作:

  1. A进程执行W(1).
  2. 操作1进行的过程中, B读取到0这个值, 用R(0)表示.
  3. 操作1和操作2都结束后, B进程再次读取, 得到了1这个值, 用R(1)表示.

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

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

start[W(1)]A
start[R(0)]B
end[W(1)]A
end[R(0)]B
start[R(1)]B
end[R(1)]B

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

start[R(0)]B
end[R(0)]B
start[W(1)]A
end[W(1)]A
start[R(1)]B
end[R(1)]B

而且这个sequential history相当于依次执行: 操作2, 操作1, 操作3.
这个sequential history仍然保持了操作1—>操作3, 操作2—>操作3的先后顺序. 也即是等价于场景一.
因此, 根据论文[1], 场景一可以认为是linearizable的.

场景二

再看如下的场景二, 该场景并不满足linearizable的限制 :
在这里插入图片描述
在这个场景中, 当B执行W(1)的同时, A也在读取, 而且已经读取到了1这个值, 也即是有以下关系成立:

W(1) B—> R(1)A

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

W(1) B—>R(1)A—>W(0)C

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

W(1) B—>R(1)A—>W(0)C—>R(1)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个操作

a. x = 1, 在中国发生
b. read x, 在美国发生
c. x = 2, 在日本发生
d. read x, 在美国发生

按照直觉来想, 操作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个操作:

a. x = 1, 分配的timestamp为s1
b. 指定timestamp s2执行read x
c. x = 2, 分配的timestamp为s3
d. 指定同一个timestamp s2执行read x

假设s2 > tabs(a), 意思是s2是在操作a之后一个timestamp, 按照直觉来想, 肯定满足s2>s1的关系式, 但是对于不支持Linearizability的系统来说, 可能存在s2 < s1, 导致操作b这个Snapshot read无法看到a操作的结果.

类似, 还假设s2 < tabs( c), 但是可能存在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

参考

[1] Linearizability: acorrectness condition for concurrent objects(ACM TOPLAS 1990)
[2] Spanner: Google’s Globally-Distributed Database (OSDI12)
[3] Spanner: Becoming a SQL System (SIGMOD17)
[4] Weak Isolation in Relational Databases, http://www.evanjones.ca/db-isolation-semantics.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值