分布式ID生成需求背景
微服务架构下单库自增ID不再适用。分库分表后多个数据库实例各自维护自增序列会导致ID冲突;跨服务调用需要一个全局唯一标识来做幂等控制和链路追踪。业界常见的分布式ID方案包括UUID、数据库号段模式、Redis原子计数、雪花算法(Snowflake)等。
不同方案在各维度上的权衡:
- UUID:唯一性有保障,但无序、过长(36字符)、无法反推时间信息
- 数据库号段:简单可靠,但存在单点瓶颈和性能上限
- Redis计数:性能高,但依赖Redis可用性,持久化可能丢号
- Snowflake:高性能、有序、可反推时间,但依赖时钟一致性
本文介绍如何用Go实现Snowflake算法,以及在集群环境中的工程化处理。
Snowflake算法原理
Snowflake生成64位整数ID,结构如下:
| 1bit | 41bit | 10bit | 12bit |
| 符号 | 时间戳(ms) | 机器ID | 序列号 |
- 符号位:固定0,保证ID为正数
- 时间戳:毫秒级,可用69年(从自定义纪元起算)
- 机器ID:最多1024个节点
- 序列号:每毫秒最多生成4096个ID
理论峰值:单节点409.6万ID/秒,1024节点约41.9亿ID/秒。时间有序,ID按生成时间递增,适合做B+树主键。
Go实现Snowflake
核心结构体和生成逻辑:
package snowflake
import (
"errors"
"sync"
"time"
)
const (
workerIDBits = 10 // 机器ID位数
sequenceBits = 12 // 序列号位数
maxWorkerID = -1 ^ (-1 << workerIDBits) // 1023
maxSequence = -1 ^ (-1 << sequenceBits) // 4095
timestampShift = workerIDBits + sequenceBits // 22
workerIDShift = sequenceBits // 12
)
// 自定义纪元:2024-01-01 00:00:00 UTC(毫秒)
var epoch = int64(1704067200000)
type Node struct {
mu sync.Mutex
workerID int64
sequence int64
lastTime int64
}
func NewNode(workerID int64) (*Node, error) {
if workerID < 0 || workerID > maxWorkerID {
return nil, errors.New("worker ID out of range")
}
return &Node{workerID: workerID}, nil
}
func (n *Node) Generate() (int64, error) {
n.mu.Lock()
defer n.mu.Unlock()
now := time.Now().UnixMilli()
if now < n.lastTime {
return 0, errors.New("clock moved backwards")
}
if now == n.lastTime {
n.sequence = (n.sequence + 1) & maxSequence
if n.sequence == 0 {
// 当前毫秒序列号耗尽,等待下一毫秒
for now <= n.lastTime {
now = time.Now().UnixMilli()
}
}
} else {
n.sequence = 0
}
n.lastTime = now
id := ((now - epoch) << timestampShift) |
(n.workerID << workerIDShift) |
n.sequence
return id, nil
}
使用示例:
func main() {
node, err := snowflake.NewNode(1) // workerID=1
if err != nil {
panic(err)
}
for i := 0; i < 10; i++ {
id, _ := node.Generate()
fmt.Println(id)
}
}
时钟回拨问题处理
Snowflake强依赖机器时钟。NTP同步可能导致时钟回跳,回拨期间生成的ID可能与已生成的ID重复。上例中的做法是直接报错拒绝生成,但在生产环境中更合理的策略是:
func (n *Node) Generate() (int64, error) {
n.mu.Lock()
defer n.mu.Unlock()
now := time.Now().UnixMilli()
if now < n.lastTime {
// 回拨时长在容忍范围内:等待追平
diff := n.lastTime - now
if diff <= 5 { // 5ms以内等待
time.Sleep(time.Duration(diff) * time.Millisecond)
now = time.Now().UnixMilli()
if now < n.lastTime {
return 0, errors.New("clock moved backwards")
}
} else {
// 回拨过大:使用扩展位或拒绝服务
return 0, errors.New("clock moved backwards too far")
}
}
// ... 正常生成逻辑
}
对于更严格的场景,可以借用序列号高位作为扩展位:当检测到时钟回拨时,将workerID的高位翻转,生成一个"影子节点"的ID。但这缩短了可用workerID范围,需要配合ZooKeeper或Etcd管理扩展位分配。
workerID自动分配
手动配置workerID在容器化环境中不可行——Pod的IP和实例编号每次部署都会变化。通过Etcd实现workerID的自动注册和租约回收:
package workerid
import (
"context"
"fmt"
"go.etcd.io/etcd/client/v3/concurrency"
"time"
)
type Manager struct {
client *clientv3.Client
keyPrefix string
}
func NewManager(endpoints []string) (*Manager, error) {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
return &Manager{client: cli, keyPrefix: "/snowflake/workers"}, err
}
func (m *Manager) AcquireWorkerID(ctx context.Context) (int64, func(), error) {
// 使用Etcd分布式锁竞争workerID
for workerID := int64(0); workerID < 1024; workerID++ {
key := fmt.Sprintf("%s/%d", m.keyPrefix, workerID)
// 创建session(带TTL租约)
session, err := concurrency.NewSession(m.client,
concurrency.WithTTL(10))
if err != nil {
continue
}
// 尝试获取该workerID的锁
mutex := concurrency.NewMutex(session, key)
if err := mutex.TryLock(ctx); err != nil {
session.Close()
continue // 已被占用,尝试下一个
}
// 成功获取workerID
release := func() {
mutex.Unlock(ctx)
session.Close()
}
return workerID, release, nil
}
return 0, nil, errors.New("no available worker ID")
}
使用方式:
func main() {
mgr, _ := workerid.NewManager([]string{"localhost:2379"})
workerID, release, err := mgr.AcquireWorkerID(context.Background())
if err != nil {
panic(err)
}
defer release() // 进程退出时释放workerID
node, _ := snowflake.NewNode(workerID)
// 正常使用node.Generate()生成ID
// ...
}
ID反解析
Snowflake ID可反解出生成时间、机器ID和序列号,方便问题排查:
func ParseID(id int64) (timestamp time.Time, workerID int64, sequence int64) {
// 提取序列号(低12位)
sequence = id & maxSequence
// 提取workerID(中间10位)
workerID = (id >> workerIDShift) & maxWorkerID
// 提取时间戳(高41位)
ts := (id >> timestampShift) + epoch
timestamp = time.UnixMilli(ts)
return
}
// 使用示例
id, _ := node.Generate()
ts, wid, seq := ParseID(id)
fmt.Printf("时间: %v, 机器: %d, 序列: %d\n", ts, wid, seq)
性能基准测试
对上述实现做基准测试,评估单机生成性能:
func BenchmarkGenerate(b *testing.B) {
node, _ := NewNode(1)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, _ = node.Generate()
}
})
}
// 测试结果(8核机器):
// BenchmarkGenerate-8 3000000 412 ns/op
// 约240万ID/秒
锁竞争是主要瓶颈。在超高并发场景下(单机百万QPS),可将Node池化:预创建多个Node实例,通过goroutine绑定不同workerID,按取模分配请求。但实际业务中单机240万QPS已满足绝大部分需求。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/go-shi-xian-snowflake-fen-bu-shi-id-sheng-cheng-qi-shi/