雪花算法 (Snowflake Algorithm) 是 Twitter 公司开源的一种分布式唯一 ID 生成算法。它旨在解决在分布式系统中生成全局唯一、递增(但非严格递增)且高性能 ID 的需求。其生成的 ID 是一个 64 位的整数,具有时间有序性,并且不依赖于数据库,易于扩展。

核心思想:将 64 位的 Long 型 ID 拆分为多个字段,分别存储时间戳、数据中心 ID、机器 ID 和序列号,通过位运算拼接以保证全局唯一性和大致的时间有序性。


一、为什么需要雪花算法?

在分布式系统中,传统的单点自增 ID 方案面临巨大挑战:

  1. 唯一性问题:不同的数据库实例或服务节点可能生成相同的 ID。
  2. 性能瓶颈:为了保证唯一性,可能需要引入中心化的 ID 生成服务或数据库锁,成为系统瓶颈。
  3. 可用性问题:中心化服务一旦宕机,整个系统的 ID 生成将受影响。

虽然 UUID 能够保证全局唯一性,但它存在一些缺点:

  1. 存储和传输效率低:128 位,比 64 位 ID 更占用空间,索引性能较差。
  2. 无序性:UUID 是无序的,插入数据库时会导致 B+ 树索引频繁分裂和重建,影响数据库性能。

雪花算法旨在提供一个兼顾高性能、高可用、全局唯一且大致有序的分布式 ID 解决方案。

二、雪花算法的 ID 结构

雪花算法生成的 ID 是一个 64 位的 long 型整数(在 Go 中是 int64),其结构被划分为以下几个部分:

1
2
| 1 位 (Unused) | 41 位 (时间戳) | 10 位 (工作节点 ID) | 12 位 (序列号) |
|---------------|----------------|---------------------|----------------|

各部分的详细说明如下:

2.1 1 位:未用 (符号位)

  • 在 Java 等语言中,最高位是符号位。由于生成的 ID 总是正数,所以这一位固定为 0,不参与 ID 的计算,也为了保证 ID 永远为正。

2.2 41 位:时间戳 (Timestamp)

  • 含义: 自定义的一个起始时间(Epoch)以来的毫秒数。
  • 作用: 保证 ID 的时间有序性,并且在分布式环境中避免时钟同步问题。
  • 计算方式: 当前时间戳 - 自定义 Epoch 时间戳。
  • 可使用时间: 41 位二进制数最大值是 2^41 - 1 = 2199023255551 毫秒,大约可以使用 69 年。
    • 2199023255551 ms ≈ 2199023255.551 s ≈ 36650387.59 min ≈ 610839.79 h ≈ 25451.65 days ≈ 69.73 years
  • 自定义 Epoch: 选择一个时间点作为 Epoch,例如 Twitter 选择了 2010-11-04 01:42:54 UTC。这使得我们不必担心 ID 的有效期限在短期内耗尽。

2.3 10 位:工作节点 ID (Worker ID)

  • 10 位又可以细分为:
    • 5 位:数据中心 ID (Datacenter ID)
      • 含义: 标识不同的数据中心。
      • 最大值: 2^5 - 1 = 31。意味着最多可以支持 32 个数据中心。
    • 5 位:机器 ID (Worker ID)
      • 含义: 标识数据中心内的不同机器(或进程)。
      • 最大值: 2^5 - 1 = 31。意味着每个数据中心最多可以支持 32 台机器。
  • 作用: 结合数据中心 ID 和机器 ID,共 10 位 2^10 - 1 = 1023,最多可支持 1024 个分布式节点。这确保了在同一个毫秒内,不同机器生成的 ID 不会重复。

2.4 12 位:序列号 (Sequence Number)

  • 含义: 在同一个毫秒内,同一个工作节点上生成 ID 的序号。
  • 最大值: 2^12 - 1 = 4095。意味着在同一个毫秒内,单个工作节点最多可以生成 4096 个不同的 ID。
  • 作用: 当同一个毫秒内有大量 ID 生成请求时,通过序列号递增来保证 ID 的唯一性。如果当前毫秒的序列号已达到最大值,系统会等待下一个毫秒再生成 ID。

三、算法实现与原理

生成雪花 ID 的核心原理是位运算,将上述各个部分拼接起来。

