时间戳与一致性

时间戳

为什么需要时间戳

数据库遵循ACID的四大特性,在多副本间保证数据的一致性,在分布式架构中,为了在多个用户同时访问数据库时保证这种性质,需要使用并发控制方法,实现并发控制的技术主要有两种:锁(locking)技术和时标(timestamp)技术。

锁技术是用户在对某个数据对象(可以是数据项、记录、数据集以至整个数据库)进行操作之前,必须先向系统发出请求获得相应的锁,以此保证对共享数据段的保护。而时标技术则是对用户的操作顺序进行定序,保证互相冲突的操作不会同时发生。

两种方法的具体实现方式包括了 2PL、MVCC 等,2PL 是最经典的基于读写锁的并发控制,但效率往往较低,已经不被时下的 DBMS 使用,而 MVCC 则是当下主流的并发控制方式。

数据库时间戳 java什么类型 数据库时间戳协议_数据库


数据库时间戳 java什么类型 数据库时间戳协议_数据库_02

MVCC 的全称是多版本并发控制(Multi-Version Concurrency Control),即同时保存数据项的多个版本,MVCC 通过比较 Snapshot 读取事务ID 和数据上的写入事务 ID,其中最大但不超过读事务ID 的版本,即为可见的版本。事务数据的管理可以以分布式的形式进行管理,也可以集中式的管理,一些数据库使用了这样的方案:

  • MySQL 的 ReadView 实现就是基于事务 ID 大小以及活跃事务列表进行可见性判断。事务 ID 在事务开启时分配,体现了事务 begin 的顺序;提交时间戳 commit_ts 在事务提交时分配,体现了事务 commit 的顺序。

参考:MySQL高级之MVCC机制详解(七)_贤子磊的博客-CSDN博客

  • 数据库 Postgres-XL 也用了同样的方案,只是将这套逻辑放在全局事务管理器(GTM)中,由 GTM 集中式地维护集群中所有事务状态,并为各个事务生成它们的 Snapshot。

参考:https://cloud.tencent.com/developer/article/2065492

这种方案将事务的数据和状态集中管理,实现上更简洁,但是制约了数据库的扩展性。

另一套方案是将事务的相关数据交由节点自行管理,同时引入真实的时间戳,只要比较数据的写入时间戳(即写入该数据的事务的提交时间戳)和 Snapshot 的读时间戳,即可判断出可见性。在单机数据库中产生时间戳很简单,用原子自增的整数就能以很高的性能分配时间戳。Oracle 用的就是这个方案。

数据库时间戳 java什么类型 数据库时间戳协议_mysql_03

而在分布式数据库中,最直接的替代方案是引入一个集中式的时间戳分配器,称为TSO(Timestamp Oracle,此 Oracle 非彼 Oracle),由 TSO 提供单调递增的时间戳。TSO 看似还是个单点,但是考虑到各个节点取时间戳可以批量(一次取 K 个),即便集群的负载很高,对 TSO 也不会造成很大的压力。TiDB 用的就是这套方案。

分布式时间戳

时间戳作为天然自增的数列,满足我们对该全序性的要求。但在集群中,要求每台服务器都能在同一时刻得到相同的时间是困难的。如果不能做到这一点,就不能使用服务器上的时间戳作为事务的时间戳,这会导致整个集群无法满足数据的一致性,举例来说,对于集群中的一台服务器,有两个 client 对其发来事务命令,他们都以各自服务器上的时间作为时间戳,理论上来说,这个执行命令的服务器应该按照其携带的时间戳,而非两个命令抵达的先后顺序,但是。假设 client A 所取到的时间戳并非正确的时间戳,其时间戳较正式的时间戳小很多,那么可能我们在一段时间内始终都在优先执行 client A 的命令,这并不是正确的行为。

数据库时间戳 java什么类型 数据库时间戳协议_数据库_04

同样的事发生在多种场景下,两个client 分别向两个保存相同数据的数据库副本发出执行命令,在这种情况下,命令地抵达顺序将是随机的,我们应该遵循使用时间戳来保证副本数据的一致性,前提是时间戳需要时正确的。

数据库时间戳 java什么类型 数据库时间戳协议_mysql_05

