原文链接

前言

嗨,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,......40944095个数字,示意同一机器同一时间戳(毫秒)内产生的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库进行数据校验,开了~~~