Go语言实现Snowflake雪花算法


Posted in Golang onJune 08, 2021

每次放长假的在家里的时候,总想找点简单的例子来看看实现原理,这次我们来看看 Go 语言雪花算法。

介绍

有时候在业务中,需要使用一些唯一的ID,来记录我们某个数据的标识。最常用的无非以下几种:UUID、数据库自增主键、Redis的Incr命令等方法来获取一个唯一的值。下面我们分别说一下它们的优劣,以便引出我们的分布式雪花算法。

雪花算法

雪花算法的原始版本是scala版,用于生成分布式ID(纯数字,时间顺序),订单编号等。

自增ID:对于数据敏感场景不宜使用,且不适合于分布式场景。 GUID:采用无意义字符串,数据量增大时造成访问过慢,且不宜排序。

UUID

首先是 UUID ,它是由128位二进制组成,一般转换成十六进制,然后用String表示。为了保证 UUID 的唯一性,规范定义了包括网卡MAC地址、时间戳、名字空(Namespace)、随机或伪随机数、时序等元素,以及从这些元素生成 UUID 的算法。

UUID 有五个版本:

  • 版本1:基于时间戳和mac地址
  • 版本2:基于时间戳,mac地址和POSIX UID/GID
  • 版本3:基于MD5哈希算法
  • 版本4:基于随机数
  • 版本5:基于SHA-1哈希算法

UUID 的优缺点:

优点是代码简单,性能比较好。缺点是没有排序,无法保证按序递增;其次是太长了,存储数据库占用空间比较大,不利于检索和排序。

数据库自增主键

如果是使用 mysql 数据库,那么通过设置主键为 auto_increment 是最容易实现单调递增的唯一ID 的方法,并且它也方便排序和索引。

但是缺点也很明显,由于过度依赖数据库,那么受限于数据库的性能会导致并发性并不高;再来就是如果数据量太大那么会给分库分表会带来问题;并且如果数据库宕机了,那么这个功能是无法使用的。

Redis

Redis 目前已在很多项目中是一个不可或缺的存在,在 Redis 中有两个命令 Incr、IncrBy ,因为Redis是单线程的所以通过这两个指令可以能保证原子性从而达到生成唯一值的目标,并且性能也很好。

但是在 Redis 中,即使有 AOF 和 RDB ,但是依然会存在数据丢失,有可能会造成ID重复;再来就是需要依赖 Redis ,如果它不稳定,那么会影响 ID 生成。

Snowflake

通过上面的一个个分析,终于引出了我们的分布式雪花算法 Snowflake ,它最早是twitter内部使用的分布式环境下的唯一ID生成算法。在2014年开源。开源的版本由scala编写,大家可以再找个地址找到这版本。

https://github.com/twitter-archive/snowflake/tags

它有以下几个特点:

  • 能满足高并发分布式系统环境下ID不重复;
  • 基于时间戳,可以保证基本有序递增;
  • 不依赖于第三方的库或者中间件;

实现原理

Snowflake 结构是一个 64bit 的 int64 类型的数据。如下:

Go语言实现Snowflake雪花算法

 

位置 大小 作用
0~11bit 12bits 序列号,用来对同一个毫秒之内产生不同的ID,可记录4095个
12~21bit 10bits 10bit用来记录机器ID,总共可以记录1024台机器
22~62bit 41bits 用来记录时间戳,这里可以记录69年
63bit 1bit 符号位,不做处理

上面只是一个将 64bit 划分的通用标准,一般的情况可以根据自己的业务情况进行调整。例如目前业务只有机器10台左右预计未来会增加到三位数,并且需要进行多机房部署,QPS 几年之内会发展到百万。

那么对于百万 QPS 平分到 10 台机器上就是每台机器承担十万级的请求即可,12 bit 的序列号完全够用。对于未来会增加到三位数机器,并且需要多机房部署的需求我们仅需要将 10 bits 的 work id 进行拆分,分割 3 bits 来代表机房数共代表可以部署8个机房,其他 7bits 代表机器数代表每个机房可以部署128台机器。那么数据格式就会如下所示:

Go语言实现Snowflake雪花算法

代码实现

实现步骤

