雪花算法基本情况
- 雪花算法是一个分布式的唯一ID生成器。
- 它应该具有高并发,以及高性能优点。
- 基于时间戳,ID具有有序性,同时分布式下机器间时间差异过大(类似同一台机器时间回拨,一定会重复),会导致重复ID。
- 基于机器码和操作中心id,ID具有不可重复性。
- 它的ID是8字节64bit的一个Long长整型数据。
ID基本组成
ID基本组成:
不用: 1bit,因为最高位是符号位,0表示正,1表示负,所以这里固定为0
时间戳: 41bit,服务上线的时间毫秒级的时间戳(为当前时间-服务第一次上线时间),这里为(2^41-1)/1000/60/60/24/365 = 49.7年
工作机器id: 10bit,表示工作机器id,用于处理分布式部署id不重复问题,可支持2^10 = 1024个节点
序列号: 12bit,用于离散同一机器同一毫秒级别生成多条Id时,可允许同一毫秒生成2^12 = 4096个Id,则一秒就可生成4096*1000 = 400w个Id
这样看,在分布式时,通过时间戳+工作机器ID+散列序列号,几乎不会重复,当然你必须保证工作机器ID的准确配置。
源码简易分析以及注意事项
我们以mybatisplus实现的雪花算法为例:
- IdWorker类:
注意:DefaultIdentifierGenerator()这个必须是单例模式的,否者在多线程高并发下,会有重复ID,而且很多。详见IDENTIFIER_GENERATOR.nextId(entity)这个方法实现
package com.baomidou.mybatisplus.core.toolkit;
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
public class IdWorker {
/**单例模式,否者会多个重复Id,
详见IDENTIFIER_GENERATOR.nextId(entity).longValue()*/
private static IdentifierGenerator IDENTIFIER_GENERATOR = new DefaultIdentifierGenerator();
public static final DateTimeFormatter MILLISECOND = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
public IdWorker() {
}
/**获取Id*/
public static long getId() {
return getId(new Object());
}
/**获取Id,最终都会走这一步*/
public static long getId(Object entity) {
return IDENTIFIER_GENERATOR.nextId(entity).longValue();
}
/**机器码和数据中心的ID,参数不能重复*/
public static void initSequence(long workerId, long dataCenterId) {
IDENTIFIER_GENERATOR = new DefaultIdentifierGenerator(workerId, dataCenterId);
}
}
- IDENTIFIER_GENERATOR.nextId(entity).longValue() 方法
nextId是一个线程安全的方法,这也是多线程唯一Id生成的必要之一。
synchronized并不会直接转成重量级锁,因为java在1.6已经优化,因此在资源上没有过分占用,是从偏向锁,轻量级锁,再到重量级锁的过程。
public synchronized long nextId() {
/** long timestamp = this.timeGen();这步代码一定要注意当你DefaultIdentifierGenerator是多例时,这里获取的timestamp不具有真正的唯一性,因为多个实例一起在工作,所以写工具类时不要写成多例 */
long timestamp = this.timeGen();
// 这里是时间异常,针对分布式下,不同机器时间的差异过大
if (timestamp < this.lastTimestamp) {
long offset = this.lastTimestamp - timestamp;
if (offset > 5L) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", offset));
}
try {
this.wait(offset << 1);
timestamp = this.timeGen();
if (timestamp < this.lastTimestamp) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", offset));
}
} catch (Exception var6) {
throw new RuntimeException(var6);
}
}
if (this.lastTimestamp == timestamp) {
this.sequence = this.sequence + 1L & 4095L;
if (this.sequence == 0L) {
// 散列序列号没了,必须转到下一毫秒,采用阻塞
timestamp = this.tilNextMillis(this.lastTimestamp);
}
} else {
// 散列序列号
this.sequence = ThreadLocalRandom.current().nextLong(1L, 3L);
}
this.lastTimestamp = timestamp;
// 或运算连接运算后的时间戳,数据中心,工作中心,序列号的bit位,会被自动返回为一个长整型的数据。
return timestamp - 1288834974657L << 22 | this.datacenterId << 17 | this.workerId << 12 | this.sequence;
}
雪花算法很好用,让我了解了更多的东西,学习了。