归根揭底,是我们没有一个可靠的自增序列,来保证全局唯一。但是想要获取这样的时间戳却并不容易,服务器中虽然时钟同步的功能,但是返回的时间与标准时间相比都有一定的误差,基于这种需求,人们提出了许多不同的时间同步方案。

NTP 协议

NTP 协议是最常见的时间同步方案,1985年由特拉华大学的David L. Mills设计提出。NTP协议的目标是将所有计算机的时间同步到几毫秒误差内。实际上广域网可以达到几十毫秒的误差,局域网误差可以在1毫秒内。NTP协议是一种主从式架构协议,使用分层的时钟源系统,每一层称为Stratum,阶层的上限是15,阶层16表示未同步设备。常见的阶层如下:

数据库时间戳 java什么类型 数据库时间戳协议_数据库时间戳 java什么类型_06

除过第一阶层是同0阶层直接连接,其他每一阶层会向上一阶层请求时间,以同步自身系统的时间,由于从发起请求到收到回复需要一定的时间间隔,这也造成了 NTP 系统中的时间误差,具体的同步流程如下所示:

数据库时间戳 java什么类型 数据库时间戳协议_database_07

其导致的时间误差可表示为:
数据库时间戳 java什么类型 数据库时间戳协议_database_08

Lamport Timestamp

由于完全同步时间戳总会收到网络传输等因素的影响而存在误差,而时间戳的存在是为了确立在分布式系统中真实的顺序关系,因此只要能准确表示出事件发生的顺序关系就可以取代真实的时间戳。Leslie Lamport 在1978年的论文《Time, Clocks, and the Ordering of Events in a Distributed System》提出了 Lamport 逻辑时钟,就是基于这样一种思想实现的。

向量时间戳

在 Lamport 逻辑时钟中,存在着这样一个问题:对于任意两个事件 数据库时间戳 java什么类型 数据库时间戳协议_database_09数据库时间戳 java什么类型 数据库时间戳协议_数据库_10 ,如果 数据库时间戳 java什么类型 数据库时间戳协议_数据库_11,那么 数据库时间戳 java什么类型 数据库时间戳协议_数据库时间戳 java什么类型_12,但是反向并不成立,数据库时间戳 java什么类型 数据库时间戳协议_database_13 推不出来 数据库时间戳 java什么类型 数据库时间戳协议_数据库_14向量时钟(Vector Clocks)是这个问题的一种可行的解决方式,可以保证反向也能成立。

这一设计在1986 年由 Barbara Liskov, Rivka Ladin提出,虽然彼时这个机制还未被正式称为 Vector Clocks(当时是叫做"multipart timestamp")。

向量时钟可以解决 Lamport 逻辑时钟中存在的问题,它的思想是进程间通信的时候,不光同步本进程的时钟值,还同步自己知道的其他进程的时钟值,具体来说:分布式系统中每个进程数据库时间戳 java什么类型 数据库时间戳协议_c++_15保存一个本地逻辑时钟向量值数据库时间戳 java什么类型 数据库时间戳协议_mysql_16,向量的长度是分布式系统中进程的总个数。数据库时间戳 java什么类型 数据库时间戳协议_数据库_17 表示进程数据库时间戳 java什么类型 数据库时间戳协议_c++_15在与进程数据库时间戳 java什么类型 数据库时间戳协议_database_19在通讯时记录的本地逻辑时钟值。

数据库时间戳 java什么类型 数据库时间戳协议_mysql_16 的更新算法同 Lamport 逻辑时钟数据库时间戳 java什么类型 数据库时间戳协议_mysql_16的 类似,仅有细微的不同:

  1. 数据库时间戳 java什么类型 数据库时间戳协议_mysql_22在执行一个本地事件之前,对所记录的本地时间戳数据库时间戳 java什么类型 数据库时间戳协议_database_23进行自增。
V[i] = V[i] + 1;
  1. 当发生进程间的通讯事件时,数据库时间戳 java什么类型 数据库时间戳协议_mysql_22 会自增本地时间戳,然后将整个向量时间戳数据库时间戳 java什么类型 数据库时间戳协议_mysql_25发送给对应的进程数据库时间戳 java什么类型 数据库时间戳协议_mysql_26
