雪花算法默认算法生成一个 64bit的长整型(Long
)数据。主要由 4部分组成,1bit
符号位、41bit
时间戳位、10bit
工作进程位以及 12bit
序列号位。
正常情况下,该算法可以保证系统中的序号唯一,只是在时钟回拨的时候,有可能会造成id重复的问题。虽然概率不大,单生产系统中,还是尽量避免为好。
将算法做一下调整,仍然由 4部分组成,
1bit
符号位保持不变
41bit
时间戳位,将时间戳变更为31位,2,147,483,647秒,大约68年,时间单位变更为秒,而不是毫秒,起始时间采用2020-01-10 00:00:00为起始时间。
10bit
工作进程位,保持不变
12bit
序列号位,变更为22位,最大值为4,194,300,也就是说,在400万以内,都不会重复;
在算法上,时间戳保持与系统时间同步增加,序列号在最大值以前保持持续增加,最大值后重新开始计数。
package com.demo.server.config;
import cn.hutool.core.net.NetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CustomSnowFlake {
private static Logger log = LoggerFactory.getLogger(CustomSnowFlake.class);
// 2020-01-01 00:00:00 对应的秒
private final static long beginTs = 1577808000L;
// 顺序号最大值
private final static long maxSequence = 4194300L;
// 最大时钟回拨(秒)
private final static long maxTimeback = 4194L;
private long lastTs = 0L;
private long processId;
private int processIdBits = 10;
private long sequence = 1L;
private int sequenceBits = 22;
public CustomSnowFlake() {
String ipAddr = NetUtil.getLocalhostStr();
log.info("当前机器的ipAddr:" + ipAddr);
Long workerId = NetUtil.ipv4ToLong(ipAddr);
workerId = workerId % 1024;
log.info("当前机器的workId:" + workerId);
this.processId = workerId;
}
public CustomSnowFlake(long processId) {
if (processId > ((1 << processIdBits) - 1)) {
throw new RuntimeException("进程ID超出范围,设置位数" + processIdBits + ",最大" + ((1 << processIdBits) - 1));
}
this.processId = processId;
}
// 获取当前时间(秒为单位)
protected long timeGen() {
return System.currentTimeMillis() / 1000;
}
// 生成一个Id
public synchronized long nextId() {
// 获取当前时间(秒为单位)
long ts = timeGen();
// 刚刚生成的时间戳比上次的时间戳还小,出错
long tempDiff = lastTs - ts;
if (tempDiff >= maxTimeback) {
log.warn("时钟回拨超过4194秒,存在Id重复风险");
}
sequence = sequence + 1;
if(sequence >= maxSequence)
{
sequence = 1;
}
// 更新lastTs时间戳
lastTs = ts;
long timeDiff = ts - beginTs;
return (timeDiff << (processIdBits + sequenceBits)) | (processId << sequenceBits) | sequence;
}
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
CustomSnowFlake ig = new CustomSnowFlake();
for (int i = 0; i < 10; i++) {
System.out.println(ig.nextId());
Thread.sleep(1000);
}
}
}
理论上,只要单节点并发量不超过1000次每秒,回拨时间在1小时以内,都不会出现重复记录。为了进一步规避id重复,可以在业务逻辑层面增加重试机制,当出现主键冲突时,重新生成id进行写入尝试。