ULID生成器 - 在线免费创建唯一可排序ID
免费的在线ULID生成器,使用Crockford Base32编码规则,创建长度固定为26个字符、按字典顺序可直接排序的唯一标识符。生成的每个ID都携带毫秒级时间戳信息,可读性优于随机UUID,非常适合用作数据库主键,以及分布式系统和API接口中需要按时间顺序排列的资源标识符。
ULID生成器
文档
什么是 ULID?
ULID(Universally Unique Lexicographically Sortable Identifier,通用唯一的可按字典序排序标识符)是一种 26 个字符的编码,用于标记数据库中的记录或分布式系统中的事件。它把当前时间与随机数据结合起来,因此按字母顺序排序的一组 ULID,实际上也按各自创建的时间排好了序。ULID 生成器就是产生这类编码的工具。
ULID 的结构
ULID 由两部分依次连写而成,中间没有分隔符:
- 时间戳(10 个字符):自 1970年1月1日(Unix 纪元)以来经过的毫秒数,存储为 48 位整数。
- 随机部分(16 个字符):80 位随机数据,其作用是让同一毫秒内创建的两个 ULID 仍然几乎必定不同。
两部分都使用 Crockford Base32 字符集书写:数字 0-9 和字母 A-Z,但去掉字母 I、L、O 和 U。I 和 L 容易与数字 1 混淆,O 容易与数字 0 混淆。去掉 U 是为了避免意外拼出不雅的单词。这样得到的字符串很短,便于朗读,也可以安全地直接放入 URL。
如何生成 ULID
ULID 生成器分三步构建一个标识符。
- 读取当前时间,单位为自 Unix 纪元以来的毫秒数。这是一个 48 位的数。
- 生成 80 位随机数据,通常来自密码学安全的随机数生成器,共 10 个随机字节。
- 分别编码两个部分,使用 Crockford Base32 字符集:48 位的时间戳变成 10 个字符,80 位的随机值变成 16 个字符。把两个编码后的字符串连接起来,就得到最终 26 个字符的 ULID。
编码的方式是每次读取 5 位,因为每个 Base32 字符代表 32 个可能值之一(2^5 = 32)。十个字符最多可容纳 50 位,足以放下 48 位的时间戳。十六个字符恰好容纳 80 位,与随机部分完全吻合,没有剩余。
计算示例
假设 ULID 生成器在时间戳 1712345678901(自纪元以来的毫秒数)运行,并取得随机字节 [12, 240, 88, 3, 199, 45, 6, 231, 128, 17]。
步骤 1:编码时间戳。 将 1712345678901 反复除以 32,每次取余数,再从最后一个余数到第一个依次读出各位数字(这是标准的进制转换)。得到 10 个字符的字符串 01HTQW311N。
步骤 2:编码随机部分。 将 10 个随机字节拼成 80 位的数据流,然后每次读取 5 位,把每个 5 位的分组转换为一个 Base32 字符。得到 16 个字符的字符串 1KR5G0Y75M3EF00H。
步骤 3:连接两部分。 最终的 ULID 为:
101HTQW311N 1KR5G0Y75M3EF00H
2去掉空格后写作:01HTQW311N1KR5G0Y75M3EF00H,共 26 个字符。
ULID 与 UUID 的比较
| 特性 | ULID | UUID(版本 4) |
|---|---|---|
| 长度 | 26 个字符 | 36 个字符(含连字符) |
| 可按创建时间排序 | 是 | 否 |
| 编码 | Crockford Base32 | 十六进制 |
| 包含时间戳 | 是(48 位) | 否 |
| 无需转义即可安全用于 URL | 是 | 是 |
两种格式的目标都是生成在各个系统之间唯一的标识符,而不需要由中央机构来分配编号。实际使用中的主要差别在于排序:由于 ULID 以时间戳开头,把 ULID 当作普通文本排序,同时也就按时间排好了序。随机 UUID 没有这个特性,因为它的每一部分都是随机的。
常见用途
- 数据库主键:ULID 让数据库大致按插入顺序为新行建立索引,许多数据库引擎处理这种情况比处理完全随机的键更高效。
- 分布式系统:各台服务器可以各自独立生成 ULID,无需与中央计数器协调,得到的结果仍然几乎必定是唯一的。
- 事件日志:由于 ULID 按时间排序,一组事件 ID 也可以当作一条粗略的时间线。
- API 和文件标识符:固定为 26 个字符、可安全用于 URL 的格式,在网址和文件名中都很好用。
相关的标识符格式包括 KSUID 和 Snowflake ID。KSUID 同样编码了用于排序的时间戳;Snowflake ID 由 Twitter 等公司使用,它把时间戳、机器标识符和计数器组合在一起。
代码示例
下面的每个示例都按 ULID 规范描述的方式构建 ULID:将 48 位的时间戳编码为 10 个 Crockford Base32 字符,再把 80 位的随机数据编码为另外 16 个字符,然后将二者连接起来。
JavaScript
1const ENCODING_CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
2
3function encodeULID(time, randomBytes) {
4 // Timestamp: 48 bits -> 10 characters
5 let timestampStr = '';
6 let t = time;
7 for (let i = 0; i < 10; i++) {
8 timestampStr = ENCODING_CHARS[t % 32] + timestampStr;
9 t = Math.floor(t / 32);
10 }
11
12 // Randomness: 80 bits -> 16 characters
13 let result = timestampStr;
14 let bits = 0;
15 let bitCount = 0;
16 for (let i = 0; i < 10; i++) {
17 bits = (bits << 8) | randomBytes[i];
18 bitCount += 8;
19 while (bitCount >= 5) {
20 bitCount -= 5;
21 result += ENCODING_CHARS[(bits >> bitCount) & 0x1f];
22 }
23 }
24 return result;
25}
26
27function generateULID() {
28 const randomBytes = crypto.getRandomValues(new Uint8Array(10));
29 return encodeULID(Date.now(), randomBytes);
30}
31
32console.log(generateULID());
33Python
1import os
2import time
3
4ENCODING_CHARS = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
5
6def encode_ulid(time_ms, random_bytes):
7 # Timestamp: 48 bits -> 10 characters
8 chars = []
9 t = time_ms
10 for _ in range(10):
11 chars.append(ENCODING_CHARS[t % 32])
12 t //= 32
13 timestamp_part = "".join(reversed(chars))
14
15 # Randomness: 80 bits -> 16 characters
16 bits = 0
17 bit_count = 0
18 random_part = []
19 for byte in random_bytes:
20 bits = (bits << 8) | byte
21 bit_count += 8
22 while bit_count >= 5:
23 bit_count -= 5
24 random_part.append(ENCODING_CHARS[(bits >> bit_count) & 0x1F])
25
26 return timestamp_part + "".join(random_part)
27
28def generate_ulid():
29 return encode_ulid(int(time.time() * 1000), os.urandom(10))
30
31print(generate_ulid())
32Java
1import java.security.SecureRandom;
2import java.time.Instant;
3
4public class ULIDGenerator {
5 private static final SecureRandom random = new SecureRandom();
6 private static final char[] ENCODING_CHARS =
7 "0123456789ABCDEFGHJKMNPQRSTVWXYZ".toCharArray();
8
9 public static String generateULID() {
10 long timestamp = Instant.now().toEpochMilli();
11 byte[] randomBytes = new byte[10];
12 random.nextBytes(randomBytes);
13
14 // Timestamp: 48 bits -> 10 characters
15 char[] timestampChars = new char[10];
16 long t = timestamp;
17 for (int i = 9; i >= 0; i--) {
18 timestampChars[i] = ENCODING_CHARS[(int) (t % 32)];
19 t /= 32;
20 }
21
22 // Randomness: 80 bits -> 16 characters
23 StringBuilder result = new StringBuilder(new String(timestampChars));
24 long bits = 0;
25 int bitCount = 0;
26 for (byte b : randomBytes) {
27 bits = (bits << 8) | (b & 0xFF);
28 bitCount += 8;
29 while (bitCount >= 5) {
30 bitCount -= 5;
31 result.append(ENCODING_CHARS[(int) ((bits >> bitCount) & 0x1F)]);
32 }
33 }
34 return result.toString();
35 }
36
37 public static void main(String[] args) {
38 System.out.println(generateULID());
39 }
40}
41常见问题
ULID 有什么用途? ULID 用作数据库行、API 资源或日志事件的唯一标识符,适用于同时需要按创建时间排序的场合。
ULID 有多长? ULID 的长度始终为 26 个字符:时间戳占 10 个字符,随机部分占 16 个字符。
ULID 使用什么编码? ULID 使用 Crockford Base32 字符集,由数字 0-9 和字母 A-Z 组成,其中去掉了 I、L、O 和 U,以避免误读和意外拼出单词。
ULID 与 UUID 有什么不同? ULID 按创建时间排序,长度为 26 个字符。随机(版本 4)UUID 不按时间排序,包括连字符在内长度为 36 个字符。
两个 ULID 发生冲突的可能性有多大? 80 位随机数据使冲突极不可能发生。要使任意两个 ULID 相同的概率达到 50%,需要在同一毫秒内生成大约 1.3 万亿个 ULID。
没有网络连接时可以生成 ULID 吗? 可以。ULID 只需要本地时钟和随机数生成器,因此可以完全离线创建。
参考文献
- ULID 项目。Universally Unique Lexicographically Sortable Identifier。ULID 规范 — 48 位毫秒时间戳,80 位随机数据,10 + 16 个字符的标准形式,Crockford Base32 字符集。
- Douglas Crockford。Base 32。Crockford 的 Base32 — 这个包含 32 个符号的字符集,以及为什么排除 I、L、O 和 U。