分布式 ID 与防重:雪花算法实战#
订单号要有序、消息 ID 要全局唯一、防重要有幂等键——分布式系统的第一行代码往往是 ID 生成。雪花算法(Snowflake)是应用最广的方案,这篇文章讲透它的原理和落地坑。
一、为什么需要分布式 ID#
二、雪花算法原理#
三、Go 实现#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
type Snowflake struct {
mu sync.Mutex
lastTs int64
machineId int64 // 机器位(10 bit)
sequence int64 // 序列号(12 bit)
}
func (s *Snowflake) NextID() int64 {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now().UnixMilli()
if now == s.lastTs {
s.sequence = (s.sequence + 1) & 0xFFF // 4096 上限
if s.sequence == 0 { // 用完了,等下一毫秒
for now <= s.lastTs {
now = time.Now().UnixMilli()
}
}
} else {
s.sequence = 0
}
s.lastTs = now
return (now << 22) | (s.machineId << 12) | s.sequence
}
|
四、核心坑:时钟回拨#
最经典的坑:NTP 校时导致系统时钟回拨,now < lastTs → 生成的 ID 小于已生成的 → ID 重复或乱序。
方案对比#
| 方案 |
做法 |
适用 |
| 抛异常 |
回拨超过阈值直接报错 |
强一致场景 |
| 等待 |
回拨小(< 5ms)等待追上 |
大多数场景 |
| 备用位 |
记录 lastTs,回拨用"回拨期序列号" |
复杂,少用 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// 实践:小回拨等待,大回拨报警+降级
func (s *Snowflake) waitClock(now int64) int64 {
if now < s.lastTs {
diff := s.lastTs - now
if diff > 500 { // 回拨 > 500ms:报警 + 用备用序列
log.Error("时钟回拨过大", diff)
s.sequence = (s.sequence + 1) & 0xFFF // 保底防重
return s.lastTs
}
time.Sleep(time.Duration(diff) * time.Millisecond)
now = time.Now().UnixMilli()
}
return now
}
|
五、机器位分配#
我的实践:启动时从配置中心(Nacos)注册拿 machineId,重复自动重试——避免手配导致两台机器同 ID。
六、方案对比#
| 方案 |
性能 |
有序 |
依赖 |
适用 |
| 雪花 |
40万/s |
趋势递增 |
时钟 |
绝大多数场景 |
| DB 号段 |
万级 |
递增 |
DB |
订单号(严格递增需求) |
| Redis INCR |
万级 |
递增 |
Redis |
简单场景 |
| UUID |
快 |
无序 |
无 |
不追求有序 |
| 美团 Leaf |
雪花增强 |
递增 |
ZK/DB |
大厂规模化 |
选型建议:默认雪花;要"严格递增且可回看"(订单)用 DB 号段;大数据量分布式部署用 Leaf 类增强方案。
七、防重的组合拳#
ID 生成是防重的地基:
八、踩坑记录#
- 时钟回拨没处理:NTP 校时 → 重复 ID → 订单串号 → 等待/兜底必做
- 机器位配错:两台机器同 machineId → 同毫秒序列冲突 → 注册中心分配
- ID 反解滥用:从 ID 能看出时间戳 → 不要暴露业务敏感信息在 ID 里(可混淆)
- 性能误判:单机 40 万/s 够用,别为"更高性能"过度设计(除非真有单机百万级)
分布式 ID 的核心认知:
- 雪花算法:时间戳 + 机器位 + 序列号,40 万/s,趋势递增
- 时钟回拨是头号坑:小回拨等待、大回拨报警兜底
- 机器位要自动分配:别手配,避免冲突
- ID 是防重的地基:幂等键要确定性生成,业务语义组合
ID 生成看似小事,崩起来是串号、错单级别的灾难。 把雪花算法的坑都踩平了,分布式系统的第一行代码才稳。
微信公众号「福清而不淡」
后端架构 · AI 工程 · 云原生的一线实践,扫码关注,不错过更新。
本文已同步发布到公众号,欢迎留言交流。