一種高效的唯一標識符
程序中的唯一標識符對于跟蹤非常有用。當這些 id 包含高分辨率時間戳?xí)r,它們會更加有用。
唯一標識符不僅記錄事件的時間,而且是唯一可以幫助跟蹤通過系統(tǒng)的事件。
這種獨特的時間戳根據(jù)實現(xiàn)方式的不一樣,所需要的成本會比較高。
接下來我們探討了一種輕量級的方法,可以在我們研發(fā)中生成一個獨特的、單調(diào)遞增的納秒分辨率時間戳。
唯一標識符的用途
唯一標識符可用于與一條信息相關(guān)聯(lián),以便以后可以明確地引用信息。它可以是事件、請求、訂單 ID 或客戶 ID。
它們的業(yè)務(wù)可以用作數(shù)據(jù)庫或鍵/值存儲中的主鍵,以便后面檢索辨識該信息。
生成這些標識符的挑戰(zhàn)之一是在不增加成本的同時避免創(chuàng)建重復(fù)項。
我們可以記錄在數(shù)據(jù)庫中創(chuàng)建的每個標識符,但是我們要添加更多標識符時,這會使用 O(n) 存儲。
您可以生成一個隨機標識符,例如不太可能重復(fù)的 UUID,但是,這會創(chuàng)建比較大 id(不要看只是一個字符串,當量大時,就非常龐大了),否則不包含任何信息。例如,UUID 可能看起來像
d85686f5-7a53-4682-9177-0b64037af336。
此 UUID 可以存儲為 16 個字節(jié),但通常存儲為占用 40 個字節(jié)內(nèi)存的對象。
使用 256 位可降低重復(fù)標識符的風(fēng)險,但會使內(nèi)存增加一倍。
時間戳作為唯一標識符
使用時間戳有兩個好處。您不需要存儲太多信息,因為時鐘是驅(qū)動程序的。您只需要檢查兩個不同時間的線程,缺點是在重新啟動時丟失,例如,時鐘時間應(yīng)該已經(jīng)足夠長,仍然不會得到重復(fù)的時間戳。
這樣的標識符也更容易閱讀,并提供對跟蹤有用的附加信息?;跁r間戳的唯一標識符可能類似于2021-12-20T23:30:51.8453925。
這個時間戳可以存儲在 LocalDateTime 對象中,可以存儲為 8 個字節(jié)長。
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);
}
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();
}
}
}
以下技術(shù)已用于確保時間戳的唯一性和效率
內(nèi)存共享
TimeProvider 使用共享內(nèi)存來確保納秒分辨率時間是唯一的。內(nèi)存映射文件以線程安全的方式訪問,以確保時間戳單調(diào)遞增。Chronicle Bytes有一個庫支持對內(nèi)存映射文件的線程安全訪問。
讀取內(nèi)存映射文件中的值并嘗試在循環(huán)中更新。CAS 或compare-and-swap操作是原子的,并檢查先前的值沒有被另一個線程更改。當然,這是在同一臺服務(wù)上的一個線程上操作。
存儲一個納秒的時間戳
我們使用原始的 long 來存儲時間戳可以提高效率,但這可更難使用,我們支持print和解析稱為。
NanoTimestampLongConverter的長時間戳,我們也將這些時間戳解析并隱式呈現(xiàn)為文本使其更容易打印、調(diào)試和創(chuàng)建單元測試。
public class Event extends SelfDescribingMarshallable {
(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
}
由于納秒時間戳是一種高分辨率格式,它只會持續(xù)到 2262 年作為有符號長整數(shù)或 2554 年,值溢出之前,可以假設(shè)它是無符號長整數(shù)。
我們已經(jīng)將時間戳中的額外位置用于其他目的,例如存儲主機標識符或源 ID。出于這個原因,我們還確保時間戳對于 32 ns 的倍數(shù)是唯一的,我們?nèi)绻敢猓梢詫⒌?5 位用于其他目的。
效果
在正常操作下,在服務(wù)器上獲得唯一的納秒時間戳需要不到 50 ns。在繁重的多線程負載下,可能需要幾百納秒。
MappedUniqueTimeProvider 應(yīng)用程序可以維持超過 3000 萬/秒的生成。
可重啟性
只要時間不倒退,這種策略就可以丟失所有狀態(tài),但仍能確保僅從時鐘上的唯一性。如果時鐘時間確實倒退了一個小時,那么狀態(tài)將確保沒有重復(fù),但是,在時鐘趕上之前,時間戳不會與時鐘匹配。
結(jié)論
可以有一個輕量級的、唯一的標識符生成器來保存納秒時間戳。