V[i] = V[i] + 1;
  1. 对于事件的接收方,数据库时间戳 java什么类型 数据库时间戳协议_mysql_26会增加所记录的数据库时间戳 java什么类型 数据库时间戳协议_数据库时间戳 java什么类型_28的时间戳,然后对于检查传递来的向量时间戳数据库时间戳 java什么类型 数据库时间戳协议_mysql_25,更新自身时间戳。
V[i] = V[i] + 1
V[j] = max(Vmsg[j], V[j]) for j ≠ i

现在,对于进程数据库时间戳 java什么类型 数据库时间戳协议_c++_30数据库时间戳 java什么类型 数据库时间戳协议_c++_31上的任意事件,可以通过以下的规则比较事件的发生顺序(假设其时间戳V1,V2):

V1 = V2 , iff V1[i] = V2[i], for all i = 1 to N (i.e, V1 and V2 are equal if and only if all the corresponding values in their vector matches)
V1 ≤ V2 , iff V1[i] ≤ V2[i], for all i = 1 to N

进而得出两个任意事件间的关系:

  • 顺序发生:
V1 < V2 , iff  V1 ≤ V2 & there exists a j such that 1 ≤ j ≤ N & V1[j] < V2[j]
  • 并发:
NOT (V1 ≤ V2 ) AND NOT (V2 ≤ V1 )

向量时间戳的应用

数据库时间戳 java什么类型 数据库时间戳协议_数据库_32

这张图展示了在不同事件发生时,不同线程上记录的向量时间戳是如何变化的:

其中 数据库时间戳 java什么类型 数据库时间戳协议_database_09数据库时间戳 java什么类型 数据库时间戳协议_数据库_10数据库时间戳 java什么类型 数据库时间戳协议_mysql_35 这些事件都是顺序发生的,因为他们的向量时间戳都是可以比较的,而 数据库时间戳 java什么类型 数据库时间戳协议_数据库_10数据库时间戳 java什么类型 数据库时间戳协议_c++_37

向量时间戳解决了 Lamport 逻辑时钟中时间戳大小无法推导事件发生顺序的问题,但他依然仅仅是一个逻辑时钟,无法对并发事件的定序

wiki 上也列出了其他相关机制:

  • In 1999, Torres-Rojas and Ahamad developed Plausible Clocks,[ref] a mechanism that takes less space than vector clocks but that, in some cases, will totally order events that are causally concurrent.
  • In 2005, Agarwal and Garg created Chain Clocks,[ref] a system that tracks dependencies using vectors with size smaller than the number of processes and that adapts automatically to systems with dynamic number of processes.
  • In 2008, Almeida et al. introduced Interval Tree Clocks.[ref][ref][ref] This mechanism generalizes Vector Clocks and allows operation in dynamic environments when the identities and number of processes in the computation is not known in advance.
  • In 2019, Lum Ramabaja developed Bloom Clocks,[ref] a probabilistic data structure whose space complexity does not depend on the number of nodes in a system. If two clocks are not comparable, the bloom clock can always deduce it, i.e. false negatives are not possible. If two clocks are comparable, the bloom clock can calculate the confidence of that statement, i.e. it can compute the false positive rate between comparable pairs of clocks.

根据介绍可以看到,这些 Vector Clocks 的修改方案大多都是针对由于服务器数量增长而导致的向量时钟增长,包括压缩等等方法,都是为了使用更少的内存空间或者更快的检测。

TrueTime

Vector Clocks 和 Lamport 逻辑时钟都是依赖逻辑时钟解决问题的方式,这也是物理时钟的不可靠所导致的,面对这一问题,Google选择提升物理时钟上的准确性来尝试解决,这个方案首先应用在 Spanner 数据库上(Spanner: Google’s Globally-Distributed Database)。Google Spanner 是一个定位于全球部署的数据库。如果用 TSO 方案则需要横跨半个地球拿时间戳,延迟是较高的。但是 Google 的工程师认为 linearizable 是必不可少的,这就有了 TrueTime。

TrueTime 利用原子钟和 GPS 实现了时间戳的去中心化。之所以使用这两种硬件的原因是因为这两种硬件的故障原因不一样,GPS时钟故障的原因有天线和接收器故障,无线信号干扰等。原子钟可能由于频率问题造成时钟漂移。这两种原因是不相交的,所以能提高整个硬件的可靠性。

即使如此,原子钟和 GPS 提供的时间也是有误差的,TrueTime时钟的误差范围 ε 是1ms到7ms,平均4ms。

