原文链接
前言
嗨,everybody,我是asong,这是我的第十二篇文章,明天给大家介绍一下雪花算法。介绍雪花算法是主要的,因为大家都太相熟了,次要目标是举荐一下我的新系列。明天,我突发奇想,想创立一个新系列。这个系列次要是存储咱们日常工作开发中应用的算法,比方雪花算法、哈希算法等等。网络上对于这些算法的常识还是很多的,然而很杂,想找一个算法很不容易,还要看各种各样的博客,形形色色的。所以咱们当初的想法就是想将这些算法整顿到一起,全副采纳go
实现,并附带基础知识学习,代码阐明这样的具体文档,这样咱们想要学习一门新算法时,都能够在这里找到,如果没有也能够提交issues
一块欠缺。目前也是刚刚起步,当初只有雪花算法的代码文档,当前会缓缓欠缺起来的,有趣味的小伙伴欢送退出进来,咱们一起欠缺。详情请看github阐明:https://github.com/asong2020/...。
雪花算法背景
雪花算法产生的背景当然是twitter
高并发环境下对惟一ID生成的需要,得益于twitter
外部牛逼的技术,雪花算法可能流传于至今并且被宽泛应用,是因为它有几个特点
- 能满足高并发分布式系统环境下ID不反复
- 生成效率高
- 基于工夫戳,能够保障根本有序递增
- 不依赖于第三方的库或者中间件
- 生成的
id
具备时序性和唯一性
雪花算法原理
先来看一个图片吧,来源于网络:
由图咱们能够看进去,snowFlake ID
构造是一个64bit的int
型数据。
- 第1位bit
在二进制中最高位为1,示意的是正数,因为咱们应用的id
应该都是整数,所以这里最高位应该是0。
- 41bit工夫戳
41位能够示意2^41-1
个数字,如果只用来示意正整数,能够示意的数值范畴是:0 - (2^41 -1),这里减去1的起因就是因为数值范畴是从0开始计算的,而不是从1开始的。
这里的单位是毫秒,所以41位就能够示意2^41-1
个毫秒值,这样转化成单位年则是 (2^41-1)/(1000 * 60 * 60 * 24 * 365) = 69
- 10bit-工作机器id
这里是用来记录工作机器的id。2^10=1024
示意以后规定容许分布式最大节点数为1024个节点。这里包含5位的workerID
和5位的dataCenterID
,这里其实能够不辨别,但我上面的代码进行了辨别。
- 12bit-序列号
用来记录同毫秒内产生的不同id。12bit能够示意的最大正整数是2^12-1=4095
,即能够用0,1,2,3,......4094
这4095
个数字,示意同一机器同一时间戳(毫秒)内产生的4095个ID序号。
原理就是下面这些,没有什么难度吧,上面咱们看代码如何实现:
go实现雪花算法
1. 定义根本常量
先看代码,咱们顺次介绍:
const ( workerIDBits = uint64(5) // 10bit 工作机器ID中的 5bit workerID dataCenterIDBits = uint64(5) // 10 bit 工作机器ID中的 5bit dataCenterID sequenceBits = uint64(12) maxWorkerID = int64(-1) ^ (int64(-1) << workerIDBits) //节点ID的最大值 用于避免溢出 maxDataCenterID = int64(-1) ^ (int64(-1) << dataCenterIDBits) maxSequence = int64(-1) ^ (int64(-1) << sequenceBits) timeLeft = uint8(22) // timeLeft = workerIDBits + sequenceBits // 工夫戳向左偏移量 dataLeft = uint8(17) // dataLeft = dataCenterIDBits + sequenceBits workLeft = uint8(12) // workLeft = sequenceBits // 节点IDx向左偏移量 // 2020-05-20 08:00:00 +0800 CST twepoch = int64(1589923200000) // 常量工夫戳(毫秒))
上面对这段代码的每一个常量进行解释:
worerIDBits
:这里就是对应上图中的10bit-工作机器id,我这里进行拆分了。这是其中5bit`worerIDBits
dataCenterIDBits
:这里就是对应上图中的10bit-工作机器id,我这里进行拆分了。这是其中5bitdataCenterIDBits
sequenceBits
:对应上图中的12bit的序列号maxWorkerID
:这里就是求最大,只不过咱们采纳了异或的形式,因为-1的二进制示意为1的补码,说艰深一点,这里其实就是2^5-1
,还不懂的同学,能够本人验证一下maxDataCenterID
:原理同上maxSequence
:原理同上timeLeft
:工夫戳向左偏移量,这么你们可能不懂,看下面的图片,由有向左的偏移量是不是22,这么说你们懂了吗?dataLeft
:原理同上,也是求偏移量的workLeft
:原理同上;twepoch
:41bit的工夫戳,单位是毫秒,这里我抉择的工夫是2020-05-20 08:00:00 +0800 CST
,这个ID一但生成就不要改了,要不会生成雷同的ID。
2. 定义worker
工作节点
因为这个是在分布式下应用的ID生成算法,所以咱们要生成多个worker
,所以要把节点参数形象进去。
type Worker struct { mu sync.Mutex LastStamp int64 // 记录上一次ID的工夫戳 WorkerID int64 // 该节点的ID DataCenterID int64 // 该节点的 数据中心ID Sequence int64 // 以后毫秒曾经生成的ID序列号(从0 开始累加) 1毫秒内最多生成4096个ID}
代码解释:
mu sync.Mutex
:增加互斥锁,确保并发安全性LastStamp int64
:记录上一次生成ID的工夫戳WorkerID int64
:该工作节点的ID 对上图中的5bit workerID 一个意思DataCenterID int64
: 原理同上Sequence int64
:以后毫秒曾经生成的id序列号(从0开始累加) 1毫秒内最多生成4096个ID
3. 创立一个worker
对象
//分布式状况下,咱们应通过内部配置文件或其余形式为每台机器调配独立的idfunc NewWorker(workerID,dataCenterID int64) *Worker { return &Worker{ WorkerID: workerID, LastStamp: 0, Sequence: 0, DataCenterID: dataCenterID, }}
这里没有什么好解释的~~~。
4. 生成id
先看代码:
func (w *Worker) getMilliSeconds() int64 { return time.Now().UnixNano() / 1e6}func (w *Worker)NextID() (uint64,error) { w.mu.Lock() defer w.mu.Unlock() return w.nextID()}func (w *Worker)nextID() (uint64,error) { timeStamp := w.getMilliSeconds() if timeStamp < w.LastStamp{ return 0,errors.New("time is moving backwards,waiting until") } if w.LastStamp == timeStamp{ w.Sequence = (w.Sequence + 1) & maxSequence if w.Sequence == 0 { for timeStamp <= w.LastStamp { timeStamp = w.getMilliSeconds() } } }else { w.Sequence = 0 } w.LastStamp = timeStamp id := ((timeStamp - twepoch) << timeLeft) | (w.DataCenterID << dataLeft) | (w.WorkerID << workLeft) | w.Sequence return uint64(id),nil}
代码有点长,我先来顺次解释一下:
getMilliSeconds()
:封装的一个办法,用来获取以后的毫秒值func (w *Worker)NextID() (uint64,error)
这个代码的内容没有什么,具体生成ID算法封装在func (w *Worker)nextID() (uint64,error)
这里了,这里不过是为理解藕作用;对了还有一个特地重要的中央,加锁、开释锁,这个步骤很重要。
func (w *Worker)nextID() (uint64,error)
这里是真正的生成id代码了。分为几个步骤:
- 获取以后工夫戳,进行判断,要确保以后工夫戳值大于上一次生成ID的工夫戳,否则会呈现反复。
- 如果想等了,首先获取以后的以后毫秒曾经生成的id序列号。这里你们可能没看懂,其实他等效于
if w.sequence++ > maxSequence
, 如果以后毫秒曾经生成的id序列号溢出了,则须要期待下一毫秒,如果不期待,就会导致很多反复。
- 咱们在else里将
w.sequence
置零了,这里解释一下,如果以后工夫与工作节点上一次生成ID的工夫不统一 则须要重置工作节点生成ID的序号。 - 最初一步,比拟重要,采纳了或运算,这里的目标就是各局部的bit进行归位并通过按位或运算(就是这个‘|’)将其整合。
<<
这个就是向左偏移的作用进行归位,而|
运算就是为了整合。可能大家还没懂,看上面这一张图片吧:
怎么样,是不是晓得什么意思了。
5. 测试
写好了代码,咱们就来测试一下吧,这里我并发10000个goroutine
进行生成ID,存入到map,查看是否呈现反复,来看代码:
var wg sync.WaitGroupfunc main() { w := idgen.NewWorker(5,5) ch := make(chan uint64,10000) count := 10000 wg.Add(count) defer close(ch) //并发 count个goroutine 进行 snowFlake ID 生成 for i:=0 ; i < count ; i++ { go func() { defer wg.Done() id,_ := w.NextID() ch <- id }() } wg.Wait() m := make(map[uint64]int) for i := 0; i < count; i++ { id := <- ch // 如果 map 中存在为 id 的 key, 阐明生成的 snowflake ID 有反复 _, ok := m[id] if ok { fmt.Printf("repeat id %d\n",id) return } // 将 id 作为 key 存入 map m[id] = i } // 胜利生成 snowflake ID fmt.Println("All", len(m), "snowflake ID Get successed!")}
验证后果:
All 10000 snowflake ID Get successed!
总结
好啦,这一篇文章到这里完结啦,雪花算法并没有咱们设想的那么难,还是很好实现的,你学会了吗? 没学会不要紧,下载源代码,再看一看,对你不是问题的~~~。
GitHub:https://github.com/asong2020/... 欢送star 感激各位~~~
敌人们,还记得我结尾说的那些吗?有趣味的欢送退出进来呦,咱们一起提高~~~。
结尾给大家发一个小福利吧,最近我在看[微服务架构设计模式]这一本书,讲的很好,本人也收集了一本PDF,有须要的小伙能够到自行下载。获取形式:关注公众号:[Golang梦工厂],后盾回复:[微服务],即可获取。
我翻译了一份GIN中文文档,会定期进行保护,有须要的小伙伴后盾回复[gin]即可下载。
我是asong,一名普普通通的程序猿,让我一起缓缓变强吧。我本人建了一个golang
交换群,有须要的小伙伴加我vx
,我拉你入群。欢送各位的关注,咱们下期见~~~
举荐往期文章:
- 详解Context包,看这一篇就够了!!!
- go-ElasticSearch入门看这一篇就够了(一)
- 面试官:go中for-range应用过吗?这几个问题你能解释一下起因吗
- 学会wire依赖注入、cron定时工作其实就这么简略!
- 据说你还不会jwt和swagger-饭我都不吃了带着实际我的项目我就来了
- 把握这些Go语言个性,你的程度将进步N个品位(二)
- go实现多人聊天室,在这里你想聊什么都能够的啦!!!
- grpc实际-学会grpc就是这么简略
- go规范库rpc实际
- 2020最新Gin框架中文文档 asong又捡起来了英语,用心翻译
- 基于gin的几种热加载形式
- boss: 这小子还不会应用validator库进行数据校验,开了~~~