分布式唯一ID生成:深入理解Snowflake算法在Go中的实现

简介: 在分布式系统中,确保每个节点生成的 ID 唯一且高效至关重要。Snowflake 算法由 Twitter 开发,通过 64 位 long 型数字生成全局唯一 ID,包括 1 位标识位、41 位时间戳、10 位机器 ID 和 12 位序列号。该算法具备全局唯一性、递增性、高可用性和高性能,适用于高并发场景,如电商促销时的大量订单生成。本文介绍了使用 Go 语言的 `bwmarrin/snowflake` 和 `sony/sonyflake` 库实现 Snowflake 算法的方法。

在分布式系统中,为了确保每个节点生成的 ID 在整个系统中是唯一的,我们需要一种高效且可靠的 ID 生成机制。

分布式 ID 的特点

  • 全局唯一性:不能出现有重复的 ID 标识,这是基本要求。
  • 递增性:确保生成的 ID 对于用户或业务是递增的。
  • 高可用性:确保任何时候都能生成正确的 ID。
  • 高性能性:在高并发的环境下依然表现良好。

分布式 ID 的应用场景

不仅仅是用于用户 ID,实际互联网中有很多场景都需要能够生成类似 MySQL 自增 ID 这样不断增大,同时又不会重复的 ID,以支持业务中的高并发场景。

比较典型的场景有:电商促销时短时间内会有大量的订单涌入到系统,比如每秒 10W+ 在这些业务场景下将数据插入数据库之前,我们需要给这些订单和数据先分配一个唯一 ID,然后再保存到数据库中。

对这个 ID 的要求是希望其中能带有一些时间信息,这样即使我们后端的系统对数据进行了分库分表,也能够以时间顺序对这些数据进行排序。

Snowflake 算法就是这样的一种算法,它最初由 Twitter 开发,并因其高效、稳定、可扩展等优点,被广泛应用于分布式系统中。

Snowflake 算法(雪花算法)

Twitter 的分布式 ID 生成算法,是一个经过实践考验的算法,它的核心思想是:使用一个 64 位的 long 型的数字作为全局唯一 ID。在这 64 位中,其中 1 位是不用的,然后用其中的 41 位作为毫秒数,用 10 位作为工作机器 id,12 位作为序列号。

雪花算法

  • 1 位标识位:最高位是符号位,正数是 0,负数是 1,生成的 ID 一般是正数,所以为 0。
  • 时间戳:占用 41bit,单位为毫秒,总共可以容纳约 69 年的时间。当然,我们的时间毫秒计数不会真的从 1970 年开始计,那样我们的系统跑到 2039 年 9 月 7 日 23:47:35 就不能用了,所以这里的时间戳只是相对于某个时间的增量,比如我们的系统上线是 2024-08-20,那么我们的时间戳就是当前时间减去 2024-08-20 的时间戳,得到的偏移量。
  • 机器 id:占用 10bit,其中高位 5bit 是数据中心 ID(datacenterId),低位 5bit 是机器 ID(workerId),可以部署在 2^5=32 个机房,每个机房可以部署 2^5=32 台机器,可以容纳 1024 个节点。
  • 序列号:占用 12bit,用来记录同毫秒内产生的不同 ID。每个节点每毫秒开始不断累加,最多可以累加到 4095,同一毫秒一共可以产生 4096 个 ID。

SnowFlake 算法在同一毫秒内最多可以生成多少个全局唯一 ID 呢?

同一毫秒的 ID 数量 = 1024 * 4096 = 4194304,也就是说在同一毫秒内最多可以生成 4194304 个全局唯一 ID。

雪花算法的 Go 语言实现

在本文中,我们将通过 Go 语言的两个库——bwmarrin/snowflakesony/sonyflake,来详细探讨如何实现基于 Snowflake 算法的分布式唯一 ID 生成器。

1. 使用bwmarrin/snowflake生成唯一ID

我们首先使用bwmarrin/snowflake库来生成唯一ID。

package snow_flake

import (
    "fmt"
    "reflect"
    "time"

    "github.com/bwmarrin/snowflake"
)