数据库时间戳 java什么类型 数据库时间戳协议_mysql_38

数据库时间戳 java什么类型 数据库时间戳协议_数据库_39
TrueTime API

TrueTime提供了三个API来操作时间:

Method

Returns

TT.now()

TTinterval: [earliest, latest]

TT.after(t)

true if t has definitely passed

TT.before(t)

true if t has definitely not arrived

  • 数据库时间戳 java什么类型 数据库时间戳协议_mysql_40 返回的是当前时间,由于时钟硬件误差的存在,这个当前时间存在一个不确定的范围(uncertainty time),也即一个范围 数据库时间戳 java什么类型 数据库时间戳协议_database_41,可以保证当前绝对时间一定在这个范围内,上面介绍过,这个间隔范围最大是 7ms。
  • 数据库时间戳 java什么类型 数据库时间戳协议_c++_42 判断传入的时间戳是否已经是过去的时间,也即 数据库时间戳 java什么类型 数据库时间戳协议_数据库时间戳 java什么类型_43
  • 数据库时间戳 java什么类型 数据库时间戳协议_c++_44 判断传入的时间戳是否是未来的时间,也即 数据库时间戳 java什么类型 数据库时间戳协议_c++_45

使用TrueTime API时,需要搭配下面两个规则。

  • Start: 提交事务数据库时间戳 java什么类型 数据库时间戳协议_database_46时,leader必须选择一个大于等于 数据库时间戳 java什么类型 数据库时间戳协议_数据库_47 的时间作为提交时间戳 数据库时间戳 java什么类型 数据库时间戳协议_mysql_48
  • Commit Wait: leader必须等待 数据库时间戳 java什么类型 数据库时间戳协议_database_49 为true后才能提交数据,也即必须等待数据库时间戳 java什么类型 数据库时间戳协议_mysql_48的绝对时间过去了才能提交数据。

使用这两个规则可以保证:如果事务 T1 提交后 T2 才开始,那么 T2 的提交时间一定晚于 T1 的提交时间。也就是说事务的提交顺序一定和事务发生的绝对时间上的顺序一致。

TrueTime应用

数据库时间戳 java什么类型 数据库时间戳协议_数据库_51

举个例子:分布式事务中有三台服务器 S1,S2,S3。执行分布式事务时,某一台参与者作为协调者提交事务,提交时使用这次事务所有参与者中最大的时间戳作为事务的提交时间。每台服务器和绝对时间 Tabs 都有误差,S1的时间比绝对时间快5ms,即 Tabs + 5,S2 的时间比绝对时间慢4ms,即 Tabs - 4,S3 的时间比绝对时间慢2ms,即 Tabs - 2。

  • 现在有一个事务T1,参与者包括 S1 和 S2,S1 执行分支事务的本地时间是15ms,S2 执行分支事务的本地时间是7ms。S2 作为协调者,提交事务时选择了**15ms **作为整个事务的执行时间。
  • 另外一个事务 T2,参与者包括 S2 和 S3,S3 执行分支事务的本地时间是13ms,S2 执行分支事务的本地时间是12ms。S2 还是作为协调者,提交事务时选择了 13ms 作为整个事务的执行时间。

在绝对时间上事务 T2 比 T1要晚执行,但是提交时间 T2 却比 T1 要早,这显然是错误的。

我们看看使用TrueTime如何解决这个问题。

数据库时间戳 java什么类型 数据库时间戳协议_mysql_52

假设 TrueTime 误差 ε 为7ms:

  • T1 事务协调者选择提交时间 数据库时间戳 java什么类型 数据库时间戳协议_database_53 时,根据 Start 规则,必须大于所有事务参与者中最大的本地时间,还要大于协调者本地数据库时间戳 java什么类型 数据库时间戳协议_数据库时间戳 java什么类型_54。计算得出 数据库时间戳 java什么类型 数据库时间戳协议_database_55
    选择提交时间 数据库时间戳 java什么类型 数据库时间戳协议_database_53后,根据 Commit Wait 规则,还要等待 数据库时间戳 java什么类型 数据库时间戳协议_c++_57 为true后才能提交数据。也就是 数据库时间戳 java什么类型 数据库时间戳协议_数据库_58 是 [16, 30],S2的本地时间是 23ms,绝对时间 27ms 提交数据。
  • T2 事务协调者选择提交时间 数据库时间戳 java什么类型 数据库时间戳协议_c++_59 时,根据 Start 规则,计算得出 数据库时间戳 java什么类型 数据库时间戳协议_数据库时间戳 java什么类型_60
    选择提交时间 数据库时间戳 java什么类型 数据库时间戳协议_c++_59 后,根据 Commit Wait 规则,还要等待 数据库时间戳 java什么类型 数据库时间戳协议_数据库时间戳 java什么类型_62 为true后才能提交数据。也就是 数据库时间戳 java什么类型 数据库时间戳协议_数据库_58 是[20, 34],S2 的本地时间是 27ms,绝对时间 31ms 提交的数据。

