在聊时间这个话题之前我们先了解两个概念:墙上时钟和单调时钟
墙上时钟:也称为墙上时间。大多是1970年1月1日(UTC)以来的秒数和毫秒数。墙上时间可以和NTP(Network Time Protocal,网络时间协议)同步,但是如果本地时钟远远快于NTP服务器,则强制重置之后会跳到先前某个时间点。
单调时钟:机器大多有自己的石英晶体振荡器,并将其作为计时器。单调时钟的绝对值没有任何意义,根据操作系统和语言的不同,单调时钟可能在程序开始时设为0、或在计算机启动后设为0等等。但是通过比较同一台计算机上两次单调时钟的差,可以获得相对准确的时间间隔。
在go 1.9之前,记录比较简单,就是1-1-1 00:00:00 到现在的整数s和ns数,以及时区数据。也就是墙上时钟
代码语言:javascript复制type Time struct {
// sec gives the number of seconds elapsed since
// January 1, year 1 00:00:00 UTC.
sec int64
// nsec specifies a non-negative nanosecond
// offset within the second named by Seconds.
// It must be in the range [0, 999999999].
nsec int32
// loc specifies the Location that should be used to
// determine the minute, hour, month, day, and year
// that correspond to this Time.
// The nil location means UTC.
// All UTC times are represented with loc==nil, never loc==&utcLoc.
loc *Location
}
在1.9之后记录了墙上时钟和单调时钟,wall和ext共同记录了时间,但是分为两种情况:
代码语言:javascript复制type Time struct {
wall uint64
ext int64
loc *Location
}
一种是没有记录单调时钟(比如是通过字符串解析得到的时间),另一种是记录了单调时钟(比如通过Now)。wall的第一位是一个标记位:
- 如果为1,则表示记录了单调时钟。则wall的2-34(闭区间)位记录了从1885-1-1到现在的秒数,最后30位记录了纳秒数。而ext记录了从程序开始运行到现在经过的单调时钟数。单位nanoseconds
- 如果为0,则表示没有记录单调时钟。则wall的2-34(闭区间)位全部为0(最后30位记录了纳秒数)。而ext记录了从1-1-1 00:00:00到现在经过的秒数。
单调时钟的计算逻辑如下
代码语言:javascript复制 var startNano = 0
func init(){
startNano = runtimeNano()
}
runtimeNano() - startNano
Time其实是实现了String函数的,因此可以print,打印出的时间格式为“年月日时分秒.小数秒” 精度为ns;后面还带着"m=±<value>",表示单调时钟的s数表示,小数点后精度到ns
代码语言:javascript复制func (t Time) String() string {
// String returns the time formatted using the format string
// "2006-01-02 15:04:05.999999999 -0700 MST"
//
// If the time has a monotonic clock reading, the returned string
// includes a final field "m=±<value>", where value is the monotonic
// clock reading formatted as a decimal number of seconds.
m1, m2 := m2/1e9, m29
m0, m1 := m1/1e9, m19
if m0 != 0 {
buf = appendInt(buf, int(m0), 0)
wid = 9
}
buf = appendInt(buf, int(m1), wid)
buf = append(buf, '.')
buf = appendInt(buf, int(m2), 9)
}
了解完golang的时间格式表示,我们过来看下mysql的时间格式表示:
- MySQL DATETIME存储包含日期和时间的值。从DATETIME列查询数据时,MySQL会以以下格式显示DATETIME值:YYYY-MM-DD HH:MM:SS。默认情况下,DATETIME的值范围为1000-01-01 00:00:00至9999-12-31 23:59:59。DATETIME使用5个字节进行存储。另外,DATETIME值可以包括格式为YYYY-MM-DD HH:MM:SS [.fraction]例如:2017-12-20 10:01:00.999999的尾数有小数秒。 当包含小数秒精度时,DATETIME值需要更多存储2017-12-20 10:01:00.999999需要8个字节,2015-12-20 10:01:00需要5个字节,3个字节为.999999,而2017-12-20 10:01:00.9只需要6个字节,小数秒精度为1字节。在MySQL 5.6.4之前,DATETIME值需要8字节存储而不是5个字节。
- TIMESTAMP需要4个字节,而DATETIME需要5个字节。 TIMESTAMP和DATETIME都需要额外的字节,用于分数秒精度。 TIMESTAMP范围从1970-01-01 00:00:01 UTC到2038-01-19 03:14:07 UTC。 如果要存储超过2038的时间值,则应使用DATETIME而不是TIMESTAMP。
总结下,也就是说常用的5.7版本,时间戳只能存到2038年,精度是秒,但是只需要4个字节,DATETIME存储的时间长度为5到8个字节,精度是微秒。
那么问题来了,当我们用golang驱动写mysql和从mysql查数据的时候,精度是什么样子的呢?显然写数据的时候会丢失精度从ns圆整到us
代码语言:javascript复制 result, err := db.Exec("insert into time_test(`ctime`) values(?)", time.Now())
然后查询会得到
代码语言:javascript复制'2023-02-21 22:55:39.980742 0800 CST m= 0.005420710'
元整发生在什么时候呢?在github.com/go-sql-driver/mysql 1.5.0版本和以前会在驱动里将时间元整到ms,但是1.6.0版本不再元整
代码语言:javascript复制https://github.com/go-sql-driver/mysql/commit/fe2230a8b20cee1e48f7b75a9b363def5f950ba0
就导致了一个有趣的现象,在mysql的各个版本中,因为mysql在处理时间参数的时候做了精度的元整,如果在datetime字段上加了索引,即使传了精度为ns的时间,也会走索引。但是对于marindb,如果传入的时间是ns精度,刚好把mysql驱动由1.5.0升级到了1.6.0会导致索引失效。