道阻且长,行则将至。请相信我,你一定会更优秀!

「 人人都听过的雪花算法,你真的推敲过吗?」

很多人听过或用过雪花算法,可是到底一个ID是如何计算出来的,没几个讲明白。别上来就跟我聊时钟回拨的问题,貌似你很懂一样,首先这是个算法,你先给我写个代码我看看?大牛请绕道,文章还是比较基础的。

技术人要对自己写的每一行代码负责,静下心来,我带你吃透雪花算法。关于分布式ID的文章有很多,你可以自己去查阅,此处我们不再赘述,本文只聊雪花算法。


目录

一、为啥叫雪花?

二、技术设计点剖析

1、技术背景

a. 我们希望一个id有以下2个特性足以:

b. 我们希望id服务有以下几个特点:

c. 综上,雪花算法的id由以下部分组成,完美匹配我们想要的规则:

d. 为啥不用纳秒?纳秒不是更精确吗,更不容易冲突吗?

2、twepoch 这个是干嘛的?

a. 那这个时间起啥作用?我们用的时候,这个时间写什么呢?

3、假如同一毫秒内,12个bit位的序列自增用完了,咋办?

4、如何判断序列号用完了?比大小吗?

5、计算 id的移位操作,是如何移位的?

6、生成的 id是单调递增、趋势递增的,会出现第2次拿到的比第1次拿到的id小吗?

7、生成 id的长度有多长?

8、这个 id的位数分配规则,自己能调整吗?

9、workerId怎么计算的?

三、安全问题

1、时钟回拨,即时间走着走着却倒回去了,如何解决?

2、新起的节点中,机器时间不对,那就压根不能提供服务,如何校正?

3、业务上,通过 getById 接口遍历能拿走多少越权数据?

4、友商是否可以解析你的 id大概计算出你的业务量?


一、为啥叫雪花?

世界上没有两片完全相同的雪花。任何一个技术设计,都值得有一个代号,都值得有自己的 slogan 并为之努力让它产生价值。

二、技术设计点剖析

雪花算法的源代码或者优秀代码示例有很多,但思想是一样的。我们这里不再一行行写代码,把里边的关键代码拿出来解析即可。这里提供一个代码示例:GitHub - Meituan-Dianping/Leaf: Distributed ID Generate Service,是美团的雪花算法代码实现(个人认为,美团开源出的代码真的很糟糕,内部一定是有另外一套,这个代码只是供大家参考的,不过不影响核心逻辑)。不管你有没有读过雪花算法代码,关键的代码我会在下边一个个剖析,读懂这些关键点,你就掌握了雪花算法。

1、技术背景

a. 我们希望一个id有以下2个特性足以:

  • 纯正整数,占用内存小
  • 递增,后一个大于前一个,且多节点时依然这样

第1条,纯数字好说,十进制,不出现字母就行。表中用 bigint表示,对应 java中用 long表示即可(均占用8个byte,64个bit);

第2条,递增,那我们就想,什么是递增的?时间呀,时间天然是递增的;那多个节点在同一时间不就生成了相同的id了?再加上每个节点的唯一标识;同一时间内有多个请求咋办?再加上个序列号。就这点事。

b. 我们希望id服务有以下几个特点:

  • 速度快,并发高
  • 支持集群高可用HA
  • 对业务系统无侵入,与业务系统松耦合

第1条,速度快:纯内存操作就可以,1s出100万,够用吧

第2条,可集群部署,且id也是递增状态,且单节点故障不影响集群HA

第3条,与业务系统松耦合,遵循业务各服务间调用方式即可,如业务系统服务调用使用Dubbo,此id服务也是Dubbo服务提供出去就好

c. 综上,雪花算法的id由以下部分组成,完美匹配我们想要的规则:

from: 美团leaf > 雪花算法(图中序列化应为12-bit)

分析:一共 64个bit,占用 8个字节,MySQL中是 bigint表示,java中是 long表示。注意是64个bit运算表示出一个数字,而不是几个段的数字用字符串拼接起来!