可以看出 T2 的提交时间要晚于 T1,解决了这个例子中的问题。当然,实际处理时,还会对数据进行加锁等操作,Google Spanner 中详细的事务处理流程。

混合逻辑时钟

无论是物理时钟,包括 NTP协议 和 TrueTime 都属于物理时钟,另一种是逻辑时钟,包括 Lamport逻辑时钟 和 向量时钟(Vector clocks)。两种时钟有各自的优缺点。物理时钟的优点在于直观,使用方便,缺点在于无法做到绝对精确,成本相对高一些。逻辑时钟的优点在于可以做到精确的因果关系,缺点在于依赖节点之间需要通信,而且使用上不如物理时钟直观。

HLC 由 Sandeep Kulkarni, Murat Demirbas等人在2014年的论文《Logical Physical Clocks and Consistent Snapshots in Globally Distributed Databases》中提出。目的是为了填补理论(逻辑时钟)和实际(物理时钟)之间的差距,建立起既支持因果关系,同时又有物理时钟的直观特点的时间戳。

给分布式系统中每个事件分配一个HLC,比如 e 事件的HLC记作 数据库时间戳 java什么类型 数据库时间戳协议_database_64,HLC保证能够满足以下四个性质:

  1. 如果 e 事件发生在 f 事件之前(e happened before f),那么 数据库时间戳 java什么类型 数据库时间戳协议_c++_65 一定小于 数据库时间戳 java什么类型 数据库时间戳协议_数据库_66,也就是满足因果关系。
  2. 数据库时间戳 java什么类型 数据库时间戳协议_c++_65
  3. 数据库时间戳 java什么类型 数据库时间戳协议_c++_65
  4. 数据库时间戳 java什么类型 数据库时间戳协议_c++_65 的值和 e 事件发生的物理时钟值接近,数据库时间戳 java什么类型 数据库时间戳协议_数据库_70的值会小于一定的范围。(数据库时间戳 java什么类型 数据库时间戳协议_数据库_71

HLC 同时包含了物理时钟和逻辑时钟,这两种时钟不能混为一谈,不同时钟引起的时间增加应该是独立的,不然 HLC 的设计就没有意义了,因此 HLC 分成两部分 数据库时间戳 java什么类型 数据库时间戳协议_数据库时间戳 java什么类型_72数据库时间戳 java什么类型 数据库时间戳协议_数据库时间戳 java什么类型_73数据库时间戳 java什么类型 数据库时间戳协议_数据库时间戳 java什么类型_72 表示事件 数据库时间戳 java什么类型 数据库时间戳协议_mysql_35 发生时所感知到的最大物理时钟值,数据库时间戳 java什么类型 数据库时间戳协议_数据库时间戳 java什么类型_73 是事件 数据库时间戳 java什么类型 数据库时间戳协议_mysql_35 的逻辑时钟部分,当几个事件在同一个物理时钟值内发生时,数据库时间戳 java什么类型 数据库时间戳协议_database_78 用于记录事件之间的因果关系。数据库时间戳 java什么类型 数据库时间戳协议_mysql_79

  1. 发送消息事件或者本地事件,需要判断 数据库时间戳 java什么类型 数据库时间戳协议_数据库_80数据库时间戳 java什么类型 数据库时间戳协议_database_81之间的关系,来决定 数据库时间戳 java什么类型 数据库时间戳协议_数据库_82
if pt.j <= l.j then c.j = c.j + 1
else c.j = 0; l.j = pt.j
  1. 当线程 数据库时间戳 java什么类型 数据库时间戳协议_mysql_22
// 如果j事件,m消息和本地物理时钟的值相同,增加逻辑时钟部分
  if l.j == l.m == pt.j then c.j = max(c.j, c.m) + 1
  // 如果本地物理时钟没赶上HLC的物理时钟,并且j事件的逻辑时钟更大,更新逻辑时钟的值
  else if pt.j <= l.j and l.m <= l.j then c.j = c.j + 1
  // 如果本地物理时钟没赶上HLC的物理时钟,并且m消息的逻辑时钟更大,更新HLC的逻辑时钟部分和物理时钟部分
  else if pt.j <= l.m and l.j <= l.m then c.j = c.m + 1; l.j = l.m
  else c.j = 0; l.j = pt.j

新算法执行的过程中,本地时钟的值通过 NTP 协议更新,HLC 的值并不会修改本地时钟的值。由于分离了物理时钟和逻辑时钟,新的事件发生时,如果物理时钟部分的值没增长,就只增加逻辑时钟部分的值。如果本地的物理时钟赶上了HLC的物理时钟部分的值 数据库时间戳 java什么类型 数据库时间戳协议_数据库时间戳 java什么类型_72,就可以重置逻辑时钟部分的值 数据库时间戳 java什么类型 数据库时间戳协议_数据库时间戳 java什么类型_73,并把 数据库时间戳 java什么类型 数据库时间戳协议_数据库时间戳 java什么类型_72

对于任何一个事件 j,数据库时间戳 java什么类型 数据库时间戳协议_c++_87,也即 HLC 的物理时钟部分的值一定大于等于本地NTP的时钟值。假设整个分布式系统中,NTP协议的时钟误差值为 ε。新算法中,对于任何一个事件 j,数据库时间戳 java什么类型 数据库时间戳协议_mysql_88,也就是HLC物理部分的值和本地物理时钟值的差距不会超过 ε。这个误差值在局域网内大概1毫秒内,广域网可能达到100毫秒或更大。

HLC应用

HLC可以用于分布式数据库一致性快照读的处理中,很多系统中都使用了HLC,比如HBase和CockRoachDB。

数据库时间戳 java什么类型 数据库时间戳协议_数据库_89

比如,我们要获取 t = 10 这个时间点的数据快照,也即HLC为(l = 10, c = 0),拿这个HLC值去每个节点查找,可以得出上图中黑色的粗线,这条线对应的数据就是系统在 t = 10 的数据快照。

CockRoachDB在分布式事务中使用了HLC。根据 HLC 的性质4,数据库时间戳 java什么类型 数据库时间戳协议_数据库_90的值会小于一定的范围 数据库时间戳 java什么类型 数据库时间戳协议_c++_91,CockRoachDB默认这个值为500毫秒,数据库时间戳 java什么类型 数据库时间戳协议_数据库_92 一定是系统中最大的物理时间。启动事务时会获取本地的 HLC 值 数据库时间戳 java什么类型 数据库时间戳协议_database_93,并且确定一个区间 数据库时间戳 java什么类型 数据库时间戳协议_mysql_94,然后发往其他节点执行快照读,如果节点上某个数据的HLC值为 数据库时间戳 java什么类型 数据库时间戳协议_c++_95,分三种情况考虑:

  1. 如果 数据库时间戳 java什么类型 数据库时间戳协议_database_96,即处于区间的右边,那么e事件肯定发生在g事件之前,不能读取这个数据。
  2. 如果 数据库时间戳 java什么类型 数据库时间戳协议_数据库_97,即处于区间之中,由于物理时钟的不确定性,不能分辨出 e 事件和 g 事件的先后关系。这个时候需要重启事务,获取一个更大的 数据库时间戳 java什么类型 数据库时间戳协议_mysql_98,相当于等待这个不确定的时间过去,推迟事务的执行。
  3. 如果 数据库时间戳 java什么类型 数据库时间戳协议_c++_99,那么 g 事件肯定发生在 e 之前,这时可以读取这个数据。(对于这点我有点疑问,由于不确定时间的存在,物理时间可能快,也可能慢,这个区间应该是数据库时间戳 java什么类型 数据库时间戳协议_database_100,为什么这里 数据库时间戳 java什么类型 数据库时间戳协议_c++_99