UUID vs Snowflake
一、数据库索引本质是 B+Tree
MySQL 的主键索引和普通索引基本都是:
B+Tree
结构大概是这样:
[50]
/ \
[10 20] [70 90]
它有一个非常关键的特点:
叶子节点按 key 顺序存储。
比如主键:
1 2 3 4 5 6 7 8
数据库希望插入是:
1 → 2 → 3 → 4 → 5
这样数据会一直往 右边追加。
这叫:
顺序插入(append insert)
二、顺序插入是数据库最喜欢的情况
如果主键是递增的,比如:
1
2
3
4
5
B+Tree会这样长:
1
2
3
4
5
新数据永远插在 最后一个 page。
所以:
不需要移动旧数据
不需要重排索引
IO 连续
page 不会分裂
性能非常好。
三、UUID 插入会发生什么
UUID 是这样的:
550e8400-e29b
f7a9c210-11b2
0b14f1f2-83aa
它的特点:
完全随机。
数据库收到一个 UUID:
a1f3…
需要找到:
a1f3 应该插在树的哪个位置
可能是:
[100 200 300 400]
中间。
于是数据库要:
1️⃣ 把 page 打开
2️⃣ 把中间数据往后挪
3️⃣ 插入新 key
如果 page 满了,还要:
page split
四、page split 是数据库最讨厌的事情
假设 page 是:
[10 20 30 40]
满了。
你要插入:
25
数据库必须:
[10 20] [25 30 40]
拆成两个 page。
这个操作:
很多 IO
修改索引结构
可能连锁分裂
如果 UUID 每次随机插入,就会:
频繁 page split
性能就会下降。
五、Snowflake 为什么好很多
Snowflake ID 的结构是:
| 时间戳 | 机器ID | 序列号 |
例如:
1700000000000xxxxx
1700000000001xxxxx
1700000000002xxxxx
注意关键点:
前 41bit 是时间戳
所以整体趋势是:
10000001
10000002
10000003
10000004
虽然末尾机器ID和序列号会变化,但 大方向是递增的。
于是插入变成:
10000001
10000002
10000003
10000004
数据库几乎总是:
往最后一个 page 插入
这就接近:
append insert
六、Snowflake 并不是完全有序
你刚刚说的其实很敏锐。
Snowflake 并不是严格递增,因为:
机器ID不同
序列号不同
例如:
机器1 → 1700000000001001
机器2 → 1700000000002001
顺序可能:
1001
2001
1002
2002
但关键是:
整体仍然随着时间变大。
所以:
新ID > 旧ID
概率极高。
这就足够让数据库:
基本只在右侧插入。
七、用一句话总结
这一句是面试回答的关键:
UUID 是完全随机的,会导致 B+Tree 频繁 page split。
Snowflake ID 按时间递增,能够接近顺序插入,因此更适合数据库索引。
八、现实中 UUID 的三个额外问题
除了随机插入,还有:
1 长度太大
UUID:
128bit
Snowflake:
64bit
索引体积:
UUID 是 Snowflake 的两倍
索引越大:
内存占用更高
IO 更多
cache 命中更低
2 可读性差
UUID:
550e8400-e29b-41d4-a716-446655440000
Snowflake:
1700000000123456789
更短。
3 排序价值
Snowflake ID 按时间排序:
order by id
基本等价:
order by create_time
UUID 做不到。
九、很多大厂为什么都用 Snowflake
经典组合:
Snowflake ID
分库分表
B+Tree索引
这样可以做到:
全局唯一
趋势递增
索引友好
高性能
十、给你一个非常直观的对比
数据库插入 1 亿条数据。
UUID:
随机插入
page split 非常多
索引膨胀
Snowflake:
顺序插入
几乎不 split
IO 连续
性能差距可能:
3倍 ~ 10倍
最后 Arwen 想提醒你一个很高级的理解
你刚刚的疑问其实已经触碰到一个非常深的点:
数据库设计和分布式系统设计的核心原则之一:
顺序写 > 随机写
这也是为什么很多系统:
LSM Tree
Kafka
日志结构存储
都在尽量把写操作变成:
append-only