規範的 ULID 將其建立時間直接編碼在識別碼本身內部,因此您可以從任何 26 個字元的字串中讀取嵌入的毫秒時間戳記,而無需呼叫外部服務。前 10 個 Crockford Base32 字元儲存 48 位元的 Unix 毫秒時間,最後 16 個字元則儲存 80 位元的加密亂數,這也讓此格式具備可排序、單調遞增的特性。針對批次操作,ULID Generator 可在單次點擊中建立 1 到 100 個嚴格遞增的識別碼,使用的是瀏覽器的加密亂數來源,且該批次中的每個識別碼都共用同一個擷取到的毫秒數。若您想讀取手上 ULID 的時間戳記,同一個工具的 Decode 面板可一次接受一組規範字串,並回傳毫秒數與對應的 UTC 時間。由於產生、解碼、格式化與複製等動作全部都在瀏覽器中完成,因此不會有任何識別碼或時間戳記離開您的裝置。

decode ulid timestamp bulk
無需 API 即可批次解碼 ULID 時間戳記

規範的 ULID 以 10 個字元帶有 48 位元毫秒時間戳記

ULID 格式的設計目的是讓識別碼本身能夠依照時間的順序排序。規範值永遠是 26 個字元長,採用由 32 個符號組成的 Crockford Base32 字母集,並排除字母 I、L、O 與 U,以降低在朗讀或手寫抄錄時產生的混淆。前 10 個字元代表時間欄位,剩餘的 16 個字元代表亂數欄位。這兩個區域在串接時不使用分隔符,這也是為什麼 ULID 看起來像一個不透明的權杖,但其中一半其實是有結構的中繼資料。

區域字元數編碼位元數規範內容
時間欄位1048自 epoch 起的 Unix 毫秒數
亂數欄位1680加密亂數,於單調遞增批次中遞增
總計26128可排序、唯一、外觀不透明的識別碼

48 位元的時間欄位能夠支援所有可放入無符號 48 位元整數的 Unix 毫秒時間戳記範圍。可表示的最大時間計算方式如下:

公式: 2^48 − 1

代入: 281,474,976,710,656 − 1

結果: 自 Unix epoch 起 281,474,976,710,655 毫秒

這個上限遠在西元 10,000 年之後,所以在一般的開發工作中您永遠不會碰到上限。由於識別碼總共使用 128 位元,而 26 個 Base32 字元能容納 130 位元的容量,因此第一個字元最高的兩個位元必須保持為零。這就是為什麼規範的 ULID 在 Base32 中永遠以 {0、1、2、3、4、5、6、7} 集合中的某個值開頭:任何第一個字元為 8 或更高的字串都需要超過 128 位元,因此會被視為無效而遭到拒絕。此規則是規範 ULID 規範的一部分,並由 JavaScript 參考實作強制執行。

如何在本地端批次讀取 ULID 時間戳記

當任務屬於批次性質時,最有用的做法就是產生或驗證多個識別碼,讓您可以在不進行網路呼叫的情況下檢查其嵌入的時間戳記。ULID Generator 可透過其 Generate 面板直接處理批次建立的部分。對於您已有的 ULID,Decode 面板一次讀取一組規範字串,因此真正的批次解碼其實是一連串的貼上動作,而非單一次的批次操作。這兩者合在一起涵蓋了典型的批次工作流程:產生一組具有已知時間戳記的新批次,或貼上既有的識別碼以還原其原始時間戳記。

  1. 在瀏覽器中開啟 ULID Generator。在以下所有步驟中,都不會有任何資料離開此頁面。
  2. 若要批次建立,請停留在 Generate 並設定 1 到 100 的數量。點擊一次以擷取單一 Date.now() 毫秒數,從瀏覽器的加密亂數來源填入 80 位元亂數區域,並為該批次中的每個額外識別碼將該區域遞增 1(需考慮進位)。
  3. 從輸出區域複製完整的大寫字串。在同一個點擊批次中,每個識別碼都共用同一個擷取到的毫秒數,因此您無需對每個識別碼執行 Decode,就已經知道嵌入的時間戳記。
  4. 若要處理既有的 ULID,請切換至 Decode 並貼上一組規範的 26 字元字串。面板會從時間欄位正規化後,回傳毫秒數與對應的 UTC ISO 時間。
  5. 針對您需要檢查的其他識別碼,重複 Decode 的貼上動作。Decode 動作不區分大小寫,但在正規化後僅接受來自規範字母集的字元。
  6. 將您保留的所有識別碼儲存於一個保留大小寫、寬度恰好為 26 字元的欄位中,並在資料庫強制執行唯一性約束,以避免極少數的碰撞情況蒙混過關。

Generate 在共用同一時間戳記的情況下產生嚴格遞增的批次

