java雪花算法动态⽣成workId与dataCenterId
雪花算法(SnowFlake),是 Twitter 开源的分布式 id ⽣成算法。其核⼼思想就是:使⽤⼀个 64 bit 的 long 型的数字作为全局唯⼀id。在分布式系统中的应⽤⼗分⼴泛,且ID 引⼊了时间戳,基本上保持⾃增的,后⾯的代码中有详细的注解。这 64 个 bit 中,其中 1 个 bit 是不⽤的,然后⽤其中的 41 bit 作为毫秒数,⽤ 10 bit 作为⼯作机器 id,12 bit 作为序列号。⽐如下⾯那个 64 bit 的 long 型数字:
第⼀个部分,是 1 个 bit:0,这个是⽆意义的。
第⼆个部分是 41 个 bit:表⽰的是时间戳。
第三个部分是 5 个 bit:表⽰的是机房 id,10001。
第四个部分是 5 个 bit:表⽰的是机器 id,1 1001。
第五个部分是 12 个 bit:表⽰的序号,就是某个机房某台机器上这⼀毫秒内同时⽣成的 id 的序号,0000 00000000。
SnowFlake算法的优点:
(1)⾼性能⾼可⽤:⽣成时不依赖于数据库,完全在内存中⽣成
(2)容量⼤:每秒中能⽣成数百万的⾃增ID
(3)ID⾃增:存⼊数据库中,索引效率⾼
SnowFlake算法的缺点:
依赖与系统时间的⼀致性,如果系统时间被回调,或者改变,可能会造成id冲突或者重复。实际中我们的机房并没有那么多,我们可以改进改算法,将10bit的机器id优化,成业务表或者和我们系统相关的业务
①1 bit:是不⽤的,为啥呢?
因为⼆进制⾥第⼀个 bit 为如果是 1,那么都是负数,但是我们⽣成的 id 都是正数,所以第⼀个 bit 统⼀都是 0。
②41 bit:表⽰的是时间戳,单位是毫秒。
41 bit 可以表⽰的数字多达 2^41 - 1,也就是可以标识 2 ^ 41 - 1 个毫秒值,换算成年就是表⽰ 69 年的时间。
③10 bit:记录⼯作机器 id,代表的是这个服务最多可以部署在 2^10 台机器上,也就是 1024 台机器。
但是 10 bit ⾥ 5 个 bit 代表机房 id,5 个 bit 代表机器 id。意思就是最多代表 2 ^ 5 个机房(32 个机房),每个机房⾥可以代表 2 ^ 5 个机器(32 台机器),也可以根据⾃⼰公司的实际情况确定。
④12 bit:这个是⽤来记录同⼀个毫秒内产⽣的不同 id。
12 bit 可以代表的最⼤正整数是 2 ^ 12 - 1 = 4096,也就是说可以⽤这个 12 bit 代表的数字来区分同⼀个毫秒内的 4096 个不同的 id。简单来说,你的某个服务假设要⽣成⼀个全局唯⼀ id,那么就可以发送⼀个请求给部署了 SnowFlake 算法的系统,由这个 SnowFlake 算法系统来⽣成唯⼀ id。这个 SnowFlake 算法系统⾸先肯定是知道⾃⼰所在的机房和机器的,⽐如机房 id = 17,机器 id = 12。接着 SnowFlake 算法系统接收到这个请求之后,⾸先就会⽤⼆进制位运算的⽅式⽣成⼀个 64 bit 的 long 型 id,64 个 bit 中的第⼀个 bit 是⽆意义的。接着41 个 bit,就可以⽤当前时间戳(单位到毫秒),然后接着 5 个 bit 设置上这个机房 id,还有 5 个 bit 设置上机器 id。最后再判断⼀下,当前这台机房的这台机器上这⼀毫秒内,这是第⼏个请求,给这次⽣成 id 的请求累加⼀个序号,作为最后的 12 个 bit。
SnowFlake 算法的实现代码如下:
public class IdWorker {
//因为⼆进制⾥第⼀个 bit 为如果是 1,那么都是负数,但是我们⽣成的 id 都是正数,所以第⼀个 bit 统⼀都是 0。
//机器ID 2进制5位 32位减掉1位 31个
private long workerId;
//机房ID 2进制5位 32位减掉1位 31个
private long datacenterId;
//代表⼀毫秒内⽣成的多个id的最新序号 12位 4096 -1 = 4095 个
private long sequence;
//设置⼀个时间初始值 2^41 - 1 差不多可以⽤69年
private long twepoch = 1585644268888L;
//5位的机器id
private long workerIdBits = 5L;
//5位的机房id
private long datacenterIdBits = 5L;
//每毫秒内产⽣的id数 2 的 12次⽅
private long sequenceBits = 12L;
// 这个是⼆进制运算,就是5 bit最多只能有31个数字,也就是说机器id最多只能是32以内
private long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 这个是⼀个意思,就是5 bit最多只能有31个数字,机房id最多只能是32以内
private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
private long workerIdShift = sequenceBits;
private long datacenterIdShift = sequenceBits + workerIdBits;
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private long sequenceMask = -1L ^ (-1L << sequenceBits);
//记录产⽣时间毫秒数,判断是否是同1毫秒
private long lastTimestamp = -1L;
public long getWorkerId(){
return workerId;
}
public long getDatacenterId() {
return datacenterId;
}
public long getTimestamp() {
return System.currentTimeMillis();
}
public IdWorker(long workerId, long datacenterId, long sequence) {
// 检查机房id和机器id是否超过31 不能⼩于0
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(
String.format("worker Id can't be greater than %d or less than 0",maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(
String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
this.sequence = sequence;
}
// 这个是核⼼⽅法,通过调⽤nextId()⽅法,让当前这台机器上的snowflake算法程序⽣成⼀个全局唯⼀的id
public synchronized long nextId() {
// 这⼉就是获取当前时间戳,单位是毫秒
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
"clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
lastTimestamp - timestamp));
}
// 下⾯是说假设在同⼀个毫秒内,⼜发送了⼀个请求⽣成⼀个id
// 这个时候就得把seqence序号给递增1,最多就是4096
if (lastTimestamp == timestamp) {
// 这个意思是说⼀个毫秒内最多只能有4096个数字,⽆论你传递多少进来,
//这个位运算保证始终就是在4096这个范围内,避免你⾃⼰传递个sequence超过了4096这个范围
sequence = (sequence + 1) & sequenceMask;
//当某⼀毫秒的时间,产⽣的id数超过4095,系统会进⼊等待,直到下⼀毫秒,系统继续产⽣ID
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
// 这⼉记录⼀下最近⼀次⽣成id的时间戳,单位是毫秒
lastTimestamp = timestamp;
// 这⼉就是最核⼼的⼆进制位运算操作,⽣成⼀个64bit的id
// 先将当前时间戳左移,放到41 bit那⼉;将机房id左移放到5 bit那⼉;将机器id左移放到5 bit那⼉;将序号放最后12 bit // 最后拼接起来成⼀个64 bit的⼆进制数字,转换成10进制就是个long型
return ((timestamp - twepoch) << timestampLeftShift) |
(datacenterId << datacenterIdShift) |
(workerId << workerIdShift) | sequence;
}
/**
* 当某⼀毫秒的时间,产⽣的id数超过4095,系统会进⼊等待,直到下⼀毫秒,系统继续产⽣ID
* @param lastTimestamp
* @return
*/
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
//获取当前时间戳
private long timeGen(){
return System.currentTimeMillis();
}
/**
* main 测试类
* @param args
*/
public static void main(String[] args) {
IdWorker worker = new IdWorker(1,1,1);
for (int i = 0; i < 22; i++) {
System.out.Id());
}
}
}
项⽬组的应⽤是容器多实例启动的,workId和dataCenterId需要分布式动态⽣成
@Configuration
public class SnowFlakeIdConfig {
@Bean
public SnowFlakeIdUtil propertyConfigurer() {
return new SnowFlakeIdUtil(getWorkId(), getDataCenterId(), 10);
}
/**
* workId使⽤IP⽣成
* @return workId
*/
private static Long getWorkId() {
try {
String hostAddress = LocalHost().getHostAddress();
int[] ints = CodePoints(hostAddress);
int sums = 0;
for (int b : ints) {
sums = sums + b;
}
return (long) (sums % 32);
}
catch (UnknownHostException e) {
// 失败就随机
Long(0, 31);
}
}
/**
* dataCenterId使⽤hostName⽣成
* @return dataCenterId
*/
private static Long getDataCenterId() {
try {
String hostName = HostName();
int[] ints = CodePoints(hostName);
int sums = 0;
for (int i: ints) {
java64位
sums = sums + i;
}
return (long) (sums % 32);
}
catch (Exception e) {
// 失败就随机
Long(0, 31);
}
}
}
53bitID由32bit秒级时间戳+16bit⾃增+5bit机器标识组成
Snowflake算法采⽤41bit毫秒时间戳,加上10bit机器ID,加上12bit序列号,理论上最多⽀持1024台机器每秒⽣成4096000个序列号,对于Twitter的规模来说够⽤了。但是对于绝⼤部分普通应⽤程序来说,根本不需要每秒超过400万的ID,机器数量也达不到1024台,所以,我们可以改进⼀下,使⽤更短的ID⽣成⽅式:53bitID由32bit秒级时间戳+16bit⾃增+5bit机器标识组成,累积32台机器,每秒可以⽣成6.5万个序列号,核⼼代码:
private static synchronized long nextId(long epochSecond) {
if (epochSecond < lastEpoch) {
// warning: clock is turn back:
logger.warn("clock is back: " + epochSecond + " from previous:" + lastEpoch);
epochSecond = lastEpoch;
}
if (lastEpoch != epochSecond) {
lastEpoch = epochSecond;
reset();
}
offset++;
long next = offset & MAX_NEXT;
if (next == 0) {
logger.warn("maximum id reached in 1 second in epoch: " + epochSecond);
return nextId(epochSecond + 1);
}
return generateId(epochSecond, next, SHARD_ID);
}
时间戳减去⼀个固定值,此⽅案最⾼可⽀持到2106年。如果每秒6.5万个序列号不够怎么办?没关系,
可以继续递增时间戳,向前“借”下⼀秒的6.5万个序列号。同时还解决了时间回拨的问题。机器标识采⽤简单的主机名⽅案,只要主机名符合host-1,host-2就可以⾃动提取机器标识,⽆需配置。
最后,为什么采⽤最多53位整型,⽽不是64位整型?这是因为考虑到⼤部分应⽤程序是Web应⽤,如果要和JavaScript打交道,由于JavaScript⽀持的最⼤整型就是53位,超过这个位数,JavaScript将丢失精度。因此,使⽤53位整数可以直接由JavaScript读取,⽽超过53位时,就必须转换成字符串才能保证JavaScript处理正确,这会给API接⼝带来额外的复杂度。这也是为什么新浪微博的API接⼝会同时返
回id和idstr的原因
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论