分布式全局唯一ID: snowflake 算法

在分布式系统中,经常需要生成全局唯一ID。雪花算法是由Twitter开源的一种生成全局唯一ID的算法,在业中广泛应用。本文将介绍雪花算法,以及Go如何使用雪花算法来生成全局唯一ID。

snowflake

雪花算法的核心思想是将一个64bit的整数分成多个部分,包括: 时间戳、机器ID、序列号等, 不依赖mysql、redis、内存等,通过服务器时间、定义的工作机器ID和序号来保证全局唯一性,同时具体一定的有序性。生成的值大概是这样的:

js 复制代码
1765396306500521984
1765757491058053120
1765758879200710656

组成部分

json 复制代码
|    最高位    |      中位         |     中下位      |       末位           |
+--------------------------------------------------------------------------+
| 1 Bit Unused | 41 Bit Timestamp |  10 Bit NodeID  |   12 Bit Sequence ID |
+--------------------------------------------------------------------------+
  • 最高位占1 bit,值固定为 0
  • 中位占 41 bit,值为毫秒级时间戳,总共可以容纳约70年左右的时间
  • 中下位占 10 bit,值为工作机器的 ID,值的上限为 1024
  • 末位占 12 bit,值为当前毫秒内 0开始不断累加,值的上限为 4096

所以,SnowFlake一毫秒内最多可以生成全局唯一ID的数量为:1024 X 4096 = 4194304

Go实现snowflake

参考[4]中的Example, 看看Go如何实现snowflake

go 复制代码
package main

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

// 定义常量
const (
	epoch           = 1704038400000 // 2024-01-01的时间戳
	workerIDBits    = 5             // 机器ID所占的位数
	sequenceBits    = 8             // 序列号所占的位数
	maxWorkerID     = -1 ^ (-1 << workerIDBits)
	maxSequence     = -1 ^ (-1 << sequenceBits)
	workerIDShift   = sequenceBits
	timestampShift  = workerIDBits + sequenceBits
)

// 定义结构体
type Snowflake struct {
	mu           sync.Mutex
	workerID     int64
	lastTimestamp int64
	sequence     int64
}

// 创建Snowflake实例
func NewSnowflake(workerID int64) (*Snowflake, error) {
	if workerID < 0 || workerID > maxWorkerID {
		return nil, errors.New("invalid worker ID")
	}
	return &Snowflake{
		workerID: workerID,
	}, nil
}

// 生成ID的方法
func (s *Snowflake) GenerateID() int64 {
	s.mu.Lock()
	defer s.mu.Unlock()

	timestamp := time.Now().UnixNano() / 1000000
	if timestamp < s.lastTimestamp {
		panic("clock moved backwards")
	}

	if timestamp == s.lastTimestamp {
		s.sequence = (s.sequence + 1) & maxSequence
		if s.sequence == 0 {
			// 当前毫秒的序列号已经用完,等待下一毫秒
			for timestamp <= s.lastTimestamp {
				timestamp = time.Now().UnixNano() / 1000000
			}
		}
	} else {
		s.sequence = 0
	}

	s.lastTimestamp = timestamp

	id := ((timestamp - epoch) << timestampShift) | (s.workerID << workerIDShift) | s.sequence
	return id
}

// 使用示例
func main() {
	snowflake, err := NewSnowflake(1)
	if err != nil {
		panic(err)
	}

	id := snowflake.GenerateID()
	println(id)
}

可以看到,实现的代码简练,不过短短的数十行。通过传入node值,调用NewSnowflake生成一个ID生成器,执行函数GenerateID生成唯一id。让我们写一下基准测试,看看性能如何。

go 复制代码
func BenchmarkGenerate(b *testing.B) {
	node, err := NewNode(1)
	if err != nil {
		b.Fatal(err)
	}

	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		_ = node.Generate()
	}
}

输出:

js 复制代码
BenchmarkGenerate
BenchmarkGenerate-4        64616             28031 ns/op

当然,snowflake的缺点也是比较明显的:

  1. 服务器的时间发生了错乱或者回拨, 有很大 概率生成重复的 ID
  2. 中位是 41 bit 的毫秒级时间戳,也就是从当前起始到41 bit 耗尽, 只够用70 年左右

bwmarrin/snowflake

