SnowFlake算法Twitter提出的一种算法,如果是MySQL数据库的主键采用BIGINT的话,那么他的取值范围是-2^63 到 2^63 ,即存储一个BIGINT类型需要64位二进制。雪花算法就是针对这64位进行设计。
- 第1位二进制值固定位0,没有业务含义。
- 第2~42位,共41位二进制,为时间戳,用于存入精确到毫秒数的时间。
- 第43~52位,共10位二进制,为工作机器id位。
- 第53~64位,共12位二进制,代表1ms内可以产生的序列号,当它表示整数时,取值区间为[0,4095]
这样的话,1ms内产生4096个编号,1秒就最多产生4096000个ID,这样的性能就能满足分布式系统的需求。
public class SnowFlakeWorker {
//开始时间(这里使用2019年4月1日整点)
private final static long START_TIME = 1554048000000L;
//数据中心编号所占数位
private final static long DATA_CENTER_BITS = 10L;
//最大数据中心编号
private final static long MAX_DATA_CENTER_ID = 1023;
//序列编号占位位数
private final static long SEQUENCE_BIT = 12L;
//数据中心编号向左移12位
private final static long DATA_CENTER_SHIFT = SEQUENCE_BIT;
//时间戳向左移22位(10+12)
private final static long TIMESTAMP_SHIFT = DATA_CENTER_BITS+DATA_CENTER_SHIFT;
//最大生成序列号,这里为4095
private final static long MAX_SEQUENCE = 4095;
//数据中心ID(0~1023)
private long dataCenterId;
//毫秒内序列(0~4095)
private long sequence = 0L;
//上次生成ID的时间戳
private long lastTimestamp = -1L;
/*
现在的微服务和分布式趋于中心化,所以不需要受理机器编号
10位二进制全部用于数据中心
*/
public SnowFlakeWorker(long dataCenterId){
if (dataCenterId>MAX_DATA_CENTER_ID){
String msg = "数据中心编号["+dataCenterId+"]超过最大允许值["+MAX_DATA_CENTER_ID+"]";
throw new RuntimeException(msg);
}
if (dataCenterId<0){
String msg = "数据中心编号["+dataCenterId+"]不允许小于0";
throw new RuntimeException(msg);
}
this.dataCenterId = dataCenterId ;
}
/*
获得下一个ID
*/
public synchronized long nextId(){
//获取当前时间
long timestamp = System.currentTimeMillis();
//如果是同一个毫秒时间戳的处理
if(timestamp == lastTimestamp){
sequence+=1;//序号+1
if(sequence>MAX_SEQUENCE){
sequence = 0;
//等待到下一毫秒
timestamp = tilNextMillis(timestamp);
}
}else {
//修改时间戳
lastTimestamp = timestamp ;
//序列号重新开始
sequence = 0;
}
//二进制的位运算,其中“<<”代表左移
long result = ((timestamp-START_TIME)<<TIMESTAMP_SHIFT)
|(this.dataCenterId<<DATA_CENTER_SHIFT)
|sequence;
return result;
}
/*
阻塞到下一毫秒,直到获得新的时间戳
*/
protected long tilNextMillis(long lastTimestamp){
long timestamp;
do{
timestamp = System.currentTimeMillis();
}while (timestamp>lastTimestamp);
return timestamp;
}
public static void main(String[] args) {
SnowFlakeWorker snowFlakeWorker = new SnowFlakeWorker(1000);
for (int i = 0;i<10;i++)
System.out.println(snowFlakeWorker.nextId());
}
}
运行结果
347840848269836288
347840848269836289
347840848269836290
347840848269836291
347840848269836292
347840848269836293
347840848269836294
347840848269836295
347840848269836296
347840848269836297
代码解读
SnowFlakeWorker构造器就是简单的对数据中心的id进行校验
nextId 获取当前时间的毫秒数和上一次ID的毫秒数
如果是同一个毫秒数,就增加序列号,如果序列号大于规定的大小,就等待到下一毫秒,然后归零序列号。
如果不是同一个毫秒数,就是从零计数序列号。
最后返回结果。
通过左移操作将对应的数移动到正确的位置上
时间戳左移22位到高41位(工作机器编号10位,12位序列号)
机器号移动 12位(跨12位的序列号)
序列号不用动
通过异或将他们进行拼接起来就组成了最后的ID
ps
ID包含数据中心、时间戳和序列3种业务逻辑。
时间戳41位,这样就限制了使用时间为69年
此外实际情况,对于机器位和序列号不需要这么长,会引起二进制位的浪费。
通过上述的情况,我们可以删减其他部分的位数,然后可以多余出其他位数,可以加入发号机制,作为发号节点编号。
但是实际情况,ID会受到数据存储的限制,比如MySQL的BIGINT只能存储64位,如果拓展到DECIMAL,但是这种类型性能不如BIGINT,就如果实际可能需要更多位的话,可以采用其他方式进行限制,比如限流。