Snowflake分布式自增ID算法

Twitter的snowflake算法解决了分布式系统生成全局ID的需求,生成64位(二进制位)的Long型数字(8字节),组成部分:

  • 第一位未使用
  • 接下来41位是毫秒级时间,41位的长度可以表示69年的时间
  • 5位datacenterId,5位workerId。10位的长度最多支持部署1024个节点
  • 最后12位是毫秒内的计数,12位的计数顺序号支持每个节点每毫秒产生4096个ID序列。2^12=4096

这样的好处是:毫秒数在高位,生成的ID整体上按时间趋势递增;不依赖第三方系统,稳定性和效率较高,理论上QPS约为409.6w/s(1000*2^12),并且整个分布式系统内不会产生ID碰撞;可根据自身业务灵活分配bit位。

不足就在于:强依赖机器时钟,如果时钟回拨,则可能导致生成ID重复。

结合数据库和snowflake的唯一ID方案,可以参考业界较为成熟的解法:Leaf——美团点评分布式ID生成系统,并考虑到了高可用、容灾、分布式下时钟等问题。

Golang 举例:

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
package main

import (
"fmt"
"time"
"github.com/bwmarrin/snowflake"
)
func main() {
// 设置起始时间和机器 ID
startTime := "2025-03-20"
machineID := 1
// 解析起始时间
st, err := time.Parse("2006-01-02", startTime)
if err != nil {
panic(err)
}
// 设置雪花算法的起始时间
snowflake.Epoch = st.UnixNano() / 1000000
// 创建节点实例
node, err := snowflake.NewNode(int64(machineID))
if err != nil {
panic(err)
}
fmt.Println(st)
// 生成并输出 ID
id := node.Generate()
fmt.Printf("Int64 ID: %d\n", id.Int64())
fmt.Printf("String ID: %s\n", id)
}

结果:
2025-03-07 00:00:00 +0000 UTC
Int64 ID: 1283328784765816832
String ID: 1283328784765816832

代码解析

1.设置起始时间和机器 ID:起始时间用于确定雪花算法的时间起点,机器 ID 用于标识不同的机器节点。
可以通过修改 startTime 变量来自定义雪花算法的起始时间。
在分布式系统中,不同的机器节点需要有不同的机器 ID,以确保生成的 ID 全局唯一。
2.创建节点实例:通过 snowflake.NewNode 函数创建一个新的节点实例,该实例会根据当前时间和机器 ID 生成唯一的 ID。
3.生成唯一 ID:使用 node.Generate() 方法生成唯一的 ID,并可以通过不同的方法(如 Int64()、String() 等)获取 ID 的不同表示形式。
4.time.Parse 函数中,布局字符串(例如 “2006-01-02”)并不表示具体的时间值,而是表示时间的格式。布局字符串是一个固定的格式模板,用于定义如何解析时间字符串。
5.处理时间回拨:如果系统时钟发生回拨,可能会导致生成重复的 ID。可以通过关闭生成 ID 机器的时间同步或使用时间服务器来解决此问题

雪花算法的问题

雪花算法生成的 ID强依赖于系统时钟,其ID 包含时间戳信息,它利用时间的连续性来保证生成的 ID 的唯一性和顺序性。如果系统时钟发生回拨,可能会导致雪花算法试图生成比之前更早时间戳的 ID,从而可能生成已经使用过的 ID,破坏了唯一性。

系统时钟发生回拨的解决办法

1.在生成 ID 的机器上关闭自动时间同步功能,避免因时间同步导致的时钟回拨。这适用于那些对时间精度要求不是特别高,或者可以通过手动调整来保证时间准确性的环境。(在实际操作中,关闭时间同步可能会导致系统时间与实际时间不符,需要定期手动校准)

关闭时间同步后,能正常生成ID吗?

关闭时间同步后,仍然可以正常生成雪花算法的 ID,但需要确保系统时间不会被外部时间服务器或手动调整所改变。 关闭时间同步后,系统时钟将依赖本地的硬件时钟,如果硬件时钟正常工作且没有其他因素干扰,系统时钟会保持稳定,雪花算法可以根据这个稳定的时钟生成唯一的 ID。

如何关闭时间同步功能?

关闭自动时间同步功能并不是直接通过 Beego 等框架去设置的,而是在操作系统或时间同步服务层面进行配置。

在操作系统层面关闭自动时间同步:

1
2
>sudo systemctl stop ntpd //临时关闭时间同步服务
>sudo systemctl disable ntpd //永久禁用时间同步服务

2.部署一个高精度的时间服务器,所有生成 ID 的机器都从这个时间服务器获取时间。时间服务器可以提供稳定、准确的时间服务,避免因本地时钟漂移或回拨导致的问题。这适用于分布式系统环境,可以保证所有节点的时间一致性。(使用时间服务器需要保证网络的稳定性和时间服务器的可靠性。)

什么情况下会发生系统时钟回拨?

1)手动调整系统时间
维护或配置错误:系统管理员或用户在维护系统时,可能会手动调整系统时间,如果不小心将时间设置为过去的某个时刻,就会导致系统时钟回拨。
跨时区部署:在部署系统到不同地理位置或时区时,如果未正确配置时区或时间同步,可能会手动调整时间到目标时区的当前时间,若操作不当,就可能造成时钟回拨。