其实看懂了上面的数据结构之后,需要自己实现一个雪花算法是非常简单,步骤大致如下:

  • 获取当前的毫秒时间戳;
  • 用当前的毫秒时间戳和上次保存的时间戳进行比较;
    • 如果和上次保存的时间戳相等,那么对序列号 sequence 加一;
    • 如果不相等,那么直接设置 sequence 为 0 即可;
  • 然后通过或运算拼接雪花算法需要返回的 int64 返回值。

代码实现

首先我们需要定义一个 Snowflake 结构体:

type Snowflake struct {
 sync.Mutex     // 锁
 timestamp    int64 // 时间戳 ,毫秒
 workerid     int64  // 工作节点
 datacenterid int64 // 数据中心机房id
 sequence     int64 // 序列号
}

然后我们需要定义一些常量,方便我们在使用雪花算法的时候进行位运算取值:

const (
  epoch             = int64(1577808000000)                           // 设置起始时间(时间戳/毫秒):2020-01-01 00:00:00,有效期69年
 timestampBits     = uint(41)                                       // 时间戳占用位数
 datacenteridBits  = uint(2)                                        // 数据中心id所占位数
 workeridBits      = uint(7)                                        // 机器id所占位数
 sequenceBits      = uint(12)                                       // 序列所占的位数
 timestampMax      = int64(-1 ^ (-1 << timestampBits))              // 时间戳最大值
 datacenteridMax   = int64(-1 ^ (-1 << datacenteridBits))           // 支持的最大数据中心id数量
 workeridMax       = int64(-1 ^ (-1 << workeridBits))               // 支持的最大机器id数量
 sequenceMask      = int64(-1 ^ (-1 << sequenceBits))               // 支持的最大序列id数量
 workeridShift     = sequenceBits                                   // 机器id左移位数
 datacenteridShift = sequenceBits + workeridBits                    // 数据中心id左移位数
 timestampShift    = sequenceBits + workeridBits + datacenteridBits // 时间戳左移位数
)

需要注意的是由于 -1 在二进制上表示是:

11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111

所以想要求得 41bits 的 timestamp 最大值可以将 -1 向左位移 41 位,得到:

11111111 11111111 11111110 00000000 00000000 00000000 00000000 00000000

那么再和 -1 进行 ^异或运算:

00000000 00000000 00000001 11111111 11111111 11111111 11111111 11111111

这就可以表示 41bits 的 timestamp 最大值。datacenteridMax、workeridMax也同理。

接着我们就可以设置一个 NextVal 函数来获取 Snowflake 返回的 ID 了:

func (s *Snowflake) NextVal() int64 {
 s.Lock()
 now := time.Now().UnixNano() / 1000000 // 转毫秒
 if s.timestamp == now {
  // 当同一时间戳(精度:毫秒)下多次生成id会增加序列号
  s.sequence = (s.sequence + 1) & sequenceMask
  if s.sequence == 0 {
   // 如果当前序列超出12bit长度,则需要等待下一毫秒
   // 下一毫秒将使用sequence:0
   for now <= s.timestamp {
    now = time.Now().UnixNano() / 1000000
   }
  }
 } else {
  // 不同时间戳(精度:毫秒)下直接使用序列号:0
  s.sequence = 0
 }
 t := now - epoch
 if t > timestampMax {
  s.Unlock()
  glog.Errorf("epoch must be between 0 and %d", timestampMax-1)
  return 0
 }
 s.timestamp = now
 r := int64((t)<<timestampShift | (s.datacenterid << datacenteridShift) | (s.workerid << workeridShift) | (s.sequence))
 s.Unlock()
 return r
}

上面的代码也是非常的简单,看看注释应该也能懂,我这里说说最后返回的 r 系列的位运算表示什么意思。

首先 t 表示的是现在距离 epoch 的时间差,我们 epoch 在初始化的时候设置的是2020-01-01 00:00:00,那么对于 41bit 的 timestamp 来说会在 69 年之后才溢出。对 t 进行向左位移之后,低于 timestampShift 位置上全是0 ,由 datacenterid、workerid、sequence 进行取或填充。

在当前的例子中,如果当前时间是2021/01/01 00:00:00,那么位运算结果如下图所示:

