文章目录
-
- 先说结论
- 64 位详解
- ID 生成流程
- 时钟回拨:最大的坑
- 机器 ID 怎么分配
- 回答技巧与点评
-
- 加分回答
- 面试官点评
个人网站
前面概要讲了分布式 ID 方案,这道题深入雪花算法的实现细节。面试官问这题,他想听的是:你能不能讲清 64 位的分配、ID 的生成流程、时钟回拨的处理?
先说结论
| 结构 | 1位符号 + 41位时间戳 + 5位数据中心 + 5位机器ID + 12位序列号 |
| 时间戳 | 毫秒级,可用约 69 年 |
| 数据中心 | 0-31,最多 32 个机房 |
| 机器ID | 0-31,每机房最多 32 台机器 |
| 序列号 | 0-4095,每毫秒最多 4096 个 ID |
| 总容量 | 每毫秒 4096 × 1024 台机器 = 419 万 ID/毫秒 |
|一句话记住:雪花ID像"身份证号"——前6位是地区(数据中心+机器),中间是出生日期(时间戳),后4位是同日序号(序列号)"
64 位详解
┌─────────────────────────────────────────────────────────────────┐
| 0 | 00000000 00000000 00000000 00000000 00000000 0 | 00000 | 00000 | 000000000000 |
└─────────────────────────────────────────────────────────────────┘
1bit 41bit 5bit 5bit 12bit
符号位 时间戳(毫秒) 数据中心 机器ID 序列号
永远为0 (当前时间-纪元) (0-31) (0-31) (0-4095)
41 位时间戳能撑多久?
2^41 – 1 = 2,199,023,255,551 毫秒
≈ 2,199,023,255 秒
≈ 69.73 年
纪元通常设为 2020-01-01 00:00:00
→ 可用到约 2089 年 👈
10 位机器 ID 能支持多少台机器?
5 位数据中心 × 5 位机器 = 32 × 32 = 1,024 台机器
12 位序列号每毫秒多少个 ID?
2^12 = 4,096 个 ID/毫秒
≈ 409 万 ID/秒 👈 完全够用
ID 生成流程
public class SnowflakeIdGenerator {
private final long epoch = 1609459200000L; // 2021-01-01 00:00:00
private final long datacenterId; // 数据中心ID (0-31)
private final long workerId; // 机器ID (0-31)
private long sequence = 0; // 序列号
private long lastTimestamp = –1; // 上次时间戳
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
// 1. 时钟回拨检测 👈
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨!");
}
// 2. 同一毫秒内 → 序列号递增
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & 0xFFF; // 12位掩码,最大4095
if (sequence == 0) {
// 序列号溢出 → 等下一毫秒
timestamp = waitNextMillis(lastTimestamp);
}
} else {
// 3. 新毫秒 → 序列号归零
sequence = 0;
}
lastTimestamp = timestamp;
// 4. 拼装 ID 👈
return ((timestamp – epoch) << 22) // 时间戳部分(左移22位)
| (datacenterId << 17) // 数据中心(左移17位)
| (workerId << 12) // 机器ID(左移12位)
| sequence; // 序列号
}
}
移位规则:时间戳在最前面(高位),保证 ID 趋势递增——同一毫秒内先按数据中心、再按机器、最后按序列号排序。
时钟回拨:最大的坑
机器时间被 NTP 校准回退,或者运维手动改时间:
正常:t=1000 → 生成ID → t=1001 → 生成ID
回拨:t=1000 → 生成ID → t=999 → ??? 👈 时间倒退了!
后果:
1. 可能生成和之前重复的ID
2. lastTimestamp 逻辑混乱
解决方案:
| 直接报错 | 检测回拨抛异常 | 简单但影响可用性 |
| 等待追平 | 小幅回拨等几毫秒 | 适合偶发小幅回拨 👈 |
| 沿用上次时间 | 回拨时用 lastTimestamp | 不报错但序列号可能溢出 |
| 预生成缓冲 | Leaf 双 Buffer 预生成 | 不依赖实时时钟 |
// 美团 Leaf 的做法
if (timestamp < lastTimestamp) {
long offset = lastTimestamp – timestamp;
if (offset <= 5) {
// 小幅回拨,等一等 👈
Thread.sleep(offset << 1);
timestamp = System.currentTimeMillis();
} else {
// 大幅回拨,报错
throw new ClockBackwardsException();
}
}
机器 ID 怎么分配
多台机器必须保证 workerId 不同:
| 配置文件 | 每台机器写死 | 扩展麻烦 |
| ZK 分配 | 启动时从 ZK 获取 | 依赖 ZK |
| 数据库分配 | 启动时从 DB 获取 | 依赖数据库 |
| IP 取模 | IP hash % 32 | 可能冲突 |
| K8s Pod名哈希 | Pod 名称 hash % 32 | 容器环境友好 👈 |
雪花算法全景
64位分配
├── 1bit 符号位(永远0)
├── 41bit 时间戳(69年)
├── 5bit 数据中心(32个)
├── 5bit 机器ID(32台/数据中心)
└── 12bit 序列号(4096个/毫秒)
生成流程
├── 检测时钟回拨
├── 同毫秒序列号递增
├── 溢出则等下一毫秒
└── 移位拼装ID
核心风险
├── 时钟回拨 → 等待/报错/预生成
└── 机器ID冲突 → ZK/DB/Hash分配
口诀:一位符号四十一时间,十个比特分机房;
十二序列每毫秒,四千零九十六个号;
时钟回拨要防范,小幅等待大幅报;
机器ID要唯一,ZK分配最可靠
回答技巧与点评
标准回答:雪花算法生成 64 位 ID:1 位符号(0)+ 41 位时间戳(毫秒,约 69 年)+ 5 位数据中心(32 个)+ 5 位机器 ID(32 台)+ 12 位序列号(4096 个/毫秒)。生成时先检测时钟回拨,同一毫秒内序列号递增,溢出则等下一毫秒,最后移位拼装。最大风险是时钟回拨,小幅回拨等几毫秒追平,大幅回拨则报错。
加分回答
面试官点评
这道题考的是你对 分布式 ID 生成 的实现细节。最忌讳的回答是只背 64 位分配——面试官想听的是 生成流程(同毫秒序列号递增、溢出处理)和 时钟回拨的解决。能写出生成代码的思路,再说出 Leaf 的优化,就是高分回答。
原文阅读
内容有帮助?点赞、收藏、关注三连!评论区等你 💪