Snowflake 是一类基于“时间戳 + 节点标识 + 毫秒内序列号”生成 64 位整数 ID 的方案。Twitter 的经典实现使用 41 位时间、10 位节点和 12 位序列号,但位数划分是设计选择,不是所有系统必须遵循的标准。
它适合需要本地高吞吐、整数型、近似按时间增长的 ID 的业务。它不自动解决节点身份分配、时钟回拨、跨机房故障或业务幂等问题,这些必须在部署和数据层一并设计。
1. 先确认 ID 系统需要满足什么
选择 Snowflake 前先明确:
- 是否必须为整数,还是 UUID/ULID 也能满足需求?
- 是否要求跨节点全局唯一,还是数据库自增 ID 已足够?
- 是否需要严格全局顺序,还是只需要大致按生成时间排序?
- 节点 ID 如何在扩缩容、重启和多机房环境中保证唯一?
- 系统检测到时钟回拨时,应等待、拒绝请求还是切换节点?
Snowflake 通常提供的是全局唯一和趋势递增,不是严格的全局单调递增顺序。不同节点在同一毫秒生成的 ID 顺序由节点位和序列位决定;网络延迟也会让“先收到”与“先生成”不同。
2. 经典 64 位布局
下面使用一种常见布局:
0 | elapsed milliseconds | node ID | sequence
| 41 bits | 10 bits | 12 bits
|<------------- 63 usable bits -------------------------->|
| 部分 | 位数 | 含义 |
|---|---|---|
| 符号位 | 1 | 固定为 0,使 ID 保持在有符号 int64 的正数范围内。 |
| 时间差 | 41 | 当前毫秒减去自定义纪元,可用约 69 年。 |
| 节点 ID | 10 | 最多 1024 个同时发号的节点。 |
| 序列号 | 12 | 单节点单毫秒最多 4096 个 ID。 |
该布局的理论上限约为每节点每秒 409.6 万个 ID。实际吞吐还受互斥、CPU、时钟读取和调用方式影响,不能把理论位数直接当作压测结果。
位布局应随业务调整:节点数很多时可增加节点位;单节点突发更高时可增加序列位;两者都会挤占时间位并缩短可用年限。
3. 生成流程
每个生成器维护两个状态:上一次成功发号的毫秒值,以及该毫秒内已使用的序列号。
- 读取当前毫秒时间。
- 若时间小于上次时间,判定时钟回拨并停止发号或进入受控恢复。
- 若仍在同一毫秒,序列号递增。
- 若序列号耗尽,等待下一毫秒再生成,不能回绕产生重复 ID。
- 若进入新毫秒,序列号重置为 0。
- 将时间差、节点 ID 和序列号按位拼接。
这个过程需要在单个生成器实例内串行化。多个进程之间不共享这把互斥锁,因此节点 ID 唯一性是前提。
4. Go 实现
下面是一个进程内并发安全的最小实现。它选择在检测到时钟回拨时返回错误,由调用方根据业务决定重试、熔断或切换节点。
package snowflake
import (
"errors"
"fmt"
"sync"
"time"
)
const (
timestampBits = 41
nodeBits = 10
sequenceBits = 12
nodeMax = (1 << nodeBits) - 1
sequenceMask = (1 << sequenceBits) - 1
timestampMax = (1 << timestampBits) - 1
nodeShift = sequenceBits
timestampShift = nodeBits + sequenceBits
)
var ErrClockMovedBackwards = errors.New("snowflake: clock moved backwards")
type Generator struct {
mu sync.Mutex
epochMS int64
nodeID int64
lastMS int64
sequence int64
}
func New(epoch time.Time, nodeID int64) (*Generator, error) {
if nodeID < 0 || nodeID > nodeMax {
return nil, fmt.Errorf("snowflake: node ID must be in [0, %d]", nodeMax)
}
return &Generator{
epochMS: epoch.UnixMilli(),
nodeID: nodeID,
lastMS: -1,
}, nil
}
func (g *Generator) Next() (int64, error) {
g.mu.Lock()
defer g.mu.Unlock()
nowMS := time.Now().UnixMilli()
if nowMS < g.lastMS {
return 0, ErrClockMovedBackwards
}
if nowMS == g.lastMS {
g.sequence = (g.sequence + 1) & sequenceMask
if g.sequence == 0 {
// 当前毫秒的序列号耗尽,等待进入下一毫秒。
for nowMS <= g.lastMS {
time.Sleep(time.Millisecond)
nowMS = time.Now().UnixMilli()
}
}
} else {
g.sequence = 0
}
elapsed := nowMS - g.epochMS
if elapsed < 0 {
return 0, fmt.Errorf("snowflake: current time precedes epoch")
}
if elapsed > timestampMax {
return 0, fmt.Errorf("snowflake: timestamp field exhausted")
}
g.lastMS = nowMS
id := (elapsed << timestampShift) |
(g.nodeID << nodeShift) |
g.sequence
return id, nil
}
使用时,通常为每个进程创建一个生成器并复用:
generator, err := snowflake.New(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), 42)
if err != nil {
panic(err)
}
id, err := generator.Next()
if err != nil {
// 记录告警,并按业务策略拒绝或稍后重试。
return err
}
_ = id
5. 节点 ID 才是分布式唯一性的前提
同一时刻的两个节点只要使用相同 node ID,就可能生成相同 ID。不要根据临时 IP、容器名哈希或随机数直接推导 node ID;扩缩容、重启或网络变更时这些值可能冲突。
常见分配方式包括:
- 在部署系统中为实例显式注入稳定的 node ID,并在发布流程中校验唯一性。
- 使用具备租约和冲突检测能力的注册中心分配 node ID。
- 对固定规模的 StatefulSet 等稳定实例,使用经过约束的序号映射。
不论采用哪种方式,都应监控重复 node ID、节点数量接近位数上限和发号错误率。节点身份发生冲突时,应停止发号而不是继续尝试。
6. 时钟回拨与序列耗尽
6.1 时钟回拨
时钟回拨可能来自人工校时、虚拟化迁移或时间同步异常。短时间等待追平看似简单,但如果回拨持续时间未知,会拖住请求并隐藏故障。
更稳妥的默认策略是:检测到回拨后返回错误、触发告警并停止该实例的发号服务;只有业务明确允许等待且等待上限受控时,才等待追平。不要通过把时间差强行归零来“修复”,那会破坏唯一性。
6.2 序列耗尽
12 位序列号在单毫秒内用完时,生成器必须等到下一毫秒或主动限流。若这一点经常发生,说明单实例流量超过当前布局或锁实现的设计能力,应增加序列位、按业务分片,或使用多个唯一 node ID 的发号实例。
7. 存储、索引与安全边界
Snowflake ID 可存为数据库的 BIGINT,避免转换成浮点数。JavaScript 的 Number 无法安全表示所有 64 位整数,接口中应以字符串传输,或使用支持 BigInt 的明确协议。
时间位让 ID 通常随生成时间增长,这对 B+ 树插入局部性可能有帮助;它不是保证,也不应据此替代对索引、分片和写热点的压测。
Snowflake ID 会暴露大致生成时间和部分节点/序列信息,因此不应把它当作不可预测的公开凭证。对外部资源访问仍要进行鉴权;需要不可枚举标识时,可使用随机 token 或额外的公开 ID。
8. 如何选型
| 需求 | 更合适的方案 | 说明 |
|---|---|---|
| 单库、低到中等写入量 | 数据库自增 ID | 简单,事务内可获得 ID。 |
| 本地高吞吐、整数 ID、近似时间排序 | Snowflake | 需处理节点 ID 与时钟。 |
| 多语言、公开可用且不暴露时间 | UUIDv4 或随机 token | 无中心协调,索引局部性较弱。 |
| 希望兼顾可排序和公开标识 | ULID/UUIDv7 等 | 仍需评估实现、存储与时钟策略。 |
| 需要严格全局顺序 | 专用序列服务或单一协调点 | 吞吐、可用性与一致性需要明确取舍。 |
9. 上线检查清单
- 已确认 bit 位分配满足节点数、峰值速率和生命周期。
- node ID 由可审计的机制分配,并能检测冲突。
- 已定义时钟回拨的告警与拒绝/恢复策略。
- 已压测单实例在目标并发下的发号延迟和序列耗尽频率。
- 数据库使用整数类型,前端和 API 不会以浮点数解析 ID。
- 未把 Snowflake ID 当作权限令牌或不可枚举的公开标识。