bwmarrin/snowflake 是一个非常简单的、稳定且完整的Twitter 第三方雪花生成器。它提供了:

  • 解析现有雪花 ID 的方法。
  • 将雪花 ID 转换为多种其他数据类型并返回的方法。
  • JSON Marshal/Unmarshal 函数可在 JSON API 中轻松使用雪花 ID。
  • 单调时钟计算可防止时钟漂移。
go 复制代码
go get github.com/bwmarrin/snowflake

一个入门级的例子

go 复制代码
package main

import (
    "fmt"

    "github.com/bwmarrin/snowflake"
)

func main() {

    // Create a new Node with a Node number of 1
    node, err := snowflake.NewNode(1)
    if err != nil {
       fmt.Println(err)
       return
    }

    // Generate a snowflake ID.
    id := node.Generate()

    // Print out the ID in a few different ways.
    fmt.Printf("Int64  ID: %d\n", id)
    fmt.Printf("String ID: %s\n", id)
    fmt.Printf("Base2  ID: %s\n", id.Base2())
    fmt.Printf("Base64 ID: %s\n", id.Base64())

    // Print out the ID's timestamp
    fmt.Printf("ID Time  : %d\n", id.Time())

    // Print out the ID's node number
    fmt.Printf("ID Node  : %d\n", id.Node())

    // Print out the ID's sequence number
    fmt.Printf("ID Step  : %d\n", id.Step())

    // Generate and print, all in one.
    fmt.Printf("ID       : %d\n", node.Generate().Int64())
}

输出

js 复制代码
Int64  ID: 1765396306500521984
String ID: 1765396306500521984
Base2  ID: 1100001111111111100101001011001011000110000000001000000000000
Base64 ID: MTc2NTM5NjMwNjUwMDUyMTk4NA==
ID Time  : 1709738253604
ID Node  : 1
ID Step  : 0
ID       : 1765396306500521985

为了和上例进行对比,这里我们做了基准测试,仅供参考:

go 复制代码
func BenchmarkGenerate(b *testing.B) {
    node, err := snowflake.NewNode(1)
    if err != nil {
       fmt.Println(err)
       return
    }

    b.ResetTimer()

    for i := 0; i < b.N; i++ {
       _ = node.Generate()
    }
}
js 复制代码
BenchmarkGenerate
BenchmarkGenerate-4      1000000              1119 ns/op

更多的snowflake算法实现可以参考:链接【3】【4】

总结

本文简要介绍了雪花算法, 核心是使用时间戳、机器ID、序列号等生成一个64bit整数的全局唯一ID。文中还介绍了Go语言是如何实践的,并介绍了使用snowflake的几个第三方库bwmarrin/snowflake、sonyflake等,并对比了几个基准测试结果,简单且高效。

参考

  1. github.com/twitter-arc...
  2. github.com/bwmarrin/sn...
  3. github.com/sony/sonyfl...
  4. github.com/rs/xid
  5. mp.weixin.qq.com/s/zT4kKkaGd...
  6. zhuanlan.zhihu.com/p/85837641
相关推荐
李洋-蛟龙腾飞公司16 分钟前
HarmonyOS Next 应用元服务开发-分布式数据对象迁移数据文件资产迁移
分布式·华为·harmonyos
搬码后生仔1 小时前
asp.net core webapi项目中 在生产环境中 进不去swagger
chrome·后端·asp.net
凡人的AI工具箱1 小时前
每天40分玩转Django:Django国际化
数据库·人工智能·后端·python·django·sqlite
Lx3522 小时前
Pandas数据重命名:列名与索引为标题
后端·python·pandas
小池先生2 小时前
springboot启动不了 因一个spring-boot-starter-web底下的tomcat-embed-core依赖丢失
java·spring boot·后端
技术路上的苦行僧3 小时前
分布式专题(10)之ShardingSphere分库分表实战指南
分布式·shardingsphere·分库分表
小蜗牛慢慢爬行3 小时前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate
GitCode官方4 小时前
GitCode 光引计划投稿 | GoIoT:开源分布式物联网开发平台
分布式·开源·gitcode
wm10434 小时前
java web springboot
java·spring boot·后端
小扳5 小时前
微服务篇-深入了解 MinIO 文件服务器(你还在使用阿里云 0SS 对象存储图片服务?教你使用 MinIO 文件服务器:实现从部署到具体使用)
java·服务器·分布式·微服务·云原生·架构