func SnowFlake1() {
   
    var (
        node *snowflake.Node
        st   time.Time
        err  error
    )

    startTime := "2024-08-20" // 初始化一个开始的时间,表示从这个时间开始算起
    machineID := 1            // 机器 ID

    st, err = time.Parse("2006-01-02", startTime)
    if err != nil {
   
        panic(err)
    }

    snowflake.Epoch = st.UnixNano() / 1000000
    // 根据指定的开始时间和机器ID,生成节点实例
    node, err = snowflake.NewNode(int64(machineID))
    if err != nil {
   
        panic(err)
    }

    // 生成并输出 ID
    id := node.Generate()

    fmt.Printf("Int64  ID: %d type of: %T -> Type %v -> Value %v \n", id, id, reflect.TypeOf(id), reflect.ValueOf(id))
    fmt.Printf("Int64  ID: %d\n", id.Int64()) // 也可以直接调用 Int64() 方法
    fmt.Printf("String ID: %s\n", id)
    fmt.Printf("Base2  ID: %s\n", id.Base2())
    fmt.Printf("Base64 ID: %s\n", id.Base64())
}

代码解析:

  1. 时间戳与机器ID:我们首先定义了一个时间戳和机器ID。这里的时间戳用于记录从特定时间开始的毫秒数,而机器ID则用于区分不同节点。
  2. 生成节点实例snowflake.NewNode()函数根据时间戳和机器ID生成一个节点实例。
  3. 生成唯一ID:使用node.Generate()方法生成唯一ID,并展示了多种表示形式。

2. 使用sony/sonyflake生成唯一ID

接下来,我们来看一下sony/sonyflake库的实现。

package snow_flake

import (
    "fmt"
    "reflect"
    "time"

    "github.com/sony/sonyflake"
)

func SnowFlake2() {
   
    var (
        sonyFlake     *sonyflake.Sonyflake
        sonyMachineID uint16
        st            time.Time
        err           error
    )

    startTime := "2024-08-20" // 初始化一个开始的时间,表示从这个时间开始算起
    machineID := 1            // 机器 ID
    st, err = time.Parse("2006-01-02", startTime)
    if err != nil {
   
        panic(err)
    }

    sonyMachineID = uint16(machineID)
    settings := sonyflake.Settings{
   
        StartTime: st,
        MachineID: func() (uint16, error) {
    return sonyMachineID, nil },
    }
    sonyFlake = sonyflake.NewSonyflake(settings)
    if sonyFlake == nil {
   
        panic("sonyflake not created")
    }

    id, err := sonyFlake.NextID()
    if err != nil {
   
        panic(err)
    }

    fmt.Printf("Int64  ID: %d type of: %T -> Type %v -> Value %v \n", id, id, reflect.TypeOf(id), reflect.ValueOf(id))
}

代码解析:

  1. StartTime:类似于snowflake,我们通过StartTime设置了ID生成的起始时间。
  2. MachineID:通过Settings结构体的MachineID字段指定机器ID的获取方式。

选择哪个库?

bwmarrin/snowflakesony/sonyflake都提供了基于Snowflake算法的分布式唯一ID生成器。选择哪个库取决于你的需求:

  • bwmarrin/snowflake:成熟、广泛应用,如果你需要生成不同进制的ID(如Base2, Base64)或对时间戳的精度要求更高,可以选择这个库。
  • sony/sonyflake:优化了一些性能细节,更适合对性能有更高要求的场景。

结论

Snowflake 算法通过简单却有效的方式解决了分布式系统中唯一 ID 生成的问题。无论是 bwmarrin/snowflake 还是 sony/sonyflake,都提供了强大的工具让我们可以在 Go 语言中轻松实现这一算法。在具体应用中,我们可以根据需求选择适合的库,以确保系统的高效性和稳定性。

