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 相关文章推荐
golang interface判断为空nil的实现代码
Apr 24 Golang
go原生库的中bytes.Buffer用法
Apr 25 Golang
golang正则之命名分组方式
Apr 25 Golang
golang中的空slice案例
Apr 27 Golang
golang goroutine顺序输出方式
Apr 29 Golang
golang 定时任务方面time.Sleep和time.Tick的优劣对比分析
May 05 Golang
Golang 实现获取当前函数名称和文件行号等操作
May 08 Golang
golang中字符串MD5生成方式总结
Jul 04 Golang
手把手教你导入Go语言第三方库
Aug 04 Golang
golang操作rocketmq的示例代码
Apr 06 Golang
Golang日志包的使用
Apr 20 Golang
详解Go语言中配置文件使用与日志配置
Jun 01 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
SONY ICF-SW07收音机电路分析
2021/03/02 无线电
js 完美图片新闻轮转效果,腾讯大粤网首页图片轮转改造而来
2011/11/21 Javascript
PHP使用方法重载实现动态创建属性的get和set方法
2014/11/17 Javascript
jQuery拖拽排序插件制作拖拽排序效果(附源码下载)
2016/02/23 Javascript
深入学习JavaScript的AngularJS框架中指令的使用方法
2016/03/05 Javascript
web打印小结
2017/01/11 Javascript
浅析Node.js非对称加密方法
2018/01/29 Javascript
JavaScript实现区块链
2018/03/14 Javascript
laypage+SpringMVC实现后端分页
2019/07/27 Javascript
js中位数不足自动补位扩展padLeft、padRight实现代码
2020/04/06 Javascript
javascript设计模式 ? 模板方法模式原理与用法实例分析
2020/04/23 Javascript
Vue实现背景更换颜色操作
2020/07/17 Javascript
JavaScript undefined及null区别实例解析
2020/07/21 Javascript
vue 实现一个简单的全局调用弹窗案例
2020/09/10 Javascript
关于Node.js中频繁修改代码重启服务器的问题
2020/10/15 Javascript
Unicode和Python的中文处理
2017/03/19 Python
Python入门_浅谈for循环、while循环
2017/05/16 Python
Numpy中stack(),hstack(),vstack()函数用法介绍及实例
2018/01/09 Python
Python不使用int()函数把字符串转换为数字的方法
2018/07/09 Python
对Python通过pypyodbc访问Access数据库的方法详解
2018/10/27 Python
CentOS 7 安装python3.7.1的方法及注意事项
2018/11/01 Python
Python中判断子串存在的性能比较及分析总结
2019/06/23 Python
Python类中方法getitem和getattr详解
2019/08/30 Python
基于Python 中函数的 收集参数 机制
2019/12/21 Python
python pandas利用fillna方法实现部分自动填充功能
2020/03/16 Python
jupyter notebook清除输出方式
2020/04/10 Python
CSS3 实现穿梭星空动画
2020/11/13 HTML / CSS
HTML5中的Article和Section元素认识及使用
2013/03/22 HTML / CSS
高级工程师英文求职信
2014/03/19 职场文书
合同协议书格式
2014/04/18 职场文书
酒店管理专业毕业生求职自荐信
2014/04/28 职场文书
党员教师个人对照检查材料范文
2014/09/25 职场文书
2015年派出所民警工作总结
2015/04/24 职场文书
企业党建工作总结2015
2015/05/26 职场文书
优秀大学生申请书
2019/06/24 职场文书
Python中Schedule模块使用详解 周期任务神器
2022/04/19 Python