1个bit:第0位,符号位,0只要正数41个bit:毫秒时间戳的数字,注意,和时间本身没有任何关系,只是借助了时间戳的数字。要理解清楚,很重要10个bit:集群部署的话,为避免不同节点生成出相同的id,给每个节点一个唯一的数字标识12个bit:同1毫秒内,同一节点,需要多个id的话,用序列号自增

记住上边的位数分配,下边会逐个解析。

d. 为啥不用纳秒?纳秒不是更精确吗,更不容易冲突吗?

  1. 你的业务并发需要纳秒级别的吗?1ms=1,000,000ns 我的业务并发连毫秒都到不了,然后我这里花时间、花内存去计算/存储纳秒?再说了,CPU主频2G的时钟周期算下来才 0.5ns,你上层建筑空想啥呢?毫无意义!
  2. 我们一共只有64个bit,要做好几件事情呢,合理分配,省着点,留着bit给其他段用吧;
  3. 所以,用毫秒ms时间戳表示足以;

2、twepoch 这个是干嘛的?

SnowflakeIDGenImpl#SnowflakeIDGenImpl

注意这行注释:

//Thu Nov 04 2010 09:42:54 GMT+0800 (中国标准时间)

首先要清楚,这个时间对于我们来说,不是什么特殊的时间点,对我们来讲没有意义,或许是美团内部开始用雪花算法的时间点,我们不用研究这个时间。

a. 那这个时间起啥作用?我们用的时候,这个时间写什么呢?

上边说过用 41个bit表示时间戳,如果41个bit都占满,能表示的最大数字是:2199023255551,也就是 2199023255551个毫秒,这么多毫秒换算下来是大概 69年。

2199023255551 / 1000 / 60 / 60 / 24 / 365 ≈ 69
毫秒          / 秒   /分钟/ 小时 / 天 /  年

也就是说,从这 41个bit都是0开始计算,到 41个bit都用满,需要 69年的毫秒时间戳才能填满。

可关键是不可能是从0开始呀,当前、现在的时间戳已经是一个很大的数字,那要是从现在开始算的话,能用多少年呢?(从0开始算只是理论值而已)

ok,继续看代码:

long id = ((timestamp - twepoch) << timestampLeftShift) | (workerId << workerIdShift) | sequence;

为啥要减去 twepoch呢?(timestamp - twepoch),如果不减这个 twepoch,问题就出现了,我们的 69年算的是从 0开始计算,但是假如我们是 2021-05-05才开始用的雪花算法,也就是说我们第一次使用的时候,就已经从 1620179006000时间戳开始了,不是从 0开始的,这个数字会浪费掉我的 41bit能表示的数据集中的一大部分,所以,必须要减,因为我才今天刚开始用,要从我的投产时间开始算的。如果不减我们会浪费掉多少呢?

1620179006000 / 1000 / 60 / 60 / 24 / 365 ≈ 51
毫秒          / 秒   /分钟/ 小时 / 天 /  年

也就是说,本来我的 41个bit够我用 69年,但是刚一投产,就耗掉了51年的,剩下的只够我用 69-51=18年!

那么我们的这个 twepoch设置为多少呢?

比方说我们是 2021-05-05上线,虽然是我们第一次使用的时间,但是我们不能这么设置,因为我们有业务历史数据,如果这个时间设置的不对,一定会遇到和现有id冲突的情况!可以设置一个公司成立或者业务线开始的时间,现在的时间和指定的时间点的差集耗掉的数字,我们认了。如果你是全新的开始,那完全可以设置为投产时间即可。

3、假如同一毫秒内,12个bit位的序列自增用完了,咋办?

12个bit能表示的最大数字是 4095,一共 4096个 [0, 4095]。

首先这个计算是在单个 JVM中计算的,按照美团的位数分配,即同一毫秒同一个worker最大能生成 4096个id,如果同一毫秒内,用完了,就等待到下一毫秒。