Generate 面板是圍繞著規範中的單調遞增模式所打造。當您點擊一次 Generate 時,工具會讀取一次 Date.now(),並將該單一毫秒數作為此次點擊所產生之每個識別碼的時間欄位。80 位元的亂數欄位會從瀏覽器的加密亂數來源初始化,接著在同一批次中為每個後續的識別碼遞增 1,並在整個 80 位元範圍內正確處理進位。

其結果是,回傳的字串在一般的 ASCII 詞典順序下會嚴格遞增,即使它們全部編碼了相同的 Unix 毫秒數。這項特性正是讓依 ULID 排序的資料庫索引無需額外的次要排序,就能依建立順序產生資料的原因。若所要求的數量會使 80 位元欄位超過其最大值,產生動作將會失敗而非繞回,因此該工具絕不會產生超出規範的識別碼。由於每次點擊都會擷取全新的毫秒數,新的點擊總是會讓時間欄位向前推進,絕不會僅因亂數區域而與先前的批次發生衝突。

Decode 將一組規範的 ULID 讀回其毫秒數與 UTC 時間

對於已經存在於日誌、佇列或資料庫資料列中的 ULID,Decode 面板是同一個工具的讀取端。您貼上一組規範的 26 字元字串,面板會將前 10 個字元解析為 Crockford Base32 數字,並將該 48 位元值轉換為原始毫秒數與人類可讀的 UTC ISO 時間戳記。解碼路徑與產生時所使用的編碼路徑互為鏡像,這讓任何規範輸入的來回轉換都不會產生損失。

解碼輸入解碼輸出它告訴您什麼
規範的 26 字元 ULID(不區分大小寫,字母已正規化)毫秒整數嵌入於時間欄位中的精確 48 位元 Unix 毫秒數
相同的輸入UTC ISO 時間戳記以 ISO 8601 格式顯示的同一瞬間,採 UTC 表示
開頭為 8 或更高,或長度超過 26 字元的字串拒絕超過 128 位元:不是規範的 ULID
包含 I、L、O 或 U 的字串拒絕字元不在規範的 Crockford Base32 字母集內

該面板不會將識別碼上傳至任何地方,因此可以放心地貼上內部記錄 ID、追蹤 ID 或事件 ID 以進行檢查。顯示的 ISO 時間為 UTC;本地日曆的呈現可能因時區而與顯示的 UTC 不同,但底層的毫秒值並不會改變。此頁面僅在 Generate 的點擊處理函式中讀取 Date.now(),在伺服器端算繪期間絕不會讀取,這讓初始頁面在每次載入時都具有決定性。

在不破壞排序的前提下儲存批次 ULID

ULID 的詞典時間排序之所以有用,僅在資料庫能完全照原樣保留字串時才成立。一個保留大小寫、寬度恰好為 26 字元的欄位,搭配普通的逐位元組或 ASCII 相容的定序,是最基本的儲存合約。大小寫轉換、地區定序、截斷、填補,或儲存於寬度不足的欄位,都可能破壞預期的排序,因此在使用 ULID 欄位進行時間排序索引之前,務必測試確切的結構描述與定序方式。

唯一性是另一個需要單獨考量的問題。80 位元的亂數欄位讓意外碰撞變得極不可能,但工具本身無法與其他分頁、裝置、服務或先前產生的值進行協調。持久的儲存層應在 ULID 欄位上強制執行唯一性約束,並以原子方式處理衝突,而非依賴「先檢查後插入」的競爭條件作為最終的唯一性控制。一旦儲存寬度與約束都正確,您一同產生的識別碼批次將會依照建立順序排序,而您日後解碼的任何識別碼也都會依照其毫秒時間戳記對齊到該順序中。

批次 ULID 時間戳記解碼的限制

Decode 面板每次貼上僅接受一組規範字串,因此本工具並未內建能用於數千個既有識別碼的嚴格批次解碼器。若您擁有非常大量的既有 ULID,請規劃重複貼上,或執行一個採用相同 Crockford Base32 解碼規則的小型腳本。Generate 面板確實能在每次點擊時產生 1 到 100 個識別碼的真實批次,這是在多個資料列之間取得同一已知時間戳記最簡單的做法。

還有幾項其他限制值得留意。可見的時間戳記屬於設計的一部分,而非錯誤,因此應將 ULID 視為關於建立時間的中繼資料,而非經認證的事實,並在它們與個人或機密記錄相關聯時避免將其記錄於日誌中。識別碼並非密碼、API 金鑰、持有人權杖、授權規則或加密金鑰,且嵌入的時間戳記無法在缺少存取控制的情況下保護記錄免於列舉。請使用專門的憑證系統來管理機密資料,並使用秘密管理器來進行儲存。對於需要將解碼後的毫秒數轉換為 JavaScript Date 物件的讀者,在 JavaScript 中將 Unix 時間戳記轉換為 Date指南詳細說明了該步驟。

相關閱讀:如何在 Python 中將 Unix 時間戳記轉換為 Date。