相关文章
|
9天前
|
存储 算法 安全
分布式系统架构1:共识算法Paxos
本文介绍了分布式系统中实现数据一致性的重要算法——Paxos及其改进版Multi Paxos。Paxos算法由Leslie Lamport提出,旨在解决分布式环境下的共识问题,通过提案节点、决策节点和记录节点的协作,确保数据在多台机器间的一致性和可用性。Multi Paxos通过引入主节点选举机制,优化了基本Paxos的效率,减少了网络通信次数,提高了系统的性能和可靠性。文中还简要讨论了数据复制的安全性和一致性保障措施。
26 1
|
17天前
|
NoSQL Java 数据处理
基于Redis海量数据场景分布式ID架构实践
【11月更文挑战第30天】在现代分布式系统中,生成全局唯一的ID是一个常见且重要的需求。在微服务架构中,各个服务可能需要生成唯一标识符,如用户ID、订单ID等。传统的自增ID已经无法满足在集群环境下保持唯一性的要求,而分布式ID解决方案能够确保即使在多个实例间也能生成全局唯一的标识符。本文将深入探讨如何利用Redis实现分布式ID生成,并通过Java语言展示多个示例,同时分析每个实践方案的优缺点。
34 8
|
18天前
|
存储 算法 安全
SnowflakeIdGenerator-雪花算法id生成方法
SnowflakeIdGenerator-雪花算法id生成方法
20 1
|
28天前
|
算法
雪花算法反思:订单ID生成的痛点与解决方案
雪花算法(Snowflake Algorithm)因其生成唯一ID的能力而被广泛应用于分布式系统中。然而,随着业务的发展和系统规模的扩大,一些隐藏的问题逐渐浮现。本文将探讨使用雪花算法生成订单ID后可能遇到的挑战,并提供相应的解决方案。
33 2
|
1月前
|
存储 缓存 算法
分布式缓存有哪些常用的数据分片算法?
【10月更文挑战第25天】在实际应用中,需要根据具体的业务需求、数据特征以及系统的可扩展性要求等因素综合考虑,选择合适的数据分片算法,以实现分布式缓存的高效运行和数据的合理分布。
|
1月前
|
分布式计算 Java 开发工具
阿里云MaxCompute-XGBoost on Spark 极限梯度提升算法的分布式训练与模型持久化oss的实现与代码浅析
本文介绍了XGBoost在MaxCompute+OSS架构下模型持久化遇到的问题及其解决方案。首先简要介绍了XGBoost的特点和应用场景,随后详细描述了客户在将XGBoost on Spark任务从HDFS迁移到OSS时遇到的异常情况。通过分析异常堆栈和源代码,发现使用的`nativeBooster.saveModel`方法不支持OSS路径,而使用`write.overwrite().save`方法则能成功保存模型。最后提供了完整的Scala代码示例、Maven配置和提交命令,帮助用户顺利迁移模型存储路径。
|
2月前
|
NoSQL Java Redis
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
Redis分布式锁在高并发场景下是重要的技术手段,但其实现过程中常遇到五大深坑:**原子性问题**、**连接耗尽问题**、**锁过期问题**、**锁失效问题**以及**锁分段问题**。这些问题不仅影响系统的稳定性和性能,还可能导致数据不一致。尼恩在实际项目中总结了这些坑,并提供了详细的解决方案,包括使用Lua脚本保证原子性、设置合理的锁过期时间和使用看门狗机制、以及通过锁分段提升性能。这些经验和技巧对面试和实际开发都有很大帮助,值得深入学习和实践。
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
|
4月前
|
NoSQL Redis
基于Redis的高可用分布式锁——RedLock
这篇文章介绍了基于Redis的高可用分布式锁RedLock的概念、工作流程、获取和释放锁的方法,以及RedLock相比单机锁在高可用性上的优势,同时指出了其在某些特殊场景下的不足,并提到了ZooKeeper作为另一种实现分布式锁的方案。
129 2
基于Redis的高可用分布式锁——RedLock
|
14天前
|
存储 NoSQL Java
使用lock4j-redis-template-spring-boot-starter实现redis分布式锁
通过使用 `lock4j-redis-template-spring-boot-starter`,我们可以轻松实现 Redis 分布式锁,从而解决分布式系统中多个实例并发访问共享资源的问题。合理配置和使用分布式锁,可以有效提高系统的稳定性和数据的一致性。希望本文对你在实际项目中使用 Redis 分布式锁有所帮助。
40 5
|
1月前
|
NoSQL Redis
Redis分布式锁如何实现 ?
Redis分布式锁通过SETNX指令实现,确保仅在键不存在时设置值。此机制用于控制多个线程对共享资源的访问,避免并发冲突。然而,实际应用中需解决死锁、锁超时、归一化、可重入及阻塞等问题,以确保系统的稳定性和可靠性。解决方案包括设置锁超时、引入Watch Dog机制、使用ThreadLocal绑定加解锁操作、实现计数器支持可重入锁以及采用自旋锁思想处理阻塞请求。
55 16