根据这个情况我们可以推理出,我们的批量获取的接口中,一定要加上数量限制,如果不限制,如果业务端传过来的是 Integer.MAX_VALUE,那就是说这一次调用需要等待的时间大概为:

2147483647 / 4096 ≈ 524287ms ≈ 524s ≈ 8.7min

忽略调用时超时时间的问题,单纯说这个事情,CPU打满8分钟... 我这里建议批量接口中数量参数不要超过1000,甚至是100。什么样的业务场景中会一下子需要这么多id?业务上能处理过来吗?自己根据情况权衡下。

4、如何判断序列号用完了?比大小吗?

SnowflakeIDGenImpl#get

注意这行代码,很有意思:

sequence = (sequence + 1) & sequenceMask;

sequenceMask为序列号的最大值,也就是序列号的 bit位都占满,都是1。那 sequence的值在正常增长情况下,任何一个 bit &1,都是1,怎么可能会出现 if (sequence == 0) 这个情况呢?

只有一种情况,就是超出了 bit位,这些 bit位已经装不下了,进位了。后面全是 0,所以 &出来结果成 0了。

5、计算 id的移位操作,是如何移位的?

long id = ((timestamp - twepoch) << timestampLeftShift) | (workerId << workerIdShift) | sequence;
  • (timestamp - twepoch) 是时间戳计算值,是long类型,64个bit。timestampLeftShift 是 12+10=22
  • (timestamp - twepoch) << timestampLeftShift) 意思是将这 64个bit 左移22位,也就是原值的后 42个bit留下为有效的,这步完成后后面还有22个bit可用;
  • (workerId << workerIdShift) 同理,这个是将 workerId 左移12位,12位为sequence的bit数,这步完成后后面还有12个bit可用,供sequence使用,sequence在末位,不用移位;
  • ((timestamp - twepoch) << timestampLeftShift) | (workerId << workerIdShift)  | sequence 或运算,将时间戳和 workerID和sequence 的bit位进行计算、整合;
  • 最后64个bit都整合完毕,为64个bit,8个byte

这个过程一定要搞明白,如果搞不明白先去复习下二进制运算,这个搞明白后可以帮你在业务上做一些别的事情的,比如 Zookeeper客户端的 auth位、redis的 bitmap等等。

6、生成的 id是单调递增、趋势递增的,会出现第2次拿到的比第1次拿到的id小吗?

基本不用考虑这个问题,生成的id是单调递增、趋势递增的。在极高并发的情况下,比如有两个workerID,1和2,前提是同一毫秒,先去2拿到的是大值,再去1拿到的是小值。这样计算的话,我们的并发至少得有2000了。

而且,如果出现了这种情况,不会影响业务使用的。可能会受影响的只有 InnoDB中 B+tree出现先小后大的效率差问题不过有这个时间还是想想如何优化业务代码吧。如果单论技术的话,业务从我这里拿走的id,不一定怎么处理呢,可能会因为业务的原因,导致大的先进表了,小的后进去。所以不要假装在这里钻研技术,实际上业务代码写的一团糟。

为什么会影响 InnoDB的 B+tree?

比如叶子节点的某一个页中放了1000个主键id,首先,叶子节点中这些id是有序递增排列的,如果你后边来了一个小的,那就有可能之前的页中放不下了,需要重排列和分页。

7、生成 id的长度有多长?

最长19位!一定不会超!表中的主键id bigint(20)足够用!19位就是百亿亿了!

id的大小区间是:[1, 9223372036854775807]
最小的id是:1(twepoch为当前时间,并且workerId是0,序列号为1)
最大的id是:9223372036854775807(64个bit,第1个bit是0不变,其余位都占满,即是最大数,为9223372036854775807,也就是java Long类型的 MAX_VALUE)

8、这个 id的位数分配规则,自己能调整吗?