1
2
3
4
ID = (timestamp - epoch) << (datacenterIdBits + workerIdBits + sequenceBits)
| (datacenterId << (workerIdBits + sequenceBits))
| (workerId << sequenceBits)
| sequence

3.1 关键变量定义

在实现中需要定义一些常量:

  • EPOCH:自定义的起始时间戳(毫秒)。
  • WORKER_ID_BITS:机器 ID 所占位数 (5)。
  • DATACENTER_ID_BITS:数据中心 ID 所占位数 (5)。
  • SEQUENCE_BITS:序列号所占位数 (12)。
  • MAX_WORKER_ID:机器 ID 的最大值 (2^5 - 1 = 31)。
  • MAX_DATACENTER_ID:数据中心 ID 的最大值 (2^5 - 1 = 31)。
  • MAX_SEQUENCE:序列号的最大值 (2^12 - 1 = 4095)。

3.2 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
package snowflake

import (
"errors"
"sync"
"time"
)

// 定义各部分的位数
const (
workerIDBits uint8 = 5 // 机器ID占5位
datacenterIDBits uint8 = 5 // 数据中心ID占5位
sequenceBits uint8 = 12 // 序列号占12位

maxWorkerID int64 = -1 ^ (-1 << workerIDBits) // 机器ID最大值 (31)
maxDatacenterID int64 = -1 ^ (-1 << datacenterIDBits) // 数据中心ID最大值 (31)
maxSequence int64 = -1 ^ (-1 << sequenceBits) // 序列号最大值 (4095)

// 左移位数
workerIDShift uint8 = sequenceBits // 机器ID左移12位
datacenterIDShift uint8 = sequenceBits + workerIDBits // 数据中心ID左移12+5=17位
timestampShift uint8 = sequenceBits + workerIDBits + datacenterIDBits // 时间戳左移12+5+5=22位

// 初始时间戳,可以根据实际情况修改为一个过去的日期,例如 2020-01-01 00:00:00 UTC
// 避免在 2079 年左右溢出
epoch int64 = 1577836800000 // 2020-01-01 00:00:00 UTC 的毫秒时间戳
)

// Snowflake 结构体
type Snowflake struct {
sync.Mutex // 保护并发访问
timestamp int64 // 上次生成ID的时间戳
workerID int64 // 机器ID
datacenterID int64 // 数据中心ID
sequence int64 // 当前毫秒内的序列号
}

// NewSnowflake 创建一个新的 Snowflake 实例
func NewSnowflake(workerID, datacenterID int64) (*Snowflake, error) {
if workerID < 0 || workerID > maxWorkerID {
return nil, errors.New("worker ID invalid")
}
if datacenterID < 0 || datacenterID > maxDatacenterID {
return nil, errors.New("datacenter ID invalid")
}
return &Snowflake{
workerID: workerID,
datacenterID: datacenterID,
timestamp: 0,
sequence: 0,
}, nil
}

// GenerateID 生成一个唯一的 ID
func (sf *Snowflake) GenerateID() int64 {
sf.Lock()
defer sf.Unlock()

now := time.Now().UnixMilli() // 获取当前毫秒时间戳

if now < sf.timestamp {
// 如果当前时间小于上次生成ID的时间,说明时钟回拨,抛出错误或等待
// 严谨的生产环境应该直接抛出错误或根据策略处理
panic(errors.New("clock moved backwards, refusing to generate ID"))
}

if now == sf.timestamp {
// 在同一个毫秒内,序列号递增
sf.sequence = (sf.sequence + 1) & maxSequence
if sf.sequence == 0 {
// 当前毫秒内的序列号已用完,等待下一个毫秒
now = sf.untilNextMilli(now)
}
} else {
// 不同毫秒,序列号重置为0
sf.sequence = 0
}

sf.timestamp = now // 更新上次生成ID的时间戳

// 组合各部分生成最终ID
id := ((now - epoch) << timestampShift) |
(sf.datacenterID << datacenterIDShift) |
(sf.workerID << workerIDShift) |
sf.sequence
return id
}

// untilNextMilli 等待到下一个毫秒
func (sf *Snowflake) untilNextMilli(lastTimestamp int64) int64 {
now := time.Now().UnixMilli()
for now <= lastTimestamp {
now = time.Now().UnixMilli()
}
return now
}

