今天在业务会议上针对数据量问题进行了讨论,在大数据高并发进行存储时Redis生成ID会重复,导致数据库存储失败,再加上是比较重要的数据,所以采用雪花ID进行,任务落在了我的身上,参考了市面上雪花ID生成的逻辑,写了个雪花ID工具类,主要包括获取一个雪花ID和获取多个雪花ID两个方法,工具类贴在下方了,异常处理目前只提供思路,暂未实现,后期进行实现时会更细代码
public class MySnowflakeUtil {
// 时间戳 当前时间减去设置时间 为41为时间戳差值
private static final long START_TIMESTAMP = 1600000000000L;
// 数据中心(机房/环境)所占ID位数
private static final long DATACENTER_ID_BITS = 5L;
// 机器(主机)所占ID位数
private static final long WORKER_ID_BITS = 5L;
// 最大数据中心ID 最大32
private static final long MAX_DATACENTER_ID = -1L ^ (-1L << DATACENTER_ID_BITS);
// 最大机器ID 最大32
private static final long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS);
// 一毫秒内同时生成的id的序号位数
private static final long SEQUENCE_BITS = 12L;
// 序列号最大值 一毫秒最大4095 1s最大数为 4095 * 1000
private static final long SEQUENCE_MASK = -1L ^ (-1L << SEQUENCE_BITS);
// 下面三个属性表明 每个属性 在最终生成ID时要如果操作原时间戳
// 时间左移位数
private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;
// 数据中心ID左移位数
private static final long DATACENTER_ID_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
// 机器ID左移位数
private static final long WORKER_ID_LEFT_SHIFT = SEQUENCE_BITS;
// 上次生成ID的时间戳和序列号
private long lastTimestamp = -1L;
// 用CAS来存储一毫秒内生成ID的最新序列号
private AtomicLong sequence = new AtomicLong(0L);
// 数据中心(机房/环境IP)ID
private long datacenterId;
// 机器(主机IP)ID
private long workerId;
// 已经错误次数
private long ERROR_COUNT = 0L;
// 备用时间
private long STANDBY_DATE;
// 构造方法
public MySnowflakeUtil(long datacenterId, long workerId) {
// 数据中心ID不能为空且大于0小于最大值
if (Objects.isNull(datacenterId) || datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenterId can't be greater than %d or less than 0", MAX_DATACENTER_ID));
}
// 机器ID不能为空且大于0小于最大值
if (Objects.isNull(workerId) || workerId > MAX_WORKER_ID || workerId < 0) {
throw new IllegalArgumentException(String.format("workerId can't be greater than %d or less than 0", MAX_WORKER_ID));
}
this.datacenterId = datacenterId;
this.workerId = workerId;
}
/**
* 获取一个雪花ID
* @return 雪花ID
*/
public synchronized long nextId() {
// 获取当前时间戳
long timestamp = timeGen();
// 判断是否小于最后一次ID生成时间 考虑到获取时间时有补偿机制 当前位置不可能小于最后时间
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
// 如果当前时间等于最后一次生成ID时间
if (lastTimestamp == timestamp) {
// 自旋锁
// 判断当前线程是否被更改 如果未被更改,则将当前属性值+1 并且不能超过最大值
sequence.compareAndSet(sequence.get(), (sequence.get() + 1) & SEQUENCE_MASK);
// 在上方代码中 Id起为0终为0 当=0时认为当前毫秒内ID以增加至0 等待下一毫秒再进行ID生成
if (sequence.get() == 0L) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 如果当前时间和最后生成时间不在一毫秒内 则 认为是新一毫秒
sequence.set(0L);
}
// 设置最后生成时间为当前时间
lastTimestamp = timestamp;
// 生成ID
return ((timestamp - START_TIMESTAMP) << TIMESTAMP_LEFT_SHIFT) |
(datacenterId << DATACENTER_ID_LEFT_SHIFT) |
(workerId << WORKER_ID_LEFT_SHIFT) |
sequence.get();
}
/**
* 获取多个ID 具体数量为count
* @param count 需要生成多少个ID
* @return
*/
public long[] nextIds(int count) {
// 多重考虑 内存开销,安全性,访问速度 采用数组而并非List
long[] ids = new long[count];
for (int i = 0; i < count; i++) {
ids[i] = nextId();
}
return ids;
}
/**
* 获取当前时间戳
* @return 时间戳
*/
// 该方法可以简化 直接返回System.currentTimeMillis(); 可以上面判断当前时间是否小于最后生成时间的位置进行逻辑处理
protected long timeGen() {
// 服务器当前时间
long now = System.currentTimeMillis();
if (now < START_TIMESTAMP || now < lastTimestamp) {
// 当前时间小于开始时间 或者 小于上次生成时间 认为时钟回调
// 采用补偿机制获取最后时间
// 方法一 配置Bean 在启动时 从数据库获取最后一次ID生成时间
// 方法二 @Value 运维人员手动设置开始时间
// 将补偿机制获取的时间赋值给备用时间
if (!Objects.isNull(STANDBY_DATE)) {
// 判断备用时间是否小于 如果备用时间也有问题 则直接抛出异常
if (STANDBY_DATE < START_TIMESTAMP || STANDBY_DATE < lastTimestamp) {
// 如果备用时间也有问题 根据业务需要 判定是否继续执行
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", "获取时间异常"));
}
// 备用时间没有问题 则直接返回备用时间
return STANDBY_DATE;
}
}
return System.currentTimeMillis();
}
/**
* 等待下一个毫秒 ERROR_COUNT
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
}