2)硬件故障或不稳定
硬件故障:主板上的RTC(实时时钟)芯片出现故障,可能会导致系统时间不准确甚至回拨。例如,电池老化或损坏,会使RTC无法保持准确的时间。
虚拟化环境中的时间同步问题:在虚拟化平台(如VMware、KVM等)中,虚拟机的时间通常与宿主机的时间进行同步。如果宿主机或虚拟机的硬件时间出现问题,可能会导致虚拟机的系统时钟回拨。

3)网络时间协议(NTP)同步问题
NTP服务器故障或配置错误:如果系统配置了错误的NTP服务器地址,或者NTP服务器本身出现故障,系统在尝试同步时间时可能会收到错误的时间信息,从而导致时钟回拨。
网络连接不稳定:在使用NTP进行时间同步时,如果网络连接不稳定或延迟过高,可能会导致时间同步过程出现异常,使系统时间回拨。

4)虚拟机迁移或快照恢复
虚拟机迁移:在虚拟化环境中,当虚拟机从一台宿主机迁移到另一台宿主机时,如果目标宿主机的时间与虚拟机之前的时间不一致,可能会导致虚拟机的系统时钟回拨。
快照恢复:恢复虚拟机的旧快照时,系统时间会恢复到快照创建时的状态,这可能导致系统时钟回拨到过去。

5)系统崩溃或电源故障
系统崩溃:系统在运行过程中突然崩溃,可能会导致系统时间未能及时保存或更新,再次启动时可能会出现时间回拨的情况。
电源故障:突然的电源故障可能导致系统非正常关机,系统重新启动后的时间可能会回拨到上次正常关机之前的状态。

6)软件更新或配置更改
软件更新:更新系统软件或某些时间相关的服务时,可能会出现时间配置文件被意外修改或重置,从而导致系统时钟回拨。
配置文件修改:手动修改系统的时间配置文件(如/etc/systemd/timesyncd.conf等),如果不小心输入了错误的时间值或配置,可能会导致时钟回拨。

7)安全攻击
恶意攻击:恶意用户可能会通过某些漏洞或攻击手段篡改系统时间,故意导致系统时钟回拨,以破坏系统的正常运行或安全机制。
在使用雪花算法生成唯一 ID 的系统中,为了避免因系统时钟回拨导致的 ID 重复问题,可以采取以下措施:
关闭时间同步:在生成 ID 的机器上暂时关闭自动时间同步功能,避免因时间同步导致的时钟回拨。
使用时间服务器:部署一个高精度的时间服务器,所有生成 ID 的机器都从这个时间服务器获取时间,确保时间的一致性和准确性

3.在应用层面处理时间回拨问题

1)使用雪花算法库的内置处理机制:一些雪花算法的实现库(如 bwmarrin/snowflake)已经内置了对时间回拨的处理机制,例如在生成 ID 时会检测时间回拨并采取相应的等待策略或使用备用时间源(如从时间服务器获取时间),确保生成的 ID 唯一性。

短暂回拨:短暂的时间回拨(如几毫秒)通常不会导致重复 ID,因为序列号可以继续递增。
长期回拨:长期的时间回拨(如几分钟或更长时间)可能导致重复 ID,尤其是如果回拨到之前已经生成过大量 ID 的时间段。

尽管雪花算法通过结合时间戳、机器 ID 和序列号来生成唯一 ID,时间回拨仍可能增加重复 ID 的风险,尤其是在时间回拨较大或频繁发生的情况下。为了避免这种风险,建议采取以下措施:
使用高精度的时间服务器确保时间的一致性和准确性。
在应用层面实现时间回拨检测和处理机制。
避免手动调整系统时间,确保时间同步服务的稳定性。

增加时间回拨的检测是否冗余
在一些对数据一致性和系统稳定性要求极高的场景,即使库有内置处理机制,也可以在应用层面增加时间回拨的检测作为冗余保障。比如金融、医疗等对数据准确性要求极高的行业。

Golang时间回拨检测机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
"time"
)
func main() {
// 上一时间戳
var lastTimestamp int64
// 模拟程序运行过程中的时间检查
for i := 0; i < 10; i++ {
currentTimestamp := time.Now().UnixNano() / 1e6 // 获取当前时间戳(毫秒)
// 如果当前时间戳小于上一时间戳,则表示发生了时间回拨
if currentTimestamp < lastTimestamp {
fmt.Printf("时间回拨检测到!当前时间戳:%d,上一时间戳:%d\n", currentTimestamp, lastTimestamp)
// 这里可以采取相应的处理措施,比如重试、记录日志、报警等
// ...
} else {
fmt.Printf("时间正常,当前时间戳:%d\n", currentTimestamp)
}
lastTimestamp = currentTimestamp // 更新上一时间戳
time.Sleep(1 * time.Second) // 模拟间隔时间
}
}

2)自定义时间源:在应用中自定义一个时间源,该时间源在检测到时间回拨时,返回之前的时间戳,避免时间回拨对雪花算法的影响。