/*
// 示例用法:
func main() {
// 创建一个数据中心ID为1,机器ID为1的Snowflake实例
node, err := NewSnowflake(1, 1)
if err != nil {
fmt.Println("Error creating snowflake:", err)
return
}

for i := 0; i < 10; i++ {
id := node.GenerateID()
fmt.Printf("Generated ID: %d\n", id)
}

// 模拟并发生成
var wg sync.WaitGroup
ids := make(chan int64, 1000)

for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 10; j++ {
ids <- node.GenerateID()
}
}()
}

wg.Wait()
close(ids)

uniqueIDs := make(map[int64]struct{})
for id := range ids {
if _, ok := uniqueIDs[id]; ok {
fmt.Printf("Duplicate ID found: %d\n", id)
}
uniqueIDs[id] = struct{}{}
}
fmt.Printf("Generated %d unique IDs concurrently.\n", len(uniqueIDs))
}
*/

3.3 Python 语言实现示例

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import time
import threading

# 定义各部分的位数
WORKER_ID_BITS = 5
DATACENTER_ID_BITS = 5
SEQUENCE_BITS = 12

# 最大值
MAX_WORKER_ID = -1 ^ (-1 << WORKER_ID_BITS) # 31
MAX_DATACENTER_ID = -1 ^ (-1 << DATACENTER_ID_BITS) # 31
MAX_SEQUENCE = -1 ^ (-1 << SEQUENCE_BITS) # 4095

# 左移位数
WORKER_ID_SHIFT = SEQUENCE_BITS # 12
DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS # 12 + 5 = 17
TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS # 12 + 5 + 5 = 22

# 初始时间戳 (2020-01-01 00:00:00 UTC 的毫秒时间戳)
# Unix epoch 1970-01-01 00:00:00 UTC
# time.time() 返回浮点秒,需乘以1000变为毫秒
EPOCH = 1577836800000

class Snowflake:
def __init__(self, worker_id, datacenter_id):
if not (0 <= worker_id <= MAX_WORKER_ID):
raise ValueError(f"Worker ID must be between 0 and {MAX_WORKER_ID}")
if not (0 <= datacenter_id <= MAX_DATACENTER_ID):
raise ValueError(f"Datacenter ID must be between 0 and {MAX_DATACENTER_ID}")

self.worker_id = worker_id
self.datacenter_id = datacenter_id
self.sequence = 0
self.last_timestamp = -1
self.lock = threading.Lock()

def _get_timestamp(self):
"""获取当前毫秒时间戳"""
return int(time.time() * 1000)

def _until_next_milli(self, last_timestamp):
"""等待到下一个毫秒"""
timestamp = self._get_timestamp()
while timestamp <= last_timestamp:
timestamp = self._get_timestamp()
return timestamp

def generate_id(self):
with self.lock:
timestamp = self._get_timestamp()

if timestamp < self.last_timestamp:
raise RuntimeError("Clock moved backwards. Refusing to generate id for %d milliseconds" % (self.last_timestamp - timestamp))

if timestamp == self.last_timestamp:
self.sequence = (self.sequence + 1) & MAX_SEQUENCE
if self.sequence == 0: # 序列号用完,等待下一个毫秒
timestamp = self._until_next_milli(self.last_timestamp)
else:
self.sequence = 0 # 新的毫秒,序列号重置

self.last_timestamp = timestamp

# 组合各部分生成最终ID
new_id = (
((timestamp - EPOCH) << TIMESTAMP_SHIFT)
| (self.datacenter_id << DATACENTER_ID_SHIFT)
| (self.worker_id << WORKER_ID_SHIFT)
| self.sequence
)
return new_id

"""
# 示例用法:
if __name__ == "__main__":
# 创建一个数据中心ID为1,机器ID为1的Snowflake实例
node = Snowflake(1, 1)

# 生成少量ID
for _ in range(10):
print(f"Generated ID: {node.generate_id()}")

# 模拟并发生成ID
num_threads = 100
ids = []

def generate_many_ids():
for _ in range(100):
ids.append(node.generate_id())

threads = []
for _ in range(num_threads):
thread = threading.Thread(target=generate_many_ids)
threads.append(thread)
thread.start()

for thread in threads:
thread.join()

print(f"\nGenerated {len(ids)} IDs concurrently.")
print(f"Number of unique IDs: {len(set(ids))}")
if len(ids) != len(set(ids)):
print("Warning: Duplicate IDs found!")
"""

四、工作流程图