当然可以,根据自己的业务情况调整。我们这儿将 bit位数分配调整为:1+42+5+16

  • 1个bit 用来表示0,正数;
  • 42个bit 用来表示毫秒时间戳;42个bit,最大是4398046511103,4398046511103个毫秒值,换算大约是 139年,也就是说从开始投入使用,可以使用 139年;
  • 5个bit 用来表示workerID;最大31,也就是目前支持最多31个worker,一般小单位没有这个量级,如果有这种体量了,再调整也可以的,不用怕重复,因为时间戳是在高位,并且时间戳肯定是递增的,高位的大小决定了数字的大小;
  • 16个bit 用来表示sequence,自增值;同一毫秒,同一 worker上,最多生成 65536个,也就是不考虑外界条件的话,单纯的这块,纯理论每秒可以出 65536000个id,大约是 6553万个。

9、workerId怎么计算的?

美团代码中,依赖 zookeeper + 本地文件缓存 双机制来生成 workerId,详细参考美团文章(GitHub - Meituan-Dianping/Leaf: Distributed ID Generate Service),讲的很清楚,这里不再赘述。

三、安全问题

上边的搞清楚之后再来看安全问题,1和2 的问题都可以参考美团文章(GitHub - Meituan-Dianping/Leaf: Distributed ID Generate Service),上边讲的很清晰了。这里不再赘述。

1、时钟回拨,即时间走着走着却倒回去了,如何解决?

2、新起的节点中,机器时间不对,那就压根不能提供服务,如何校正?

3、业务上,通过 getById 接口遍历能拿走多少越权数据?

首先雪花算法生成的id不是+1递增的,比依赖数据库的自增id要安全很多,另外还要看你的业务设计上,数据权限的控制是否存在漏洞。

4、友商是否可以解析你的 id大概计算出你的业务量?

不存在。首先有 twepoch存在,我不是时间戳,你解析出来只是数字,无法知道具体时间的;另外,后面的自增 sequence,不同的ms内,是随机生成的,不是都从0开始的,无法知道我的业务量;这两个条件加起来,足以保证 id的安全性。


【总结】本文我们主要聊了雪花算法到底是怎么算出来一个 id的。其实基本功是二进制运算,基础扎实的话,很快就可以 get到它的设计思想。但是,一定要清楚,所有的技术都是为了解决业务问题,也不存在一种技术设计完全适用所有公司的所有业务场景,具体使用的时候根据自己的情况进行调整,但就意味着你必须清楚原理,别瞎改。否则 3.25甚至更惨,哈哈。对自己的每一行代码负责是技术人的默认选项。

努力改变自己和身边人的生活。

特别希望本文可以对你有所帮助,原创不易,感谢你留个赞和关注,道阻且长,我们并肩前行!

转载请注明出处。感谢大家留言讨论交流。

