物理时钟及逻辑时钟

Published: 10 Oct 2014 Category: 其它

简介

上一篇文章中我曾分析过为什么对于应用级事务而言乐观锁是唯一可行的解决方案。乐观锁需要一个版本列,它可以表示为:

  • 物理时钟(从系统时钟里获取的一个时间戳)
  • 逻辑时钟(一个自增的数值)

本文将证明为什么对于乐观锁机制而言逻辑时钟更为合适。

系统时间

系统时间是通过操作系统内部的时钟算法来提供的。这个可编程的内部计时器会周期性地发送一个中断信号(频率是1.193182MHz)。CPU接收到时间中断并将tick计数器加一。

Unix和Windows都将时间表示为从一个预定义的绝对参考时间(新纪元, epoch)开始后所经过的滴答数。操作系统时钟的精度从1ms(安卓)到100ns(Windows)甚至1ns不等(Unix)。

单调时间

为了能进行事件排序,版本号必须单调自增。本地计数器的自增是由一个单调函数来完成的,而系统时间并非总能返回一个单调的时间戳。

Java有两种获取当前系统时间的方式。你可以使用:

  1. System#currentTimeMillis(),它返回的是自Unix新纪元(1970年1月1日0点)以来所经历的毫秒数。

这个方法返回的结果时间并不是单调的,因为它返回的是经历的时间,而这可能会进行前后调整(如果使用NTP进行系统时间同步的话)。

要想获取当前的单调时间,你可以看下Peter Lawrey的解决方法或者Bitronix Transaction Manager中的MonotonicClock

  1. System#nanoTime(), 它返回的是从一个随意选择的参考时间后所经过的纳秒数。

这个方法会尝试使用当前操作系统的单调时钟实现,但是如果无法获取到单调时间的话,它会返回时钟时间(wall clock,也就是真实的时间)。

论据1:系统时间并不都是单调自增的。

数据库时间戳精度

SQL-92标准中将TIMESTAMP数据类型定义为YYYY-MM-DD hh:mm:ss。小数部分是可选的,每个数据库实现都可以有自己特定的时间戳数据类型:

数据库 实现方式
Oracle TIMESTAMP(9)可以使用最多9位的小数位数(也就是纳秒级的精度)
MSSQL DATETIME2精度为100ns
MySQL MySQL5.6.4给TIME,DATETIME,TIMESTAMP类型增加了毫秒级的精度(就是TIMESTAMP(6))早先的MySQL版本所有的时间类型都没有小数部分
PostgreSQL TIME和TIMESTAMP都是毫秒级的
DB2 TIMESTAMP(12)可以最多使用到小数点后12位(皮秒级精度)

说起时间戳的持久化,大多数数据库都能至少支持到小数点后6位(也)。MySQL的用户一直在期待能有一个更精确的时间类型,5.6.4版本中终于增加到了毫秒级的精度。

在5.6.4版本之前的MySQL上,任意一秒中都可能会存在更新丢失(Lost Update)。这是因为所有更新数据库中同一行的事务都会看到同样的时间戳(它指向的是当前运行时的这一秒的开始处)。

论据2:MYSQL5.6.4之前的版本的时间戳只能支持秒级的精度。

时间处理并非易事

自增本地的版本号通常来说更安全,因为它无需依赖外部的因素。如果数据库行已经包含一个更高的版本号,那么你的数据就是旧的了。这相当简单。

另一方面,时间是极难处理的一个维度。如果你不相信我的话,去看下处理夏令时(daylight saving time)都需要考虑哪些因素。

Java历经了8个版本才终于有了一套成熟的DATE/TIME API。而在应用的不同分层间处理时间则会使事情变得更复杂。

论据3:系统时间的处理是一项很有挑战的任务。你得处理闰秒夏令时时区,以及不同的时间标准

分布式计算中的教训

乐观锁讲的就是事件排序,因此通常我们只对happened-before的关系感兴趣。

在分布式计算中,逻辑时钟比物理时钟(系统时钟)更受欢迎,因为网络时间同步带来了不同的延迟。

序号版本类似于Lamport时间戳算法,一个事件只会自增一个计数器。

Lamport时间戳是为了多个分布式节点进行事件同步的,而数据库的乐观锁则要更简单一些,因为这里只有一个节点(数据库服务器),而所有的事务都是同步的(来自并发的客户端连接)。

论据4:分布式计算更青睐逻辑时钟而非物理时钟,因为我们只对事件的顺序感兴趣。

结论

使用物理时间看上去更方便,但事实上是一个不太成熟的方案。在分布式环境中,系统时间无法完美地同步。总而言之,当实现乐观锁机制时,你应该优先使用逻辑时钟。

英文原文链接