分布式 ID 与防重:雪花算法实战

订单号要有序、消息 ID 要全局唯一、防重要有幂等键——分布式系统的第一行代码往往是 ID 生成。雪花算法(Snowflake)是应用最广的方案,这篇文章讲透它的原理和落地坑。

一、为什么需要分布式 ID

U U I D I D I D I D / " 便 3 6 "

二、雪花算法原理

6 4 1 b 5 l i 1 o t 0 n 0 g 0 m + 4 s 6 0 9 5 9 × 6 4 4 1 0 9 b 6 i 1 t 0 = 2 4 4 0 9 . 6 1 0 b I i D t 1 2 b i t

三、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
}

五、机器位分配

1 0 / I b P i t = 5 I D + 5 3 2 × 3 2

我的实践:启动时从配置中心(Nacos)注册拿 machineId,重复自动重试——避免手配导致两台机器同 ID

六、方案对比

方案 性能 有序 依赖 适用
雪花 40万/s 趋势递增 时钟 绝大多数场景
DB 号段 万级 递增 DB 订单号(严格递增需求)
Redis INCR 万级 递增 Redis 简单场景
UUID 无序 不追求有序
美团 Leaf 雪花增强 递增 ZK/DB 大厂规模化

选型建议默认雪花;要"严格递增且可回看"(订单)用 DB 号段;大数据量分布式部署用 Leaf 类增强方案。

七、防重的组合拳

ID 生成是防重的地基:

I D R e d i = = s S E T N " X / u D s B " e r I I D d + / I D

八、踩坑记录

  1. 时钟回拨没处理:NTP 校时 → 重复 ID → 订单串号 → 等待/兜底必做
  2. 机器位配错:两台机器同 machineId → 同毫秒序列冲突 → 注册中心分配
  3. ID 反解滥用:从 ID 能看出时间戳 → 不要暴露业务敏感信息在 ID 里(可混淆)
  4. 性能误判:单机 40 万/s 够用,别为"更高性能"过度设计(除非真有单机百万级)

总结

分布式 ID 的核心认知:

  1. 雪花算法:时间戳 + 机器位 + 序列号,40 万/s,趋势递增
  2. 时钟回拨是头号坑:小回拨等待、大回拨报警兜底
  3. 机器位要自动分配:别手配,避免冲突
  4. ID 是防重的地基:幂等键要确定性生成,业务语义组合

ID 生成看似小事,崩起来是串号、错单级别的灾难。 把雪花算法的坑都踩平了,分布式系统的第一行代码才稳。