Go语言实现Snowflake雪花算法

到此这篇关于Go语言实现Snowflake雪花算法的文章就介绍到这了,更多相关Go语言雪花算法内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Golang 相关文章推荐
一文读懂go中semaphore(信号量)源码
Apr 03 Golang
go 原生http web 服务跨域restful api的写法介绍
Apr 27 Golang
解决golang结构体tag编译错误的问题
May 02 Golang
golang 定时任务方面time.Sleep和time.Tick的优劣对比分析
May 05 Golang
GoLang中生成UUID唯一标识的实现
May 08 Golang
Golang中异常处理机制详解
Jun 08 Golang
Go语言基础知识点介绍
Jul 04 Golang
golang fmt格式“占位符”的实例用法详解
Jul 04 Golang
Go 通过结构struct实现接口interface的问题
Oct 05 Golang
Golang日志包的使用
Apr 20 Golang
Golang map映射的用法
Apr 22 Golang
在ubuntu下安装go开发环境的全过程
Aug 05 Golang
go语言中http超时引发的事故解决
Jun 02 #Golang
Golang二维数组的使用方式
May 28 #Golang
Golang标准库syscall详解(什么是系统调用)
May 25 #Golang
go 实现简易端口扫描的示例
May 22 #Golang
go xorm框架的使用
May 22 #Golang
Golang实现AES对称加密的过程详解
May 20 #Golang
go语言基础 seek光标位置os包的使用
May 09 #Golang
You might like
解决dede生成静态页和动态页转换的一些问题,及火车采集入库生成动态的办法
2007/03/29 PHP
php页面防重复提交方法总结
2013/11/25 PHP
用php+ajax新建流程(请假、进货、出货等)
2017/06/11 PHP
JavaScript(JS) 压缩 / 混淆 / 格式化 批处理工具
2010/12/10 Javascript
js不完美解决click和dblclick事件冲突问题
2012/07/16 Javascript
jQuery实现类似滑动门切换效果的层切换
2013/09/23 Javascript
jQuery动态修改超链接地址的方法
2015/02/13 Javascript
JQuery实现的图文自动轮播效果插件
2015/06/19 Javascript
Angularjs 自定义服务的三种方式(推荐)
2016/08/02 Javascript
jQuery中实现prop()函数控制多选框(全选,反选)
2016/08/19 Javascript
D3.js实现柱状图的方法详解
2016/09/21 Javascript
基于JavaScript实现右键菜单和拖拽功能
2016/11/28 Javascript
jQuery发请求传输中文参数乱码问题的解决方案
2018/05/22 jQuery
如何使node也支持从url加载一个module详解
2018/06/05 Javascript
深入理解Angularjs 脏值检测
2018/10/12 Javascript
微信上传视频文件提示(推荐)
2018/11/22 Javascript
Vue实现简单的跑马灯
2020/05/25 Javascript
[51:29]Alliance vs TNC 2019国际邀请赛小组赛 BO2 第二场 8.16
2019/08/18 DOTA
简单介绍Python中用于求最小值的min()方法
2015/05/15 Python
Python 爬虫多线程详解及实例代码
2016/10/08 Python
pandas中Timestamp类用法详解
2017/12/11 Python
使用python进行拆分大文件的方法
2018/12/10 Python
Python整数对象实现原理详解
2019/07/01 Python
如何在python中执行另一个py文件
2020/04/30 Python
Python使用socketServer包搭建简易服务器过程详解
2020/06/12 Python
css3实现信纸/同学录效果的示例代码
2018/12/11 HTML / CSS
HTML5 新旧语法标记对我们有什么好处
2012/12/13 HTML / CSS
大专应届生个人的自我评价
2013/11/21 职场文书
教师推荐信范文
2013/11/24 职场文书
酒店总经理职务说明书
2014/02/26 职场文书
内蒙古鄂尔多斯市市长寄语
2014/04/10 职场文书
绿色家庭事迹材料
2014/05/01 职场文书
资产移交协议书
2016/03/24 职场文书
SQL实现LeetCode(178.分数排行)
2021/08/04 MySQL
Django框架中视图的用法
2022/06/10 Python
Java完整实现记事本代码
2022/06/16 Java/Android