SnowFlake算法生成id的结果是一个64bit大小的整数
1位,不用。二进制中最高位为1的都是负数,但是我们生成的id一般都使用整数,所以这个最高位固定是0
41位,用来记录时间戳(毫秒)。
41位可以表示$2^{41}-1$个数字,
如果只用来表示正整数(计算机中正数包含0),可以表示的数值范围是:0 至 $2^{41}-1$,减1是因为可表示的数值范围是从0开始算的,而不是1。
也就是说41位可以表示$2^{41}-1$个毫秒的值,转化成单位年则是$(2^{41}-1) / (1000 * 60 * 60 * 24 * 365) = 69$年
10位,用来记录工作机器id。
可以部署在$2^{10} = 1024$个节点,包括5位datacenterId和5位workerId
5位(bit)可以表示的最大正整数是$2^{5}-1 = 31$,即可以用0、1、2、3、….31这32个数字,来表示不同的datecenterId或workerId
12位,序列号,用来记录同毫秒内产生的不同id。
12位(bit)可以表示的最大正整数是$2^{12}-1 = 4095$,即可以用0、1、2、3、….4094这4095个数字,来表示同一机器同一时间截(毫秒)内产生的4095个ID序号
由于在Java中64bit的整数是long类型,所以在Java中SnowFlake算法生成的id就是long来存储的。
SnowFlake可以保证:
所有生成的id按时间趋势递增
整个分布式系统内不会产生重复id(因为有datacenterId和workerId来做区分)
https://github.com/twitter-archive/snowflake
优点:
快。
没有啥依赖,实现也特别简单。
知道原理之后可以根据实际情况调整各各位段,方便灵活。
缺点:
只能趋势递增。(有些也不叫缺点,网上有些如果绝对递增,竞争对手中午下单,第二天在下单即可大概判断该公司的订单量,危险!!!)
依赖机器时间,如果发生回拨会导致可能生成id重复。
下面重点讨论时间回拨问题。
snowflake算法时间回拨问题思考
由于存在时间回拨问题,但是他又是那么快和简单,我们思考下是否可以解决呢? 零度在网上找了一圈没有发现具体的解决方案,但是找到了一篇美团不错的文章:Leaf——美团点评分布式ID生成系统(https://tech.meituan.com/MT_Leaf.html)
文章很不错,可惜并没有提到时间回拨如何具体解决。下面看看零度的一些思考:
分析时间回拨产生原因
第一:人物操作,在真实环境一般不会有那个傻逼干这种事情,所以基本可以排除。
第二:由于有些业务等需要,机器需要同步时间服务器(在这个过程中可能会存在时间回拨,查了下我们服务器一般在10ms以内(2小时同步一次))。
解决方法
由于是分布在各各机器自己上面,如果要几台集中的机器(并且不做时间同步),那么就基本上就不存在回拨可能性了(曲线救国也是救国,哈哈),但是也的确带来了新问题,各各结点需要访问集中机器,要保证性能,百度的uid-generator产生就是基于这种情况做的(每次取一批回来,很好的思想,性能也非常不错)https://github.com/baidu/uid-generator。
如果到这里你采纳了,基本就没有啥问题了,你就不需要看了,如果你还想看看零度自己的思考可以继续往下看看(零度的思考只是一种思考 可能也不一定好,期待你的交流。),uid-generator我还没有细看,但是看测试报告非常不错,后面有空的确要好好看看。
下面谈谈零度自己的思考,之前也大概和美团Leaf作者交流了下,的确零度的这个可以解决一部分问题,但是引入了一些其他问题和依赖。是零度的思考,期待更多的大佬给点建议。
时间问题回拨的解决方法:
当回拨时间小于15ms,就等时间追上来之后继续生成。
当时间大于15ms时间我们通过更换workid来产生之前都没有产生过的来解决回拨问题。
首先把workid的位数进行了调整(15位可以达到3万多了,一般够用了)
Snowflake算法稍微调整下位段:
sign(1bit)
固定1bit符号标识,即生成的畅途分布式唯一id为正数。
delta seconds (38 bits)
当前时间,相对于时间基点”2017-12-21”的增量值,单位:毫秒,最多可支持约8.716年
worker id (15 bits)
机器id,最多可支持约3.28万个节点。
sequence (10 bits)
每秒下的并发序列,10 bits,这个算法单机每秒内理论上最多可以生成1000*(2^10),也就是100W的ID,完全能满足业务的需求。
由于服务无状态化关系,所以一般workid也并不配置在具体配置文件里面,看看我这篇的思考,为什么需要无状态化。高可用的一些思考和理解,这里我们选择redis来进行中央存储(zk、db)都是一样的,只要是集中式的就可以。
下面到了关键了:
现在我把3万多个workid放到一个队列中(基于redis),由于需要一个集中的地方来管理workId,每当节点启动时候,(先在本地某个地方看看是否有 借鉴弱依赖zk 本地先保存),如果有那么值就作为workid,如果不存在,就在队列中取一个当workid来使用(队列取走了就没了 ),当发现时间回拨太多的时候,我们就再去队列取一个来当新的workid使用,把刚刚那个使用回拨的情况的workid存到队列里面(队列我们每次都是从头取,从尾部进行插入,这样避免刚刚a机器使用又被b机器获取的可能性)。
有几个问题值得思考:
如果引入了redis为啥不用redis下发id?(查看分布式系统唯一ID生成方案汇总会获得答案,我们这里仅仅是用来一致性队列的,能做一致性队列的基本都可以)。
引入redis就意味着引入其他第三方的架构,做基础框架最好是不要引用(越简单越好,目前还在学习提高)。
redis一致性怎么保证?(redis挂了怎么办,怎么同步,的确值得商榷。可能会引入会引入很多新的小问题)。