今天谈谈分布式事务的时序问题。在说这个问题以前首先说说这为何是个问题。html
对于数据库来讲,读到已经commit的数据是最基本的要求。通常来讲,为了性能,读写不互相阻塞,如今的数据库系统(Oracle,MySQL,OceanBase,Spanner,CockRoachDB,HBase)几乎无一例外的使用MVCC技术来达到这个目的。说白了,就是数据有多个版本,每次写产生新的更大的版本。读事务能够指定某个版本读,即快照读,数据库返回比指定的版本小的最大的版本的数据。固然也能够不指定,即读最新的已经commit的版本的数据。从时序上来看,越后写的数据,版本号越大,很显然,这个版本号能够经过实现一个单机内单调递增的counter来解决,counter从0开始以1递增。可是这样作,快照读搞不定:查找2015年3月29日1点的最新数据。这是由于这个counter和时间没有任何关系。那么显然,时间戳做为版本号再适合不过了。在单机上,即便出现clock skew(即单机上前后两次调gettimeofday取到的wall time,后面一次取到的wall time反而更小),维护一个单机内单调递增的时间戳很容易办到。能够看出,在单机状况下,知足了Linearizability: T2在T1 commit成功后start,T2的commit timestamp必定大于T1的commit timestamp。下面看看多机的状况。git
在多机状况下,如何知足Linearizability。github
仍是以写事务T1(修改x),T2(修改y)为例,时序上T2在T1 commit以后start,因为不一样的服务器的时钟不同,有些快有些慢,致使T2可能拿到比T1更小的时间戳。算法
举个例子:数据库
假设机器M1的时钟比M2的时钟快30,T1事务在M1上提交,得到commit timestamp 200,随后T2事务在M2上开始并提交,因为M2时钟更慢30,T2的commit timestamp多是180。随后来了一个读事务T3,读x和y,分配的读版本号多是190,结果他只能都到T2的值,不能读到T1 !数组
问题的根源在于机器之间的时钟不一样,没有全局时钟。服务器
Google的Spanner(看这 和 这)和Percolator(看这和这)都是搞了一个全局时钟来解决,区别在于Percolator的全局时钟就是基于固定的一台服务器产生,全部的事务获取commit时间戳都问这个全局时钟服务器要,天然保证了单调递增。问题,显而易见,单点,性能,扩展性。Spanner利用原子钟和GPS接收器,实现了一个较为精确的时钟,这个时钟叫作TrueTime,每次调用TrueTime API返回的是一个时间区间,而不是一个具体的值,这个TrueTime保证的是真实时间(absolute time/real time)必定在这个区间内,这个区间范围一般大约14ms,甚至更小。app
下面说说Spanner是如何保证Linearizability(external consistency)。分布式
事务的执行过程当中,Spanner保证每一个事务最后获得的commit timestamp介于这个事务的start和commit之间。基于这个条件,若是T2在T1 commit完成后才start,那么显然,T2的commit timestamp确定大于T1的timestamp。性能
Spanner是如何保证每一个事务最后获得的commit timestamp介于这个事务的start和commit之间?
在事务开始阶段调用一次TrueTime,返回[t-ε1,t1+ε1],在事务commit阶段时再调用一次TrueTime,返回[t2-ε2,t2+ε2],根据TrueTime的定义,显然,只要t1+ε1<t2-ε2,那么commit timestamp确定位于start和commit之间。等待的时间大概为2ε,大约14ms左右。能够说,这个延时基本上还能够接受。
至于读请求,直接调用TrueTime API,拿着右界去读便可。
CockRoachDB是一个前Google员工创业的开源项目,基本上能够认为就是Spanner的开源实现。机器时钟经过NTP同步,基本能够保证机器间偏差在150ms左右。
若是按照Spanner的作法,写事务提交时每次都须要等待150ms,性能基本不可接受,固然CockRoachDB可让客户端选择是否使用这种方案,这种方法实现了Linearizability,能够性能太差,由于时钟偏差太大,和Spanner的高精度时钟无法比。
CockRoachDB作了一点work around,同时实现了一种比Linearizability更relax一点的一致性模型,能够保证下面两种状况的Linearizability。
CockRoachDB 实现了单个客户端的Linearizability,保证同一个客户端前后发出去的两个事务T1和T2,T2的commit timestamp比T1的commit timestamp更大。方法就是T1事务执行完成会将commit timestamp返回给客户端,客户端执行T2时提供一个更大的时间戳给server,告诉server,T2的commit timestamp必须比这个时间戳更大。这样就保证了单个客户端的Linearizability。
假设有两个客户端C1和C2,C1先执行写事务T1,请求发送给了机器M1,其中须要修改x,T1 commit后,C2写事务T2 start,请求发给了机器M2,事务也须要修改x,CockRoachDB能够保证T2分配到的commit timestamp比T1更大。
说这个以前,先看看如何界定两个事件的前后顺序。
经过捕捉两个事件的因果关系能够给两个事件定序,主要基于以下两条规则:
Vector Clock能够用来维护这种因果关系,基本原理就是在一个有N个节点的集群中,每一个机器都维护一个大小为N的数组(Vector),数组记做VC,机器i上的VC[k]表明机器i对机器k的clock的认知。每一个机器i在发消息m时都会将本地的VC[i]加1(更新本地的clock),而后用它标记消息m,最后把消息发出。每一个接收到消息的机器都会取本身的clock和消息中的clock的最大值来更新本身的clock(更新本地的clock)。这个clock实际上就是Logical Clock。Logical Clock越大说明这台机器的"时间"越靠后。在这个思想中,实际上,假设的是机器和机器之间的物理时钟差是无穷大的,只要两台机器之间没有进行过消息交互,那么这两台机器互相之间对对方没有任何知识。那么,显然,因为这种logical clock的实现和物理时间没有任何关系,在真实的系统中,没法知足快照读:读2015年3月29日以前1点的最新数据。
而Spanner的TrueTime API和上述方法是两个极端,彻底不捕捉事务之间的因果关系,纯粹的根据TrueTime来对事件进行定序。
而CockRoachDB使用了Hybrid Logical Clock(HLC),它是另一种Logical Clock的实现,它将Logical Clock和物理时钟(wall time)联系起来,而且他们之间的偏差在一个固定的值以内。这个值是由NTP决定的。每台机器更新HLC的算法和上面描述VC的过程大同小异。这种Logical Clock的实现很是简单,这里就不展开,具体看这篇论文。实际上,HLC带来了两点好处:
回到这一小节最开始的例子:
假设有两个客户端C1和C2,C1先执行写事务T1,请求发送给了机器M1,其中须要修改x,T1 commit后,C2写事务T2 start,请求发给了机器M2,事务也须要修改x,CockRoachDB能够保证T2分配到的commit timestamp比T1更大。
那么只要分布式事务的coordinator在肯定事务的commit timestamp的过程当中询问各个参与者participants的本地HLC,选取其中最大的HLC做为事务的commit timestamp,便可知足Linearizability的要求。
能够看出,CockRoachDB实际上实现了两种一致性级别,第一种就是Linearizability,实现方式和Spanner同样,commit的时候都须要等(实际上,Spanner不是每次都要等,而CockRoachDB每次都须要等),可是因为其时钟偏差很大,实际性能不好。第二种就是比Linearizability更宽松一点的一致性,这种一致性级别能够保证同一个客户端的Linearizability和相关事务的Linearizability。
Spanner: Google’s Globally-Distributed Database
Logical Physical Clocks
and Consistent Snapshots in Globally Distributed Databases
Beyond TrueTime: Using AugmentedTime for Improving Spanner
Spencer Kimball on CockroachDB, talk given at Yelp, 9/5/2014