跳到主要内容

实现分布式 ID 生成器

问题

如何用 Go 实现分布式环境下全局唯一的 ID 生成器?

答案

方案对比

方案有序性性能依赖长度
UUID v4无序36 字符
雪花算法趋势递增极高64 bit
Redis INCR绝对递增Redis可控
数据库号段递增数据库可控

雪花算法(Snowflake)

  1 bit     41 bit          10 bit      12 bit
┌───────┬───────────────┬───────────┬────────────┐
│ 符号位 │ 时间戳(毫秒) │ 机器 ID │ 序列号 │
│ 0 │ │ │ │
└───────┴───────────────┴───────────┴────────────┘
  • 41 bit 时间戳:约 69 年
  • 10 bit 机器 ID:1024 个节点
  • 12 bit 序列号:每毫秒 4096 个 ID
type Snowflake struct {
mu sync.Mutex
epoch int64 // 自定义起始时间戳(毫秒)
nodeID int64 // 机器 ID (0~1023)
sequence int64 // 毫秒内序列号
lastStamp int64 // 上次生成 ID 的时间戳
}

const (
nodeBits = 10
sequenceBits = 12
nodeMax = -1 ^ (-1 << nodeBits) // 1023
sequenceMax = -1 ^ (-1 << sequenceBits) // 4095
nodeShift = sequenceBits // 12
timestampShift = nodeBits + sequenceBits // 22
)

func NewSnowflake(nodeID int64) (*Snowflake, error) {
if nodeID < 0 || nodeID > nodeMax {
return nil, fmt.Errorf("nodeID 必须在 0~%d 之间", nodeMax)
}
return &Snowflake{
epoch: 1704067200000, // 2024-01-01 00:00:00 UTC
nodeID: nodeID,
}, nil
}

func (s *Snowflake) Generate() int64 {
s.mu.Lock()
defer s.mu.Unlock()

now := time.Now().UnixMilli()

if now == s.lastStamp {
// 同一毫秒内,序列号递增
s.sequence = (s.sequence + 1) & sequenceMax
if s.sequence == 0 {
// 序列号用完,等到下一毫秒
for now <= s.lastStamp {
now = time.Now().UnixMilli()
}
}
} else {
s.sequence = 0
}

s.lastStamp = now

id := (now-s.epoch)<<timestampShift |
s.nodeID<<nodeShift |
s.sequence

return id
}

// 解析 ID 中的信息
func (s *Snowflake) Parse(id int64) (timestamp time.Time, nodeID, sequence int64) {
ts := (id >> timestampShift) + s.epoch
nodeID = (id >> nodeShift) & nodeMax
sequence = id & sequenceMax
return time.UnixMilli(ts), nodeID, sequence
}

UUID 方案

import "github.com/google/uuid"

func GenerateUUID() string {
return uuid.NewString() // v4: 完全随机
}

// UUID v7: 时间有序(Go 1.22+)
func GenerateUUIDv7() string {
id, _ := uuid.NewV7()
return id.String()
}

Redis 号段模式

type SegmentIDGen struct {
rdb *redis.Client
key string
step int64
current int64
max int64
mu sync.Mutex
}

func (g *SegmentIDGen) NextID() (int64, error) {
g.mu.Lock()
defer g.mu.Unlock()

if g.current >= g.max {
// 号段用完,从 Redis 获取新号段
newMax, err := g.rdb.IncrBy(context.Background(), g.key, g.step).Result()
if err != nil {
return 0, err
}
g.current = newMax - g.step
g.max = newMax
}

g.current++
return g.current, nil
}

选型建议

提示
  • 通用场景 → 雪花算法(趋势递增,高性能)
  • 不关心顺序 → UUID v4(最简单)
  • 需要严格递增 → Redis INCR / 数据库自增
  • 结合时间排序 → UUID v7(新标准,推荐)

常见面试问题

Q1: 雪花算法的时钟回拨怎么处理?

答案

  • 短暂回拨(几毫秒):等待时钟追上
  • 较大回拨:拒绝生成 ID 并告警
  • 美团 Leaf 方案:用最后一次时间戳做兜底

Q2: 雪花算法的机器 ID 怎么分配?

答案

  • 手动配置(小规模)
  • 从 etcd/ZooKeeper 租约分配
  • 从 K8s Pod 名称/IP 计算

相关链接