唯一标识符对追踪非常有用。当这些ID包含一个高分辨率的时间戳时,就更有用了。
它们不仅记录了一个事件的时间,而且如果是唯一的,可以帮助追踪事件在系统中的传递。然而,这种独特的时间戳可能是昂贵的,这取决于它们是如何实现的。
这篇文章探讨了一种轻量级的方法来产生一个独特的、单调增长的全系统纳秒级分辨率的时间戳,可在我们的开源库中使用。
唯一标识符的用途
唯一的标识符可以与一段信息联系在一起,这样信息就可以在以后毫不含糊地被提及。这可能是一个事件,一个请求,一个订单ID,或一个客户ID。
它们自然可以被用作数据库或键/值存储中的主键,以便以后检索该信息。
生成这些标识符的挑战之一是避免创建重复,同时又没有增加成本。
你可以在数据库中记录每一个创建的标识符,然而,当你增加更多的标识符时,这将使用O(n)存储。
你可以生成一个随机的标识符,如UUID,它不可能重复,然而,这就产生了大的标识符,否则就不包含任何信息。
d85686f5-7a53-4682-9177-0b64037af336
这个UUID可以存储在16个字节中,但是经常被存储为一个对象,占用40个字节的内存。
使用256位可以减少重复标识符的风险,但会使使用的内存增加一倍。
作为唯一标识符的时间戳
使用时间戳有两个好处。你不需要存储太多的信息,因为挂钟是驱动程序。你只需要检查两个时间不一样的线程,但如果这些信息在重启时丢失,例如,挂钟时间应该有足够的进展,你仍然不会得到一个重复的信息。
这样的标识符也更容易阅读,并提供额外的信息,对追踪很有用。一个基于时间戳的唯一标识符可以看起来像
2021-12-20T23:30:51.8453925
这个时间戳可以存储在LocalDateTime对象中,但是可以存储为只有8个字节的long,这也是我们通常使用的方式。
MappedUniqueTimeProvider代码
这是GitHub上提供的MappedUniqueTimeProvider 的简略 版本:
/**
* Timestamps are unique across threads/processes on a single machine.
*/
为了确保 时间戳的 唯一性和 效率,我们使用了以下技术
共享内存
这个TimeProvider使用共享内存来确保纳秒级分辨率的时间是唯一的。一个内存映射的文件以线程安全的方式被访问,以确保时间戳是单调增长的。Chronicle Bytes有一个库,支持对内存映射文件的线程安全访问。
内存映射文件中的一个长值被读取并试图在一个循环中被更新。CAS或比较和交换操作是原子性的,并检查前一个值是否被另一个线程改变过。这只对同一台机器上的一个线程成功。
在long中存储纳秒级的时间戳
为了提高效率,我们使用原始的long来存储时间戳,这可能更难处理,然而,我们有对打印和解析long时间戳的支持,称为NanoTimestampLongConverter我们的电线格式也解析和渲染这些时间戳为文本,隐含地使它更容易打印、调试和创建单元测试。
}
Event e = new Event();
e.time = CLOCK.currentTimeNanos();
String str = e.toString();
Event e2 = Marshallable.fromString(str);
System.out.println(e2);
!net.openhft.chronicle.wire.Event {
time: 2021-12-20T23:30:51.8453925
}
由于纳秒时间戳是一种高分辨率的格式,在数值溢出之前,它只能持续到2262年的有符号长条或2554年,如果你认为它是一个无符号长条。
我们已经将时间戳中的额外位用于其他目的,如存储主机标识符或源ID。出于这个原因,我们也确保时间戳是32 ns的倍数,如果我们愿意的话,允许我们将低5位用于其他目的。如果你自己实现这个,你可以放弃这个要求。
性能
在正常操作下,在现代机器上获得唯一的纳秒级时间戳需要远远低于50纳秒。在重度多线程负载的基准下,它可能需要几百纳秒。然而,这是假设时间戳被丢弃,所以我认为这样的基准是不现实的用例。
MappedUniqueTimeProvider应用程序可以持续生成超过3000万/秒的时间,并且由于上述技术而不会超过挂钟的运行速度。
可重启性
只要时间不往后退,这个策略就可以失去所有的状态,并且仍然可以确保仅来自挂钟的唯一性。如果挂钟时间确实向后退了一小时,状态将确保没有重复,但是,时间戳不会与挂钟匹配,直到挂钟追上。
结论
拥有一个轻量级的、唯一的标识符生成器是可能的,它可以保存纳秒级的时间戳。