前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Golang实现分布式唯一ID生成器

Golang实现分布式唯一ID生成器

原创
作者头像
dddyge
发布2024-03-26 18:17:04
3250
发布2024-03-26 18:17:04
举报
文章被收录于专栏:思考与总结思考与总结

分布式唯一ID生成器是业务上经常会需要的一个基础组件,它具有ID有序,且不重复的特点。现在主流的唯一ID生成器有4种方案。

一:数据库主键生成:利用数据库的主键生成来获取唯一ID,但是这种方式依赖数据库组件,并且获取ID的效率也不高。

二:128位的UUID生成:这种方法可以生成128位的UUID,据官方说UUID每秒可以生成100万个,并且等到100年重复的概率才达到50%。但是这个方案有个缺点是UUID不是有序增长的。

三:工单服务器:其实也就是一个服务利用数据库自增主键来获取ID,并且响应请求。但这个服务很可能会有单点故障的问题。

四:雪花ID生成器:这是比较好用的方案,具有ID有序,长度是64位数字,不重复的特点,且可以自定义ID的位数来适配不同的业务的要求。

今天用Golang实现一个雪花ID生成器的组件,顺便加深对分布式唯一ID生成器的理解,也可以独立成一个服务,通过RPC请求将ID返回给对应的服务。具体的注释都在代码中,也可以针对并发不高的服务,增加毫秒时间戳的位数来保证长时间的不重复ID的稳定运行。如果是41位的时间戳表示,那么最多能运行69年的不重复ID生成服务。

代码语言:go
复制
package SnowFlakeYYtest

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

// 手写雪花ID生成器, 分布式唯一ID生成器,ID具有64位,随时间戳增长有序,不重复。
// 雪花ID 64位 第一位是符号位,一般是0
// 1-42位是毫秒时间戳位(41位)
// 5位工作中心, 最多可以表示32个数据中心
// 5位机器id,最多可以有32台机器
// 12位序号位,同一毫秒能生成4096个不同的ID

const (
	// 数据中心位数
	centerBits		uint8 = 5
	// 机器id位数
	workerBits		uint8 = 5
	// 开始毫秒时间戳 北京时间:2024-03-26 16:34:24
	epoch 			int64 = 1711442064000
	// 序号位数
	seqBits			uint8 = 12
	// 检测数据中心是否溢出
	centerMax		int64 = -1 ^ (-1 << centerBits)
	// 检测机器中心是否溢出
	workerMax		int64 = -1 ^ (-1 << workerBits)
	// 检测序号是否溢出
	seqMax			int64 = -1 ^ (-1 << seqBits)
	// 时间戳左移位数
	timeShift		uint8 = centerBits + workerBits + seqBits
	// 数据中心左移位数
	centerShift		uint8 = workerBits + seqBits
	// 机器id左移位数
	workerShift		uint8 = seqBits
)

type Worker struct {
	mu 			sync.Mutex
	// 开始毫秒时间戳 北京时间:2024-03-26 16:34:24
	epoch 			int64
	centerId		int64
	workerId		int64
	// 当前毫秒已经生成的序列号,从0开始累加
	seq 			int64
	// 上次生成的毫秒时间戳,用来检查时钟回退
	lastTimestamp	int64
}

// 返回一个Worker实例
func New(centerId int64, workerId int64) (*Worker, error) {
	if centerId < 0 || centerId > centerMax || workerId < 0 || workerId > workerMax {
		return nil, errors.New("Incorrect CenterId Or WorkerId")
	}
	return &Worker{
		workerId: workerId,
		centerId: centerId,
	}, nil
}

/**
	获取序列号
 */
func (this *Worker) NextId() (int64, error) {
	this.mu.Lock()
	defer this.mu.Unlock()

	currentTimestamp := time.Now().UnixMilli()
	if currentTimestamp < this.lastTimestamp {
		return -1, errors.New("Time Sync Error")
	}

	if currentTimestamp == this.lastTimestamp {
		this.seq = (this.seq + 1) & seqMax
		if this.seq == 0 {
			// 环状,超过了当前毫秒能够获取的最大序列号,那么就自旋等待下一个毫秒
			for currentTimestamp <= this.lastTimestamp {
				currentTimestamp = time.Now().UnixMilli()
			}
		}
	} else {
		// 当前时间和上一个毫秒数不一致, 返回0
		this.seq = 0
	}

	this.lastTimestamp = currentTimestamp
	return ((currentTimestamp - epoch) << timeShift) |
		(this.centerId << centerShift) |
		(this.workerId << workerShift) |
		this.seq, nil
}

接下来是单元测试,看看并发1万个请求是否会出现重复ID的问题

代码语言:go
复制
package SnowFlakeYYtest

import (
	"context"
	"testing"
)

func TestGetId(t *testing.T) {
	const cnt int = 10000
	worker, err := New(1,1)
	if err != nil {
		t.Fatalf("Init Worker Error")
		return
	}
	// 启动1万个协程同时访问
	hm := make(map[int64]struct{})
	ch := make(chan int64, cnt)
	defer close(ch)
	done := make(chan struct{})
	defer close(done)
	t.Logf("开始生成\n")
	go func() {
		i := 0
		for {
			select {
				case num := <- ch:
					i++
					if _,ok := hm[num]; ok {
						t.Errorf("SnowFlake Id is not unique !\n")
						done <- struct{}{}
						return
					}
					hm[num] = struct{}{}
					if i == cnt {
						t.Logf("取完%d次数据了,监控协程退出\n", cnt)
						done <- struct{}{}
						return
					}
			}
		}
	}()

	ctx,cancel := context.WithCancel(context.Background())
	for i := 0; i < cnt; i++ {
		go func(ctx context.Context) {
			select {
				case <-ctx.Done():
					t.Logf("有重复元素, 退出生成\n")
					return
				default:
					id, err := worker.NextId()
					if err != nil {
						t.Errorf("生成ID错误, %v\n", err)
						return
					}
					ch <- id
			}
		}(ctx)
	}

	<-done
	cancel()
	t.Logf("完成%d次请求, 没有重复ID\n", cnt)
}

可以看到,启动了一个监控协程进行检查,因为是单协程检查ID是否重复,所以对于Map的访问也不需要加锁,虽然这样检查一样也比较慢。如果监控协程检查到有重复元素就通知父协程退出,父协程再通过context上下文通知正在生成ID的子协程退出。

可以看到10000次请求没有重复ID,可以更改cnt的次数,即使修改成100万个协程并发请求也不会有重复ID的生成。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 分布式唯一ID生成器是业务上经常会需要的一个基础组件,它具有ID有序,且不重复的特点。现在主流的唯一ID生成器有4种方案。
  • 今天用Golang实现一个雪花ID生成器的组件,顺便加深对分布式唯一ID生成器的理解,也可以独立成一个服务,通过RPC请求将ID返回给对应的服务。具体的注释都在代码中,也可以针对并发不高的服务,增加毫秒时间戳的位数来保证长时间的不重复ID的稳定运行。如果是41位的时间戳表示,那么最多能运行69年的不重复ID生成服务。
  • 接下来是单元测试,看看并发1万个请求是否会出现重复ID的问题
  • 可以看到,启动了一个监控协程进行检查,因为是单协程检查ID是否重复,所以对于Map的访问也不需要加锁,虽然这样检查一样也比较慢。如果监控协程检查到有重复元素就通知父协程退出,父协程再通过context上下文通知正在生成ID的子协程退出。
  • 可以看到10000次请求没有重复ID,可以更改cnt的次数,即使修改成100万个协程并发请求也不会有重复ID的生成。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档