自定义雪花算法
约 1248 字大约 4 分钟
2025-01-19
1.雪花算法
1.1雪花算法组成
雪花算法是 64 位 的二进制,一共包含了四部分:
- 1位是符号位,也就是最高位,始终是0,没有任何意义,因为要是唯一计算机二进制补码中就是负数,0才是正数。
- 41位是时间戳,具体到毫秒,41位的二进制可以使用69年,因为时间理论上永恒递增,所以根据这个排序是可以的。
- 10位是机器标识,可以全部用作机器ID,也可以用来标识机房ID + 机器ID,10位最多可以表示1024台机器。、
- 12位是计数序列号,也就是同一台机器上同一时间,理论上还可以同时生成不同的ID,12位的序列号能够区分出4096个ID。
雪花算法优点:
- 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的
- 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高
- 容量大,每秒中能生成数百万的自增ID
雪花算法缺点:
- 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态
- 不是严格全局递增的
1.2实现
我们可以直接使用现成Hutool工具类的雪花算法Snowflake类实现:
/**
*
* @param epochDate 初始化时间节点
* @param workerId 工作机器节点ID
* @param dataCenterId 数据中心ID
* @param isUseSystemClock 是否使用当前时间戳
*/
public Snowflake(Date epochDate, long workerId, long dataCenterId, boolean isUseSystemClock) {
this(epochDate, workerId, dataCenterId, isUseSystemClock, DEFAULT_TIME_OFFSET);
}
1.3测试
public class HuToolSnowflakeIdGenerator implements IdGenerator {
private static final Date EPOC = new Date(2023, 1, 1);
private Snowflake snowflake;
public HuToolSnowflakeIdGenerator(int workId, int datacenter) {
snowflake = new Snowflake(EPOC, workId, datacenter, false);
}
@Override
public Long nextId() {
return snowflake.nextId();
}
public static void main(String[] args) throws InterruptedException {
HuToolSnowflakeIdGenerator huToolSnowflakeIdGenerator = new HuToolSnowflakeIdGenerator(11,1);
for(int i=0;i<20;i++){
if(i%3==0){
Thread.sleep(2);
}
System.out.println(huToolSnowflakeIdGenerator.nextId());
}
}
}1
- 测试结果:
- 生成的ID位数:19位
- 单调递增,同一机器同一毫秒内,序号+1
2.自定义雪花算法
2.1自定义原因
- 精度原因:对于使用hutool工具类生成的ID为19位,为长整型,但是对于前端使用来说,超过16位的数字,会出现精度问题,对于Vue,最后两位数字会变成00(解决方法:后端将长整型转换成String类型),自定义的雪花ID为16位
- **业务原因:**我们希望生成的ID具有标识性,因此自定义的雪花ID的前五位:年+天数
2.2实现
@Slf4j
public class KnowledgeSnowflakeIdGenerator implements IdGenerator {
/**
* 自增序号位数
*/
private static final long SEQUENCE_BITS = 10L;
/**
* 机器位数
*/
private static final long WORKER_ID_BITS = 7L;
private static final long DATA_CENTER_BITS = 3L;
private static final long SEQUENCE_MASK = (1 << SEQUENCE_BITS) - 1;
private static final long WORKER_ID_LEFT_SHIFT_BITS = SEQUENCE_BITS;
private static final long DATACENTER_LEFT_SHIFT_BITS = SEQUENCE_BITS + WORKER_ID_BITS;
private static final long TIMESTAMP_LEFT_SHIFT_BITS = WORKER_ID_LEFT_SHIFT_BITS + WORKER_ID_BITS + DATA_CENTER_BITS;
/**
* 机器id (7位)
*/
private long workId = 1;
/**
* 数据中心 (3位)
*/
private long dataCenter = 1;
/**
* 上次的访问时间
*/
private long lastTime;
/**
* 自增序号
*/
private long sequence;
private byte sequenceOffset;
public PaiSnowflakeIdGenerator() {
try {
String ip = IpUtil.getLocalIp4Address();
String[] cells = StringUtils.split(ip, ".");
this.dataCenter = Integer.parseInt(cells[0]) & ((1 << DATA_CENTER_BITS) - 1);
this.workId = Integer.parseInt(cells[3]) >> 16 & ((1 << WORKER_ID_BITS) - 1);
} catch (Exception e) {
this.dataCenter = 1;
this.workId = 1;
}
}
public PaiSnowflakeIdGenerator(int workId, int dateCenter) {
this.workId = workId;
this.dataCenter = dateCenter;
}
/**
* 生成趋势自增的id
*
* @return
*/
@Override
public synchronized Long nextId() {
long nowTime = waitToIncrDiffIfNeed(getNowTime());
if (lastTime == nowTime) {
if (0L == (sequence = (sequence + 1) & SEQUENCE_MASK)) {
// 表示当前这一时刻的自增数被用完了;等待下一时间点
nowTime = waitUntilNextTime(nowTime);
}
} else {
// 上一毫秒若以0作为序列号开始值,则这一秒以1为序列号开始值
vibrateSequenceOffset();
sequence = sequenceOffset;
}
lastTime = nowTime;
long ans = ((nowTime % DateUtil.ONE_DAY_SECONDS) << TIMESTAMP_LEFT_SHIFT_BITS) | (dataCenter << DATACENTER_LEFT_SHIFT_BITS) | (workId << WORKER_ID_LEFT_SHIFT_BITS) | sequence;
if (log.isDebugEnabled()) {
log.debug("seconds:{}, datacenter:{}, work:{}, seq:{}, ans={}", nowTime % DateUtil.ONE_DAY_SECONDS, dataCenter, workId, sequence, ans);
}
return Long.parseLong(String.format("%s%011d", getDaySegment(nowTime), ans));
}
/**
* 若当前时间比上次执行时间要小,则等待时间追上来,避免出现时钟回拨导致的数据重复
*
* @param nowTime 当前时间戳
* @return 返回新的时间戳
*/
private long waitToIncrDiffIfNeed(final long nowTime) {
if (lastTime <= nowTime) {
return nowTime;
}
long diff = lastTime - nowTime;
AsyncUtil.sleep(diff);
return getNowTime();
}
/**
* 等待下一秒
*
* @param lastTime
* @return
*/
private long waitUntilNextTime(final long lastTime) {
long result = getNowTime();
while (result <= lastTime) {
result = getNowTime();
}
return result;
}
private void vibrateSequenceOffset() {
sequenceOffset = (byte) (~sequenceOffset & 1);
}
/**
* 获取当前时间
*
* @return 秒为单位
*/
private long getNowTime() {
return System.currentTimeMillis() / 1000;
}
/**
* 基于年月日构建分区
*
* @param time 时间戳
* @return 时间分区
*/
private static String getDaySegment(long time) {
LocalDateTime localDate = DateUtil.time2LocalTime(time * 1000L);
return String.format("%02d%03d", localDate.getYear() % 100, localDate.getDayOfYear());
}
}
2.3测试
- 测试结果:
- 生成的ID位数:16位
- 生成的ID前五位:年+天数
- 时间戳从毫秒改为秒
- 同一机器同一秒,序号递增

3.应用场景
我们在新增文章的业务,使用自定义雪花算法来生成文章ID

- 测试结果