一、snowflake算法
snowflake算法生成ID是一个64bit大小的整数,它的二进制结构如下图:
2.1、算法数据结构解释
- 1bit 二进制中最高位为1表示负数,但是我们最终生成的ID一般都是整数,所以这个最高位固定为0。
- 41bit 2(41次方)用于记录时间戳(毫秒)
- 41bit可以表示2(41次方)个数字
- 如果只用来表示正整数(计算机中正数包含0),可以表示的数值范围是0到2(41次方)-1,减1是因为可表示的数值范围从0开始计算.
- 即41bit可以表示2(41次方)个毫秒值转换为年为2(41次方)/ (1000 60 60 24 365) ≈ 69年
- 10bit 用于记录机器ID
- 用于部署2(10次方)= 1024个节点,包含5bit个datacenterId 和 5bit个workerId
- 5bit可以表示的最大正整数为2(5次方)-1=31 即可以用0、1、2、3….31,这32个数字来表示不同的datacenterId 和 workerId
12bit 序列号用于记录相同毫秒内产生的不同ID
毫秒数在高位,自增序列在低位,整个ID 都是趋势递增的,整个分布式系统内不会产生重复ID,由于5bit 的 datacenterId 和5bit 的workerId来区分
- 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成 ID 的性能也是非常高的。可以根据自身业务特性分配 bit 位,非常灵活
- 高QPS,理论snowflake方案的QPS为409.6w/s,计算算法规则 = 1000
≈ 409.6w
缺点:
- 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态
2.3、算法思考
2.3.1、snowflake为啥用64位,是否可以扩容?
snowflake是否可以改变其QPS?
可以这么干的,百度的uid-generator就是这么干的,[后面在具体分析其实现原理](https://www.yuque.com/docs/share/fb19a718-7058-4a0d-91f0-a167dfb1bcfb?# 《3、uid-generator设计分析》). [百度uid-generator github](https://github.com/baidu/uid-generator)
snowflake是否可以扩展位长度?
算法角度上,snowflake是可以扩展位长度,这样可以获取更大的key不重复使用年限及QPS,但是snowflake生成的是数字,在java中最大的数字就是long,故不能扩容. 有人肯定想mysql主键设置为String突破Long长度限制,这种设计虽然突破了限制.但是会导致mysql索引问题,可能导致B树重新平衡、大字段做主键会浪费MySQl宝贵的内存资源.
二、snowflake源码
下面代码是介绍snowflake如何实现的,其未处理应用关闭后时间戳重新复位问题,不可在生产环境中使用. 目前已知基于snowflake框架是美团的Leaf. ```plsql package com.yst.tms.order.center.util;
import java.net.Inet4Address; import java.net.UnknownHostException; import java.util.HashSet; import java.util.Random; import java.util.Set;
class SnowflakeUtils {
/** 时间部分所占长度 */
private static final int TIME_LEN = 41;
/** 数据中心id所占长度 */
private static final int DATA_LEN = 5;
/** 机器id所占长度 */
private static final int WORK_LEN = 5;
/** 毫秒内序列所占长度 */
private static final int SEQ_LEN = 12;
/** 定义起始时间 2015-01-01 00:00:00 */
private static final long START_TIME = 1420041600000L;
/** 上次生成ID的时间截 */
private static long LAST_TIME_STAMP = -1L;
/** 时间部分向左移动的位数 22 */
private static final int TIME_LEFT_BIT = 64 - 1 - TIME_LEN;
/** 自动获取数据中心id(可以手动定义 0-31之间的数) */
private static final long DATA_ID = getDataId();
/** 自动机器id(可以手动定义 0-31之间的数) */
private static final long WORK_ID = getWorkId();
/** 数据中心id最大值 31 */
private static final int DATA_MAX_NUM = ~(-1 << DATA_LEN);
/** 机器id最大值 31 */
private static final int WORK_MAX_NUM = ~(-1 << WORK_LEN);
/** 随机获取数据中心id的参数 32 */
private static final int DATA_RANDOM = DATA_MAX_NUM + 1;
/** 随机获取机器id的参数 32 */
private static final int WORK_RANDOM = WORK_MAX_NUM + 1;
/** 数据中心id左移位数 17 */
private static final int DATA_LEFT_BIT = TIME_LEFT_BIT - DATA_LEN;
/** 机器id左移位数 12 */
private static final int WORK_LEFT_BIT = DATA_LEFT_BIT - WORK_LEN;
/** 上一次的毫秒内序列值 */
private static long LAST_SEQ = 0L;
/** 毫秒内序列的最大值 4095 */
private static final long SEQ_MAX_NUM = ~(-1 << SEQ_LEN);
public synchronized static long genId(){
long now = System.currentTimeMillis();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (now < LAST_TIME_STAMP) {
throw new RuntimeException(String.format("系统时间错误! %d 毫秒内拒绝生成雪花ID!", START_TIME - now));
}
//防止在同一时间,序列生成id,目的防止id重复
if (now == LAST_TIME_STAMP) {
LAST_SEQ = (LAST_SEQ + 1) & SEQ_MAX_NUM;
//?这段代码什么目的?谁知道留言!
if (LAST_SEQ == 0){
now = nextMillis(LAST_TIME_STAMP);
}
} else {
LAST_SEQ = 0;
}
//上次生成ID的时间截
LAST_TIME_STAMP = now;
/*
* 1、(now - START_TIME)这步很重要,如果不做这步会从1970-01-01 08:00:00开始计时,预计2042年结束计时,减去START_TIME,可以从START_TIME时间点计时,让snowflake寿命更长!!!
* 2、TIME_LEFT_BIT为啥是22? SnowFlake算法是计算用69年,这个时间戳移位22不超过long.max,故最大只能用22,防止越界.
* “1970-01-01 08:00:00的时间戳是0,加上69年后,时间戳是2177452800000,在移位22,就到long.max边界了”
* 3、(DATA_ID << DATA_LEFT_BIT) ,用途是按照数据中心求或,目的让数据key按照数据中心规则分布
* 4、(WORK_ID << WORK_LEFT_BIT),用途是按照部署机器id求或,目的让数据key在按照集群规则分布
* 5、LAST_SEQ, 用途是按照随机数求或,目的让可以最后按照最后的key规则生成
*/
return ((now - START_TIME) << TIME_LEFT_BIT) | (DATA_ID << DATA_LEFT_BIT) | (WORK_ID << WORK_LEFT_BIT) | LAST_SEQ;
}
/**
* 获取下一不同毫秒的时间戳,不能与最后的时间戳一样
*/
public static long nextMillis(long lastMillis) {
long now = System.currentTimeMillis();
while (now <= lastMillis) {
now = System.currentTimeMillis();
}
return now;
}
/**
* 获取字符串s的字节数组,然后将数组的元素相加,对(max+1)取余
*/
private static int getHostId(String s, int max){
byte[] bytes = s.getBytes();
int sums = 0;
for(int b : bytes){
sums += b;
}
return sums % (max+1);
}
/**
* 根据 host address 取余,发生异常就获取 0到31之间的随机数
*/
public static int getWorkId(){
try {
return getHostId(Inet4Address.getLocalHost().getHostAddress(), WORK_MAX_NUM);
} catch (UnknownHostException e) {
return new Random().nextInt(WORK_RANDOM);
}
}
/**
* 根据 host name 取余,发生异常就获取 0到31之间的随机数
*/
public static int getDataId() {
try {
return getHostId(Inet4Address.getLocalHost().getHostName(), DATA_MAX_NUM);
} catch (UnknownHostException e) {
return new Random().nextInt(DATA_RANDOM);
}
}
public static void main(String[] args) {
Set ids = new HashSet();
long start = System.currentTimeMillis();
for (int i = 0; i < 3000000; i++) {
ids.add(genId());
}
long end = System.currentTimeMillis();
System.out.println("共生成id[" + ids.size() + "]个,花费时间[" + (end - start) + "]毫秒");
}
} ```