程序中的唯一标识符对于跟踪非常有用。当这些 id 包含高分辨率时间戳时,它们会更加有用。
唯一标识符不仅记录事件的时间,而且是唯一可以帮助跟踪通过系统的事件。
这种独特的时间戳根据实现方式的不一样,所需要的成本会比较高。
接下来我们探讨了一种轻量级的方法,可以在我们研发中生成一个独特的、单调递增的纳秒分辨率时间戳。
唯一标识符的用途
唯一标识符可用于与一条信息相关联,以便以后可以明确地引用信息。它可以是事件、请求、订单 ID 或客户 ID。
它们的业务可以用作数据库或键/值存储中的主键,以便后面检索辨识该信息。
生成这些标识符的挑战之一是在不增加成本的同时避免创建重复项。
我们可以记录在数据库中创建的每个标识符,但是我们要添加更多标识符时,这会使用 O(n) 存储。
您可以生成一个随机标识符,例如不太可能重复的 UUID,但是,这会创建比较大 id(不要看只是一个字符串,当量大时,就非常庞大了),否则不包含任何信息。例如,UUID 可能看起来像
**
d85686f5-7a53-4682-9177-0b64037af336**
此 UUID 可以存储为 16 个字节,但通常存储为占用 40 个字节内存的对象。
使用 256 位可降低重复标识符的风险,但会使内存增加一倍。
时间戳作为唯一标识符
使用时间戳有两个好处。您不需要存储太多信息,因为时钟是驱动程序的。您只需要检查两个不同时间的线程,缺点是在重新启动时丢失,例如,时钟时间应该已经足够长,仍然不会得到重复的时间戳。
这样的标识符也更容易阅读,并提供对跟踪有用的附加信息。基于时间戳的唯一标识符可能类似于**
2021-12-20T23:30:51.8453925**
这个时间戳可以存储在 LocalDateTime 对象中,可以存储为 8 个字节长。
MappedUniqueTimeProvider 代码
这是GitHub 上提供的MappedUniqueTimeProvider的精简版
/** * Timestamps are unique across threads/processes on a single machine. */
public enum MappedUniqueTimeProvider implements TimeProvider {
INSTANCE;
private final Bytes bytes;
private TimeProvider provider = SystemTimeProvider.INSTANCE;
MappedUniqueTimeProvider() {
String user = System.getProperty("user.name", "unknown");
MappedFile file = MappedFile.mappedFile(OS.TMP + "/.time-stamp." + user + ".dat", OS.pageSize(), 0);
bytes = file.acquireBytesForWrite(mumtp, 0);
}
@Override
public long currentTimeNanos() throws IllegalStateException {
long time = provider.currentTimeNanos(), time5 = time >>> 5;
long time0 = bytes.readVolatileLong(LAST_TIME), timeNanos5 = time0 >>> 5;
if (time5 > timeNanos5 && bytes.compareAndSwapLong(LAST_TIME, time0, time))
return time;
while (true) {
time0 = bytes.readVolatileLong(LAST_TIME);
long next = (time0 + 0x20) & ~0x1f;
if (bytes.compareAndSwapLong(LAST_TIME, time0, next))
return next;
Jvm.nanoPause();
}
}
}
以下技术已用于确保时间戳的唯一性和效率
内存共享
TimeProvider 使用共享内存来确保纳秒分辨率时间是唯一的。内存映射文件以线程安全的方式访问,以确保时间戳单调递增。Chronicle Bytes有一个库支持对内存映射文件的线程安全访问。
读取内存映射文件中的值并尝试在循环中更新。CAS 或compare-and-swap操作是原子的,并检查先前的值没有被另一个线程更改。当然,这是在同一台服务上的一个线程上操作。
存储一个纳秒的时间戳
我们使用原始的 long 来存储时间戳可以提高效率,但这可更难使用,我们支持print和解析称为
NanoTimestampLongConverter的长时间戳,我们也将这些时间戳解析并隐式呈现为文本使其更容易打印、调试和创建单元测试。
public class Event extends SelfDescribingMarshallable {
@LongConversion(NanoTimestampLongConverter.class) long time;
}
Event e = new Event();
e.time = CLOCK.currentTimeNanos();
String str = e.toString();
Event e2 = Marshallable.fromString(str);
System.out.println(e2);
Prints
!net.openhft.chronicle.wire.Event {
time: 2021-12-20T23:30:51.8453925
}
由于纳秒时间戳是一种高分辨率格式,它只会持续到 2262 年作为有符号长整数或 2554 年,值溢出之前,可以假设它是无符号长整数。
我们已经将时间戳中的额外位置用于其他目的,例如存储主机标识符或源 ID。出于这个原因,我们还确保时间戳对于 32 ns 的倍数是唯一的,我们如果愿意,可以将低 5 位用于其他目的。
效果
在正常操作下,在服务器上获得唯一的纳秒时间戳需要不到 50 ns。在繁重的多线程负载下,可能需要几百纳秒。
MappedUniqueTimeProvider 应用程序可以维持超过 3000 万/秒的生成。
可重启性
只要时间不倒退,这种策略就可以丢失所有状态,但仍能确保仅从时钟上的唯一性。如果时钟时间确实倒退了一个小时,那么状态将确保没有重复,但是,在时钟赶上之前,时间戳不会与时钟匹配。
结论
可以有一个轻量级的、唯一的标识符生成器来保存纳秒时间戳。