snowflake原理解析
Snowflake
世界上没有两片完全相同的雪花。
Snowflake原理
这种方案把64-bit分别划分成多段,分开来标示机器、时间等,比如在snowflake中的64-bit分别表示如下图所示:
在java里,64bit正好是long类型的大小。
41-bit的时间可以表示(1L<<41)/(1000ms * 60s * 60m * 24h * 365d)=69年的时间,10-bit机器可以分别表示1024台机器。如果我们对IDC划分有需求,还可以将10-bit分5-bit给IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器,可以根据自身需求定义。12个自增序列号可以表示212个ID,理论上snowflake方案的QPS约为212=4096/ms, 也就是409.6w/s,这种分配方式可以保证在任何一个IDC的任何一台机器在任意毫秒内生成的ID都是不同的。
+-----------+--------------------------+---------------------+----------------------+----------------+
| sign | time_stamp | datacenter | worker node | sequence |
+-----------+--------------------------+---------------------+----------------------+----------------+
1bit 41 bits 5bits 5bits 12bits
+-----------+--------------------------+---------------------+----------------------+----------------+
1位
,不用。二进制中最高位为1的都是负数,但是我们生成的id一般都使用整数,所以这个最高位固定是041位
,用来记录时间戳(毫秒)。- 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
- 可以部署在$2^{10} = 1024$个节点,包括
12位
,序列号,用来记录同毫秒内产生的不同id。12位(bit)
可以表示的最大正整数是$2^{12}-1 = 4095$,即可以用0、1、2、3、....4094这4095个数字,来表示同一机器同一时间截(毫秒)内产生的4095个ID序号
位操作解释
/**
* define the initial bit for each part of 64 bits of one Id
*/
private final long sequenceBits = 12L;
private final long datacenterIdBits = 5L;
private final long workerIdBits = 5L;
private final long timestampBits = 41L;
/*
* max values of workerId, datacenterId and sequence
* 11111111 11111111 11111111 11111111 // -1 in binary format
*/
// 2^5-1 = 31
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
// 2^10-1 = 1023
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 2^12-1 = 4095
private final long maxSequence = -1L ^ (-1L << sequenceBits);
先看snowflake里面定义成员变量的一个神操作,为什么这么定义呢?需要先了解下二进制位操作的原理。
负数的二进制表示
在计算机中,负数的二进制是用补码
来表示的。
假设我是用Java中的int类型来存储数字的,
int类型的大小是32个二进制位(bit),即4个字节(byte)。(1 byte = 8 bit)
那么十进制数字3
在二进制中的表示应该是这样的:
00000000 00000000 00000000 00000011
// 3的二进制表示,就是原码
那数字-3
在二进制中应该如何表示?
我们可以反过来想想,因为-3+3=0,
在二进制运算中把-3的二进制看成未知数x来求解
,
求解算式的二进制表示如下:
00000000 00000000 00000000 00000011 //3,原码
+ xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx //-3,补码
-----------------------------------------------
00000000 00000000 00000000 00000000
反推x的值,3的二进制加上什么值才使结果变成00000000 00000000 00000000 00000000
?:
00000000 00000000 00000000 00000011 //3,原码
+ 11111111 11111111 11111111 11111101 //-3,补码
-----------------------------------------------
1 00000000 00000000 00000000 00000000
反推的思路是3的二进制数从最低位开始逐位加1,使溢出的1不断向高位溢出,直到溢出到第33位。然后由于int类型最多只能保存32个二进制位,所以最高位的1溢出了,剩下的32位就成了(十进制的)0。
补码的意义就是可以拿补码和原码(3的二进制)相加,最终加出一个“溢出的0”
以上是理解的过程,实际中记住公式就很容易算出来:
- 补码 = 反码 + 1
- 补码 = (原码 - 1)再取反码
因此-1
的二进制应该这样算:
00000000 00000000 00000000 00000001 //原码:1的二进制
11111111 11111111 11111111 11111110 //取反码:1的二进制的反码
11111111 11111111 11111111 11111111 //加1:-1的二进制表示(补码)
用位运算 得出n个bit能存储最大值
private long workerIdBits = 5L;
// 2^5-1 = 31
private long maxWorkerId = -1L ^ (-1L << workerIdBits);
其中:
^
操作符是 异或
操作, 即:如果a、b两个值不相同,则异或结果为1。如果a、b两个值相同,异或结果为0。
异或[1]也叫半加运算,其运算法则相当于不带进位的二进制加法:二进制下用1表示真,0表示假,则异或的运算法则为:0⊕0=0,1⊕0=1,0⊕1=1,1⊕1=0(同为0,异为1),这些法则与加法是相同的,只是不带进位,所以异或常被认作不进位加法。
延伸:巧用异或算法
1-1000放在含有1001个元素的数组中,只有唯一的一个元素值重复,其它均只出现
一次。每个数组元素只能访问一次,设计一个算法,将它找出来;不用辅助存储空
间,能否设计一个算法实现?
解法一、显然已经有人提出了一个比较精彩的解法,将所有数加起来,减去1+2+…+1000的和。
这个算法已经足够完美了,相信出题者的标准答案也就是这个算法,唯一的问题是,如果数列过大,则可能会导致溢出。解法二、异或就没有这个问题,并且性能更好。
将所有的数全部异或,得到的结果与123…1000的结果进行异或,得到的结果就是重复数。
google面试题的变形:一个数组存放若干整数,一个数出现奇数次,其余数均出现偶数次,找出这个出现奇数次的数?
Leecode:https://leetcode-cn.com/problems/single-number/solution/
@Test
public void fun() {
int a[] = { 22, 38,38, 22,22, 4, 4, 11, 11 };
int temp = 0;
for (int i = 0; i < a.length; i++) {
temp ^= a[i];
}
System.out.println(temp);
}
解法有很多,但是最好的和上面一样,就是把所有数异或,最后结果就是要找的,原理同上!!
<<
是 左移
操作
private long workerIdBits = 5L;
private long maxWorkerId = -1L ^ (-1L << workerIdBits);
所以long maxWorkerId = -1L ^ (-1L << 5L)
的二进制运算过程如下:
- -1 左移 5,得结果a
11111111 11111111 11111111 11111111 //-1的二进制表示(补码)
11111 11111111 11111111 11111111 11100000 //高位溢出的不要,低位补0
11111111 11111111 11111111 11100000 //结果a
- -1 异或 a
11111111 11111111 11111111 11111111 //-1的二进制表示(补码)
^ 11111111 11111111 11111111 11100000 //两个操作数的位中,相同则为0,不同则为1
---------------------------------------------------------------------------
00000000 00000000 00000000 00011111 //最终结果31
最终结果是31,二进制00000000 00000000 00000000 00011111
转十进制可以这么算:
2^4 + 2^3 + 2^2 + 2^1 + 2^0 = 16 + 8 + 4 +2 +1 = 31
所以这一通操作其实是通过位运算计算出来5bit的work id 最多能存储31个值,也就是最多支持31个节点。
位与
操作防止溢出
我们在跨毫秒时,序列号总是归0,会使得序列号为0的ID比较多,导致生成的ID取模后不均匀。优化的点是,序列号不是每次都归0,而是归一个0到100的随机数。
//如果当前操作落在同一个ms(timestamp位相同)的话
if (lastTimestamp == currentTimestamp) {
//sequence++ 且 保证不溢出,下面测试可知道如果溢出了maxSequence就会变成0
sequence = (sequence + 1) & maxSequence;
if (sequence == 0) {
// overflow: greater than max sequence
sequence = RANDOM.nextInt(100);
currentTimestamp = waitNextMillis(lastTimestamp);
}
} else {
//如果是新的ms开始,防止并发不够每次都是0
//sequence = 0L;
sequence = RANDOM.nextInt(100);
}
关于这里的溢出操作分别用不同的值测试一下,就知道原理了:
long seqMask = -1L ^ (-1L << 12L); //计算12位能耐存储的最大正整数,相当于:2^12-1 = 4095
System.out.println("seqMask: "+seqMask);
System.out.println(1L & seqMask);
System.out.println(2L & seqMask);
System.out.println(3L & seqMask);
System.out.println(4L & seqMask);
System.out.println(4095L & seqMask);
System.out.println(4096L & seqMask);
System.out.println(4097L & seqMask);
System.out.println(4098L & seqMask);
/**
* 输出结果是:
seqMask: 4095
1
2
3
4
4095
0
1
2
*/
因为maxSequence 我们选用12bit最大值是4095,这段代码通过位与运算保证计算的结果范围始终是 0-4095 !
生成id的核心代码
long id = ((currentTimestamp - epoch) << timestampShift) |
(datacenterId << datacenterIdShift) |
(workerId << workerIdShift) |
sequence;
计算分为2部分:
<<
每个对应部分的id 左移对应的bits;管道符号
|
在Java中也是一个位运算符。其含义是:
x的第n位和y的第n位 只要有一个是1,则结果的第n位也为1,否则为0
,因此,我们对四个数的位或运算
如下:
1 | 41 | 5 | 5 | 12
0|0001100 10100010 10111110 10001001 01011100 00|00000|0 0000|0000 00000000
0|0000000 00000000 00000000 00000000 00000000 00|10001|0 0000|0000 00000000
0|0000000 00000000 00000000 00000000 00000000 00|00000|1 1001|0000 00000000
or0|0000000 00000000 00000000 00000000 00000000 00|00000|0 0000|0000 00000000
------------------------------------------------------------------------------------------
0|0001100 10100010 10111110 10001001 01011100 00|10001|1 1001|0000 00000000
//结果:910499571847892992
从位或
运算角度简化看是这样的视角
1 | 41 | 5 | 5 | 12
0|0001100 10100010 10111110 10001001 01011100 00| | | //la
0| |10001| | //lb
0| | |1 1001| //lc
or0| | | |0000 00000000 //seq
------------------------------------------------------------------------------------------
0|0001100 10100010 10111110 10001001 01011100 00|10001|1 1001|0000 00000000
//结果:910499571847892992
上面的64位我按1、41、5、5、12的位数截开了,方便观察。
纵向
观察发现:- 在41位那一段,除了la一行有值,其它行(lb、lc、seq)都是0
- 在左起第一个5位那一段,除了lb一行有值,其它行都是0
- 在左起第二个5位那一段,除了lc一行有值,其它行都是0
- 按照这规律,如果seq是0以外的其它值,12位那段也会有值的,其它行都是0
横向
观察发现:- 在la行,由于左移了5+5+12位,5、5、12这三段都补0了,所以la行除了41那段外,其它肯定都是0
- 同理,lb、lc、seq行也以此类推
- 正因为左移的操作,使四个不同的值移到了SnowFlake理论上相应的位置,然后四行做
位或
运算(只要有1结果就是1),就把4段的二进制数合并成一个二进制数。
结论:
所以,在这段代码中左移运算是为了将数值移动到对应的段(41、5、5,12那段因为本来就在最右,因此不用左移)。然后对每个左移后的值(la、lb、lc、seq)做位或运算,是为了把各个短的数据合并起来,合并成一个二进制数。最后转换成10进制,就是最终生成的id。
延伸:long和double底层
问题: java中 long 和double都是64位。为什么double表示的范围大那么多呢?
标准答案是这样子的:
double是n*2^m(n乘以2的m次方)这种形式存储的,只需要记录n和m两个数就行了,m的值影响范围大,所以表示的范围比long大。
但是m越大,n的精度就越小,所以double并不能把它所表示的范围里的所有数都能精确表示出来,而long就可以。
贴上一些整数类型的范围:
1.整型 (一个字节占8位)
类型 存储需求 bit数 取值范围 备注
int 4字节 48 (32) -231~231-1
short 2字节 28 (16) -215~215-1
long 8字节 88 (64) -263~263-1
byte 1字节 18 (8) -27~27-1 = -128~127
2.浮点型
类型 存储需求 bit数 取值范围 备注
float 4字节 48 (32) 3.4028235E38 ~= 3.410^38
double 8字节 88 (64) 1.7976931348623157E308 ~=1.710^308
从范围来看double和long完全不是一个级别的了吧?long最大为=263-1,而double为21024。
Snowflake的优缺点
优点:
- 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
- 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
- 可以根据自身业务特性分配bit位,非常灵活。
缺点:
- 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。
- 我们通过5ms的等待来兼容ntp的同步,如果大于5ms就报错。
服务部署
部署模式支持2种
部署zookeeper, 配置zk地址,然后启动Spring boot
也可以把snowflake单独作为包引入项目,独立使用
配置方式
- Rest Server的配置在server/src/main/resources/application.properties中:
配置项 | 含义 | 默认值 |
---|---|---|
spring.application.name | web服务名 | default |
server.port | web服务注册端口 | |
snowflake.zk.address | zk地址 |
- 如果只是配置snowflake这个单独的模块,可以参考snowflake/src/test/resources/snowflake.properties中:
| 配置项 | 含义 | 默认值 |
| ------------------------- | ----------------------------- | ------ |
| snowflake.name | snowflake服务名 | default|
| snowflake.node.port | snowflake服务注册端口 | |
| snowflake.zk.address | zk地址 | |
远程调用方式
- HTTP调用拿一个id:
curl http://localhost:8789/api/snowflake/get
response
{
"code":0,
"message":"ok",
"content":{
"id":9546332062617603,
"status":"SUCCESS"
}
}
- HTTP调用解析一个id:
curl http://localhost:8789/api/snowflake/decode?snowflakeId=9546332062617603
response
{
"code":0,
"message":"ok",
"content":{
"format":"2019-10-27 08:13:42.926, #3, @(0,1)",
"timestamp":"2019-10-27 08:13:42.926",
"datacenterId":0,
"workerId":1,
"sequenceId":3
}
}
欢迎关注我的公众号:好奇心森林
snowflake原理解析的更多相关文章
- Twitter分布式自增ID算法snowflake原理解析
以JAVA为例 Twitter分布式自增ID算法snowflake,生成的是Long类型的id,一个Long类型占8个字节,每个字节占8比特,也就是说一个Long类型占64个比特(0和1). 那么一个 ...
- Twitter分布式自增ID算法snowflake原理解析(Long类型)
Twitter分布式自增ID算法snowflake,生成的是Long类型的id,一个Long类型占8个字节,每个字节占8比特,也就是说一个Long类型占64个比特(0和1). 那么一个Long类型的6 ...
- [原][Docker]特性与原理解析
Docker特性与原理解析 文章假设你已经熟悉了Docker的基本命令和基本知识 首先看看Docker提供了哪些特性: 交互式Shell:Docker可以分配一个虚拟终端并关联到任何容器的标准输入上, ...
- 【算法】(查找你附近的人) GeoHash核心原理解析及代码实现
本文地址 原文地址 分享提纲: 0. 引子 1. 感性认识GeoHash 2. GeoHash算法的步骤 3. GeoHash Base32编码长度与精度 4. GeoHash算法 5. 使用注意点( ...
- Web APi之过滤器执行过程原理解析【二】(十一)
前言 上一节我们详细讲解了过滤器的创建过程以及粗略的介绍了五种过滤器,用此五种过滤器对实现对执行Action方法各个时期的拦截非常重要.这一节我们简单将讲述在Action方法上.控制器上.全局上以及授 ...
- Web APi之过滤器创建过程原理解析【一】(十)
前言 Web API的简单流程就是从请求到执行到Action并最终作出响应,但是在这个过程有一把[筛子],那就是过滤器Filter,在从请求到Action这整个流程中使用Filter来进行相应的处理从 ...
- GeoHash原理解析
GeoHash 核心原理解析 引子 一提到索引,大家脑子里马上浮现出B树索引,因为大量的数据库(如MySQL.oracle.PostgreSQL等)都在使用B树.B树索引本质上是对索引字段 ...
- alibaba-dexposed 原理解析
alibaba-dexposed 原理解析 使用参考地址: http://blog.csdn.net/qxs965266509/article/details/49821413 原理参考地址: htt ...
- 支付宝Andfix 原理解析
支付宝Andfix 原理解析 使用参考地址: http://blog.csdn.net/qxs965266509/article/details/49802429 原理参考地址: http://blo ...
随机推荐
- 学习笔记:平衡树-splay
嗯好的今天我们来谈谈cosplay splay是一种操作,是一种调整二叉排序树的操作,但是它并不会时时刻刻保持一个平衡,因为它会根据每一次操作把需要操作的点旋转到根节点上 所谓二叉排序树,就是满足对树 ...
- DIV+CSS布局的优势和弊端
DIV+CSS的优势1.符合W3C标准.这保证您的网站不会因为将来网络应用的升级而被淘汰.2.对浏览者和浏览器更具亲和力.由于CSS富含丰富的样式,使页面更加灵活性,它可以根据不同的浏览器,而达到显示 ...
- 「雕爷学编程」Arduino动手做(17)---人体感应模块
37款传感器和模块的提法,在网络上广泛流传,其实Arduino能够兼容的传感器模块肯定是不止37种的.鉴于本人手头积累了一些传感器与模块,依照实践出真知(动手试试)的理念,以学习和交流为目的,这里准备 ...
- MyBatis缓存机制(一级缓存,二级缓存)
一,MyBatis一级缓存(本地缓存) My Batis 一级缓存存在于 SqlSession 的生命周期中,是SqlSession级别的缓存.在操作数据库时需要构造SqlSession对象,在对象中 ...
- P4015 运输问题 最大/最小费用最大流
P4015 运输问题 #include <bits/stdc++.h> using namespace std; , inf = 0x3f3f3f3f; struct Edge { int ...
- Django之ORM对象关系模型
MVC或者MVC框架中包括一个重要的部分,就是ORM,它实现了数据模型与数据库的解耦,即数据模型的设计不需要依赖于特定的数据库,通过简单的配置就可以轻松更换数据库,这极大的减轻了开发人员的工作量,不需 ...
- 1、JavaScript中的Cookie 用于存储 web 页面的用户信息。
总结:每个浏览器都有一定数量限制的cookie.每个浏览器中,每一个cookie都有一个path路径,指向请求访问的网页. -------------------------------------- ...
- Java通过循环结构和switch实现简易计算器
Java通过循环结构和switch实现简易计算器 可以循环计算,通过调用函数本身来实现重新计算 package com.shenxiaoyu.method; import java.util.Scan ...
- excel导入mysql数据
excel加载mysql数据 1.第一步,选择从mysql导入数据 2.单击会出现弹框: 3.可能有的同学的,这里缺少插件,例如: 4.去下载 这个 插件安装即可.https://dev.mysql. ...
- 剑指Offer之调整数组顺序使奇数位于偶数前面
题目描述 输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变. 思路:将奇数放进 ...