10分钟拿下雪花算法相关推荐

  1. 节后综合征疗愈神器,这个开源项目帮你10分钟上手AI算法开发!

    想要应用AI技术进行产业智能化升级,又担心缺乏计算机.数学等理论基础? AI算法训练完成,优化部署上线又是一个趟不过去的大坑? 别担心,今天就教大家应用一个开源项目--飞桨全流程开发工具PaddleX ...

  2. 10分钟拿下 HashMap

    道阻且长,行则将至.请相信我,你一定会更优秀! 备注:本文 jdk版本为 1.7,主要是为了帮助小白入门的,大佬请绕道.入门后自己去推敲高版本的jdk源代码. 目录 1.什么是 HashMap,什么时 ...

  3. 量子计算机 漫画,漫画 | 10分钟看懂量子比特、量子计算和量子算法

    原标题:漫画 | 10分钟看懂量子比特.量子计算和量子算法 请做好准备,即将进入烧脑模式! 宏观世界的生活经验很多都是表象.比如,你可能认为世界的运行是确定的.可预测的:一个物体不可能同时处于两个相互 ...

  4. 10分钟详解:算法面试5大必考排序方式

    来源:IT技术思维原创,转载请注明原出处 内容提供:苏勇, Google 资深软件工程师 算法学习其实是一种学习和提高思维能力的过程.无论是在面试还是实际的工作和生活中,都会碰见一些从没遇到过的问题 ...

  5. 零基础小白10分钟用Python搭建小说网站!网友:我可以!

    都说Python什么都能做,本来我是不信的!直到我在CSDN站内看到了一件真事儿:一位博主贴出了自己10分钟用Python搭建小说网站的全过程!全程只用了2步操作,简直太秀了!!-- 第一步:爬取小说 ...

  6. SpringBoot 雪花算法生成商品订单号【SpringBoot系列13】

    SpringCloud 大型系列课程正在制作中,欢迎大家关注与提意见. 程序员每天的CV 与 板砖,也要知其所以然,本系列课程可以帮助初学者学习 SpringBooot 项目开发 与 SpringCl ...

  7. 豆瓣8.9,《数学之美》第三版,让你10分钟爱上数学

    文末赠书 14年前,"数学之美"系列文章首载于谷歌黑板报,即获得上百万次点击,凡阅文者,皆叹相见恨晚,大学时痛恨万分的马尔可夫链.矩阵计算,甚至余弦函数等原来如此亲切,自然语言和信 ...

  8. 雪花算法通过顺序号持久化解决时钟回拨

    在雪花算法自定义解决时钟回拨问题一文中,对雪花算法的时钟回拨解决思路进行了说明,由于顺序号保存在内存中,每次启动都是从初始值开始,在特定场景下,比如停止服务后进行了时钟回拨,在理论上,还是可能出现序列 ...

  9. 基于java注册登录MD5算法加盐加密颁发 Token身份令牌使用各种邮箱发送验证码详解雪花算法

    目的作用 == 在项目中,为了防止别人窥视我们的密码通常我们会采取一些加密方式.这里简单介绍一下MD5 加盐加密方法,MD5叫做信息-摘要算法,严格来说不是加密方式,而是信息摘要. 对于可以接触到数据 ...

最新文章

  1. python创建虚拟环境sublime_如何设置python 虚拟环境 sublime text
  2. python使用matplotlib可视化线图(line plot)、将可视化图像的图例(legend)放置在图像外部、底部区域
  3. mysql backlog_一次优化引发的血案
  4. linux shell 字符串 数组,bash shell函数返回数组字符串
  5. idea工程本地依赖_IDEA最新版2020.1的maven工程本地依赖仓库无法使用问题(已解决)...
  6. 关于在pycharm下提示ModuleNotFoundError: No module named 'XXX' 的一种可能
  7. Asp.Net Core Web Api图片上传及MongoDB存储实例教程(一)
  8. Linux删除文件和文件夹【命令】
  9. Linux CPU信息和使用情况查看(CentOS)
  10. 删除数据表中的重复行
  11. EIGRP分解试验部分-LAB1:EIGRP基本试验
  12. 利用numpy.gradient计算图像梯度
  13. java modbus tcp plc_PLC1200MODBUS TCP 程序实例
  14. 【数据挖掘】二手车交易价格预测(三)数据分析
  15. 日本的美景,从谷歌卫星地图上就可以一撇究竟
  16. 苹果8硬件保修服务器,iPhone手机刷机报错,很多是硬件问题
  17. 类似YY 9158网页版多人语音视频聊天室 远程教学系统源码
  18. UCan技术开放日|告别转型“焦虑”,从云原生开始
  19. css -moz_moz-border-radius(CSS属性)
  20. python循环案例:模拟银行ATM存款取款

热门文章

  1. java jvisualvm linux,在CentOS上安装jvisualvm
  2. CAD填充图案如何缩放
  3. (完全解决)Python中pip如何安装github上的一个包
  4. 线性规划的简单应用及使用EXCEL求解
  5. C语言API函数大全(转载)
  6. qss 画框_PyQt5系列教程(77):QSS入门2
  7. 【烂笔头】android 左上角三角形 右上角三角形
  8. 适用于老版本的魔兽世界登陆器编写指南
  9. JS常用封装方法大全
  10. 在父域的基础上,添加子域