go语言实战_Go实战 Twitter 的分布式 ID 生成算法 snowflake 的 Go 语言实现

snowflake 是什么

snowflake 单词原意为雪花,是 twitter 开源的一种分布式 ID 生成算法。该算法可以保证在不借助第三方服务或者说工具的情况下,在多台机器上持续生成保证唯一性的 64 位整型 ID。

snowflake ID 的组成

snowflake 将 ID 的 64 位划分为四块区域,分别填入的是当前服务实例的数据中心 ID,节点 ID,时间戳,以及相同时间戳下的递增序号。

以下是 snowflake 原始算法中,各字段的顺序以及所占的位数:

go语言实战_Go实战 Twitter 的分布式 ID 生成算法 snowflake 的 Go 语言实现_第1张图片

ID组成图

小提示,在生产环境,使用 snowflake 的思想生成分布式 ID 时,这些字段所占的位数可以根据自身业务灵活调整。本文如无特殊说明,都是针对 snowflake 原始算法进行讲解。

数据中心 ID 和节点 ID

不同的服务实例间,使用不同的数据中心 ID 和节点 ID,可以保证生成的 ID 不重复。

从该算法使用数据中心 ID 加节点 ID(而不是单个服务实例 ID)的方式决定服务实例的唯一性,也可以看出,该算法设计之初是一个非常面向具体业务场景的算法。

数据中心 ID 和节点 ID 分别占 5 位。即整个系统最多可以有 32 个数据中心,每个数据中心最多可以有 32 个节点。为什么数据中心 ID 和节点 ID 是 5 位?额,应该也是 twitter 根据当时的业务拍脑袋拍的。

那么数据中心 ID 和节点 ID 怎么生成呢?这点不由 snowflake 负责,也即这两个 ID 在多个服务实例之间不重复是由 snowflake 的调用方保证的。比如一种简单的方式是直接使用静态配置,这种方式适用于节点数量比较少且相对固定的情况下。

时间戳

如前文图中展示,snowflake 使用 42 位存储时间戳。

我们平时所说的 UNIX 时间戳,指的是从 1970 年 1 月 1 号 0 点到现在所经过的时间。如果单位是毫秒,需要使用 64 位整型存储。

由于在 snowflake 算法中,只需要保证时间戳随时间递增即可,所以 snowflake 中的时间戳是使用 UNIX 时间戳减去一个基准时间,这个基准时间在原版代码中是 1288834974657,也即2010/11/4 9:42:54.657。你也可以换种方式理解,即从 2010 年 11 月 4 号 xxx 这个基准时间点到现在所经过的时间。

减去自定义的基准时间后,时间戳比 UNIX 时间戳小了很多,snowflake 使用得到的时间戳的低 42 位作为 ID 的一部分(即 ID 的高 42 位)。如下图所示:

go语言实战_Go实战 Twitter 的分布式 ID 生成算法 snowflake 的 Go 语言实现_第2张图片

时间戳取值图

时间戳可能存在的问题

首先,时间戳随着系统时间增长,当时间戳的第 42 位增长到 1 时,ID 的最高位也将变为 1。由于 snowflake 默认使用有符号 64 位整型,最高位为符号位,这将导致生成的 ID 变为负数。

通过如下公式(1<<41) / 1000 / 60 / 60 / 24 / 365计算,可知道在基准时间上(snowflake 默认的基准时间是 2010/11/4)再过 69.7 年,将出现 ID 为负数的情况。

这里展开说明下,如果想要生成的 ID 永远不为负数,可以保持 ID 的最高位始终为 0,其他的字段减少 1 位,比如说时间戳只使用 41 位。这样时间戳出现翻转归零的时长缩短 1 倍,大概为 35 年,基本上是可接受的。你可能会说,69.7 年已经足够长啦,到出现负数的时候我早退休了,哪管它洪水滔天。但是假设你要设计一个 32 位的分布式 ID 生成器呢?此时你必然需要考虑哪些字段可以缩短,时间戳多久出现翻转(也即 ID 可能翻转)不影响业务。

第二,假如单台机器上,获取当前时间的方法出现时间回退,那么可能出现 ID 重复的情况。

第三,假如服务重启,重启后时间戳没变(即 1 毫秒内重启成功),那么此时 snowflake 丢失了重启前当前时间戳的递增序号,递增序号重新从 0 开始,也可能出现和重启前生成的 ID 重复的情况。

最后一点值得注意的是,一个服务上线后,基准时间点不应该随意修改。避免造成时间戳回退。

递增序号

如前文图中展示,snowflake 使用 12 位存储递增序号。

为什么在时间戳的基础之上,还需要递增序号?因为即使是在单个服务实例中,我们也可能需要在 1 毫秒内生成多个 ID,这种情况下,同 1 毫秒下的 seq 会从 0 开始顺序递增。12 位的 seq 范围为[0, 4095],如果 1 毫秒内生成的 ID 数量超过 4096 怎么办呢,snowflake 会阻塞直到时间戳更新后,再生成可用的 ID 返回给调用方。相关代码如下:

now = time.Now().UnixNano() / 1e6// 时间戳回退,返回错误if now < n.lastTs {return -1, ErrGen}// 时间戳相同时,使用递增序号解决冲突if now == n.lastTs {n.seq = (n.seq + 1) & n.seqMask// 递增序号翻转为 0,表示该时间戳下的序号已经全部用完,阻塞等待系统时间增长if n.seq == 0 {for now <= n.lastTs {now = time.Now().UnixNano() / 1e6}}} else {n.seq = 0}n.lastTs = now

完整代码见:https://github.com/q191201771/naza/blob/master/pkg/snowflake/snowflake.go

snowflake 1 毫秒可产生多少 ID?

32(5位数据中心ID) * 32(5位节点ID) * 4096(递增序号) = 4194304

所生成 ID 的特点

单个服务实例生成的 ID 是递增的;多个服务实例生成的 ID,受限于系统时间不一致,或者是节点 ID 的大小,可能出现整体 ID 不递增的情况。

总结

好了,至此,snowflake 算法就基本介绍完毕了。算法的实现部分十分简单,不到 100 行代码,基本上就是一些位操作。

感兴趣的可以看看我写的一份 Go 语言实现:snowflake.go[1],在 twitter 原始 scala 实现版本的基础上,额外支持配置所有的字段所占的位数(比如支持将数据中心 ID 所占位数设置为 0,从而只使用节点 ID),支持配置基准时间,支持配置是否永远不返回负数 ID,支持并发调用。

twitter 放在 github 上的原始版本,master 分支的代码已经删了,只能在 release 下载老版本的源码压缩包。

最后,感谢阅读,如果觉得文章还不错,可以给我的 github 项目naza[2]来个 star 哈。该项目是我学习 Go 时写的一些轮子代码集合,后续我还会写一些文章逐个介绍里面的轮子以及一些写 Go 代码的技巧。

naza 项目地址:https://github.com/q191201771/naza

naza 的其他的文章:

  • Go 创建对象时,如何优雅的传递初始化参数[3]
  • 给 Go 程序加入编译版本时间等信息[4]

参考资料

[1]

snowflake.go: https://github.com/q191201771/naza/blob/master/pkg/snowflake/snowflake.go

[2]

naza: https://github.com/q191201771/naza

[3]

Go创建对象时,如何优雅的传递初始化参数: http://127.0.0.1:4000/p/60015/

[4]

给Go程序加入编译版本时间等信息: https://pengrl.com/p/37397/

你可能感兴趣的:(go语言实战,雪花算法,多少位,雪花算法出来多少位,雪花算法生成id)