生成ID
1)为什么要注意标识符
ID (ID)是实体的基本密钥:DB行、消息、文件、顺序。以下内容取决于其属性:- 唯一性和规模(冲突,水平增长)。
- 顺序和排序(时间相关性,复制,去除)。
- 存储性能(索引、热页、密钥大小)。
- 安全(不可预测,泄漏,猜测)。
- 可用/集成(短的URL安全,对大小写不敏感)。
ID选择是熵、有序性、长度、生成速度和操作之间的权衡。
2)关键要求和术语
独特:冲突的可能性必须低于可接受的风险。
熵:"多少随机性"包含ID(位)。
可排序性(time-sortable/k-sortable):词典排序≈时间排序。
单调:节点/流中不可减少的序列。
记录位置:新插入集中于索引的"尾部"(热页危险)的程度。
可预测性:是否可以猜测相邻ID (安全性/API重要)。
表示:二进制/字符串,Base16/32/36/58/64,连字符,寄存器。
3)主要标识符家族
3.1 UUID
v4 (random): 122位熵。无序,有利于安全和简单。减:由于随机分配,索引会"混乱"-但是,这会均匀地分散负载并删除"热页"。
v1 (time+MAC):排序,但携带IAU/时间(隐私);经常避免。
v7(时间顺序):毫秒时间+随机部分。按时间对词典进行排序,并在DB中进行良好的压缩。折衷方案:出现索引的"热尾巴";通过硬化/前缀/增量处理。
提示
对于外部API和非严格顺序要求-v4。
对于事件/逻辑DB和"可排序"密钥为v7。
3.2 ULID (Crockford Base32)
128位:48位时间(ms)+80位随机性。按时间进行词典排序,工人友好(没有"I,L,O,U"),URL安全。有一个单调变化(在相同的时间标记下,随机部分增加)。
优点:可读性,有序性,可移植性。
缺点:在非常高的时间点插入频率是"热尾巴"。
3.3 KSUID
160位:32位时间(秒)相对于时代+128位随机性。更大的时间范围和稳定的排序,字符串比ULID?(不-更长,但有编码),对分布式日志和对象有好处。
3.4类似雪花(k-sortable flake ID)
经典方案(可自定义):
[ timestamp bits ][ region/datacenter bits ][ worker bits ][ sequence bits ]
属性:节点上的单调生长,准全局唯一性,短(64位)二进制表示。
风险:时钟依赖性(漂移/时间倒退),在单个柚木中耗尽序列,协调区域/工作器位。
治疗:对"clock back"的保护,序列备份,时间检测器,PTP/NTP纪律。
3.5个数据库序列(SEQUENCE/IDENTITY)
单个DBMS/Chard中最简单的单调生成。
优点:简短、快速、方便本地表格。
缺点:在分布式集群中很难在全球范围内使用;可以预见(不安全地作为公共钥匙),会产生索引的热尾巴。
3.6内容地址ID (hash content)
内容SHA-256/Blake3 →稳定的ID、重复数据消除、完整性检查、缓存。
优点:确定性,替代保护。
缺点:昂贵的生成(CPU),碰撞实际零,没有时间排序,长度。
4)冲突和"生日悖论"(直觉)
"n"生成下随机ID "b"位发生冲突的概率是近似的:
p ≈ 1 - exp (-n (n-1 )/2/2 ^ b) ≈ n ^ 2/2 ^ (b + 1) (for small p)
示例:
- UUIDv4 (122位)=10^12(万亿)→ p ~ 1e-14(可忽略)。
- 64位兰德→ n=10^9已经是p ~ 0。027(明显风险)。
- 结论:对于大型系统,64位随机系统通常很少。使用96/128位。
5)索引、热页和存储
随机密钥(v4)在索引树上均匀分布插入物→没有"尾巴",但比缓存局部性差。
按时间排序(v7/ULID/Snowflake)将"插入尾巴"→更好的位置和压缩,但在高并行记录下存在热页的风险。
- tenant/region 前缀/sharding(在时间之前添加1-2字节);
- 间歇性:老年人的部分意外;
- Butch插件,B树中的滤镜,BRIN/大型日志聚类上的自动转换。
- "UUID(16B)"vs "BIGINT(8B )"/"INT8"节省了内存/缓存;Base32/58/64行增加了20-60%。对于DB,存储二进制,序列化到边缘的一行。
6)安全和隐私
未使用SEQUENCE/INT作为URL/API中的公共ID:可猜测→资源枚举。
为外部链接添加随机、不可预测的ID (v4/v7/ULID/KSUID)。
不要在ID中编码PII。如果要启用属性-加密/签名(例如JWE/JWS)或使用不透明令牌。
URL安全编码:Base32 Crockford, Base58(没有"0OIl"), Base64url。
7)多重性、前缀和路由
格式:"[TENANT_PREFIX]-[ID]"或二进制:'tenant_id || id'。
优点:快速过滤器/按租户分批,N+1扫描保护。
缺点:可能会降低旧位中的熵密度→考虑分布(前缀哈希)。
Hash后缀(2-3字节)可以减少冲突并帮助共享路由:'shard=hash(id)%N'。
8)实用选择建议
API,公共链接,无严格顺序的分布式服务:UUIDv4,ULID/KSUID。
经常按时间排序的逻辑/事件/订单:UUIDv7或ULID(单调)。
具有本地单调性和短键的超高带宽:类似于Snowflake的64位(需要时间纪律)。
文物库/法案/斑点:内容地址(SHA-256),以及人性化的简短"展示柜"(Hashids/链接)。
单个DB中的本地表:SEQUENCE/IDENTITY+用于公共参考(掩码)的外部"包装"。
9)实现和示例
9.1 PostgreSQL
按需要将UUID二进制存储,索引为"btree"或"hash"。
sql
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE orders (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- или uuid_generate_v4()
created_at timestamptz NOT NULL DEFAULT now(),
tenant smallint NOT NULL
);
-- For time-sortable (UUIDv7) store binary (uuid), generation in the application.
-- If you want a cluster by time:
CREATE INDEX ON orders (created_at DESC);
Sequential hot fix:对于time-sorted ID,在高级位中添加"盐"或tenant派对:
sql
CREATE TABLE orders_t1 PARTITION OF orders FOR VALUES IN (1);
CREATE TABLE orders_t2 PARTITION OF orders FOR VALUES IN (2);
9.2 Redis(原子计数器/单子子)
bash
INCR "seq: orders" # local sequence combine: epoch_ms<<20 (worker_id<<10) (seq & 1023)
9.3个类似雪花的生成器(伪代码)
pseudo const EPOCH = 1704067200000 # custom epoch (ms)
state: last_ms=0, seq=0, worker=7, region=3
next():
now = epoch_ms()
if now < last_ms: wait_until(last_ms) # защита от clock back if now == last_ms:
seq = (seq + 1) & ((1<<12)-1) # 12 бит if seq == 0: wait_next_ms()
else:
seq = 0 last_ms = now return (now-EPOCH)<<22 region<<17 worker<<12 seq
9.4 ULID/UUID在应用中
Go
go
// ULID t:= time. Now(). UTC()
entropy:= ulid. Monotonic(rand. New(rand. NewSource(t. UnixNano())), 0)
id:= ulid. MustNew(ulid. Timestamp(t), entropy)
//UUID v7 (if there is a library)
id:= uuid. Must(uuid. NewV7())
Node.js
js import { ulid } from 'ulid';
import { v4 as uuidv4 } from 'uuid';
const id1 = ulid();
const id2 = uuidv4(); // v4
Python
python import uuid, time id_v4 = uuid. uuid4()
For v7, use a library (for example, uuid6/7 third-party packages)
10)编码和表示
DB中的Binarno("BYTEA","UUID")→紧凑而快速。在边缘,转换为:- Base32 Crockford (ULID):对大小写不敏感,没有视觉上相似的字符。
- Base58:简称Base32/64人为令牌,URL安全。
- Base64url:简而言之,但URL中的"-"和"_"。
稳定大小写和格式(连字符/连字符不存在),以便在比较行时避免重复。
11)测试花花公子和可观察性
冲突:度量'id_collision_total'(必须为0), alert在>0时。
前缀分布:高级字节直方图-寻找占位符。
生成率:"ids_per_sec",p99生成器潜伏期。
Clock skew(用于Snowflake):节点越位,"clock went back"事件。
索引尾巴:p95/p99 "INSERT"后缀;锁定/热页的比例。
Game day:
注入"clock drift/back" →确保生成器在等待/切换。
在毫秒内溢出"序列"→检查等待next_ms。
大规模并发→索引中没有锁定风暴。
12)反模式
AUTO_INCREMENT/SEQUENCE如公共ID:猜测,泄漏。在内部使用公共不透明ID。
UUIDv1(IAU/时间)向外:隐私。
每万亿条记录64 位随机ID:真正的冲突风险。
没有HA的全局"中央发电机":SPOF和瓶颈。
不带时钟保护的Time-sorted ID:重复/反向顺序。
混合不同的ID格式而不使用显式版本/前缀,→混乱/迁移。
将ID保存为具有不同寄存器/形状的字符串→隐藏的副本。
13)实施支票
- 为域要求选择格式(v4/v7/ULID/KSUID/Snowflake/SEQ/hash)。
- 定义了顺序要求(是否需要排序)。
- 评估了碰撞概率(b位,n生成)并设置了风险阈值。
- 设计了编码(在DB+人造展示中二进制)。
- 对于计时的-反时钟保护、序列限制和NTP/PTP纪律。
- 对于公共ID-不可预测性(random/ULID/KSUID),缺少PII。
- 深思熟虑shard routing (hash (id)% N),多影子前缀。
- 可观察性:冲突,分布,延迟,clock skew的度量。
- 排序溢出/高竞争/窗口长度的测试案例。
- 格式、版本、时代、位标记和迁移计划的文档。
14) FAQ
Q: 为微服务选择"默认"是什么?
A:UUIDv7或ULID:时间顺序,大熵和边缘上的简单生成。对于外部API,也是c ULID/UUIDv4。
Q:需要简短而人性化的ID。
A: ULID/KSUID或Base58编码128位随机/时间ID。记住长度和冲突。
问:可以做"短数字"ID,但安全吗?
答:是的:储存内部SEQ,向外赠送蛋白质令牌(random 96-128位)或带有盐+签名的Hashids。
问:如何从SEQ迁移到UUIDv7?
A:键入新列"id_new" (UUID)、二进制、发布指向新ID的链接、然后切换RK/外键并删除旧键。
Q:为什么我的ULID插件变得"热"?
答:在单个索引中插入严格增加的键。按批次/tenant粉碎,搅拌高级位,使用击球插入。
15)结果
良好的ID是任务下的正确属性集:足够的熵、可预测的排序(如果需要)、安全的宣传和健康的索引利用。选择简单和分布的UUIDv4/ULID/UUIDv7/KSUID,Snowflake选择密集的单调和短键(在时间学科中),序列选择本地表,内容哈希选择人工制品。设置可观察性和测试-ID将不再是惊喜的来源。