下面是一个简化的雪花算法工作流程图,展示了核心的 ID 生成逻辑:

五、优缺点

5.1 优点

  1. 全局唯一: 结合时间戳、数据中心 ID、机器 ID 和序列号,保证了生成的 ID 在分布式环境下的全局唯一性。
  2. 时间有序: 由于 ID 的高位是时间戳,生成的 ID 大致是趋势递增的。这对于数据库索引非常友好,相比 UUID 能提高写性能。
  3. 高性能: ID 生成过程不涉及数据库或网络请求,纯内存计算,每秒可生成数十万甚至数百万个 ID。
  4. 高可用: 不依赖于任何中心化服务,每个节点都可以独立生成 ID。只要单个节点正常运行,就能提供 ID 生成服务。
  5. 信息内聚: ID 中包含了时间信息,可以通过解析 ID 粗略地知道 ID 的生成时间。
  6. 长度适中: 64 位整数,比 UUID(128 位)更短,更易于存储和传输。

5.2 缺点

  1. 依赖系统时间: 严重依赖机器的系统时钟。如果发生时钟回拨(系统时间被调回),可能导致生成重复 ID。虽然算法内部有解决机制(抛出异常或等待),但这仍然是一个潜在的风险点。
  2. ID 不够随机: 由于 ID 是大致有序的,对外暴露的 ID 可能会包含一些信息(如生成时间、机器信息),对于某些对外提供服务的 ID 场景,这或许不是最佳选择。
  3. 配置管理: datacenterIdworkerId 需要为每个节点进行唯一配置。在大型集群中,如何高效、自动化地分配和管理这些 ID 是一项挑战。
  4. 单点限制: 虽然算法本身是分布式的,但如果在同一毫秒内某个节点产生了超过 4096 个 ID,它将不得不等待下一个毫秒,这会形成一个短暂的性能瓶颈。

六、配置管理与时钟问题

6.1 Worker ID 和 Datacenter ID 的分配

这是雪花算法实施的关键挑战之一。常见的分配策略包括:

  • 静态配置: 手动为每个机器或服务配置固定的 workerIDdatacenterID。简单直接,但管理复杂,不适合弹性伸缩的场景。
  • Zookeeper/Etcd 等分布式协调服务: 在服务启动时,通过这些服务注册并获取唯一的 workerID。服务下线时释放 ID。这种方式自动化程度高,但增加了对外部服务的依赖。
  • K8s Pod 名称或 IP 地址解析: 在 Kubernetes 环境中,可以根据 Pod 的名称或 IP 地址进行哈希计算,生成 workerIDdatacenterID

6.2 时钟回拨 (Clock Rollback)

时钟回拨是雪花算法面临的最严重问题。如果系统时间被人工修改或 NTP 同步导致时间向后跳,可能会生成重复的 ID。

处理策略:

  1. 抛出异常: 这是最严格也最安全的处理方式,一旦检测到时钟回拨,立即停止服务并报错。 (如 Go/Python 示例所示)
  2. 等待校准: 让程序等待,直到时钟前进到上次生成 ID 的时间之后。这种方式避免了错误,但可能导致服务短时间不可用。
  3. 使用备用 ID 生成策略: 在时钟回拨期间,切换到另一个临时的、可能性能稍差但能保证唯一性的 ID 生成方案(例如 UUID)。

6.3 Epoch 时间选择

选择一个合适的 Epoch 时间非常重要。

  • 如果选择太早的时间(如 1970-01-01),ID 中的时间戳部分可能会很快耗尽 41 位,导致在几十年后溢出。
  • 选择一个接近当前时间的 Epoch,可以有效延长雪花 ID 的使用寿命。但请注意,一旦确定,Epoch 值不应再改变,否则会影响已生成 ID 的解析。

七、总结

雪花算法是一个高效、可靠的分布式 ID 生成方案,广泛应用于高性能、高可用要求的分布式系统。它通过巧妙地组合时间戳、数据中心 ID、机器 ID 和序列号,在保证 ID 全局唯一性的同时,实现了大致的时间有序性和极高的生成效率。

然而,在使用雪花算法时,必须充分理解其原理和潜在风险,尤其是对系统时钟的依赖和 workerIDdatacenterID 的管理。合理的配置、严谨的实现以及对时钟回拨等异常情况的妥善处理,是确保雪花算法稳定可靠运行的关键。