https://blog.csdn.net/wangming520liwei/article/details/80843248
ID 生成器 雪花算法
我们的业务需求中通常有需要一些唯一的ID,来记录我们某个数据的标识:
-
某个用户的ID
-
某个订单的单号
-
某个信息的ID
看图理解
详细的看代码注释
-
1bit:一般是符号位,不做处理
-
41bit:用来记录时间戳,这里可以记录69年,如果设置好起始时间比如今年是2018年,那么可以用到2089年,到时候怎么办?要是这个系统能用69年,我相信这个系统早都重构了好多次了。
-
10bit:10bit用来记录机器ID,总共可以记录1024台机器,一般用前5位代表数据中心,后面5位是某个数据中心的机器ID
-
12bit:循环位,用来对同一个毫秒之内产生不同的ID,12位可以最多记录4095个,也就是在同一个机器同一毫秒最多记录4095个,多余的需要进行等待下毫秒。
-
public class SnowflakeIdWorker {
-
-
/**
-
* 雪花算法解析 结构 snowflake的结构如下(每部分用-分开):
-
* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
-
* 第一位为未使用,接下来的41位为毫秒级时间(41位的长度可以使用69年),然后是5位datacenterId和5位workerId(10
-
* 位的长度最多支持部署1024个节点) ,最后12位是毫秒内的计数(12位的计数顺序号支持每个节点每毫秒产生4096个ID序号)
-
*
-
* 一共加起来刚好64位,为一个Long型。(转换成字符串长度为18)
-
*
-
*/
-
-
// ==============================Fields===========================================
-
/** 开始时间截 (2015-01-01) */
-
private final long twepoch = 1489111610226L;
-
-
/** 机器id所占的位数 */
-
private final long workerIdBits = 5L;
-
-
/** 数据标识id所占的位数 */
-
private final long dataCenterIdBits = 5L;
-
-
/** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
-
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
-
-
/** 支持的最大数据标识id,结果是31 */
-
private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits);
-
-
/** 序列在id中占的位数 */
-
private final long sequenceBits = 12L;
-
-
/** 机器ID向左移12位 */
-
private final long workerIdShift = sequenceBits;
-
-
/** 数据标识id向左移17位(12+5) */
-
private final long dataCenterIdShift = sequenceBits + workerIdBits;
-
-
/** 时间截向左移22位(5+5+12) */
-
private final long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits;
-
-
/** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
-
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
-
-
/** 工作机器ID(0~31) */
-
private long workerId;
-
-
/** 数据中心ID(0~31) */
-
private long dataCenterId;
-
-
/** 毫秒内序列(0~4095) */
-
private long sequence = 0L;
-
-
/** 上次生成ID的时间截 */
-
private long lastTimestamp = -1L;
-
-
// ==============================Constructors=====================================
-
/**
-
* 构造函数
-
* @param workerId 工作ID (0~31)
-
* @param dataCenterId 数据中心ID (0~31)
-
*/
-
public SnowflakeIdWorker(long workerId, long dataCenterId) {
-
if (workerId > maxWorkerId || workerId < 0) {
-
throw new IllegalArgumentException(String.format("workerId can't be greater than %d or less than 0", maxWorkerId));
-
}
-
if (dataCenterId > maxDataCenterId || dataCenterId < 0) {
-
throw new IllegalArgumentException(String.format("dataCenterId can't be greater than %d or less than 0", maxDataCenterId));
-
}
-
this.workerId = workerId;
-
this.dataCenterId = dataCenterId;
-
}
-
-
// ==============================Methods==========================================
-
/**
-
* 获得下一个ID (该方法是线程安全的)
-
* @return SnowflakeId
-
*/
-
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));
-
}
-
-
// 如果是同一时间生成的,则进行毫秒内序列
-
// sequenceMask 为啥是4095 2^12 = 4096
-
if (lastTimestamp == timestamp) {
-
// 每次+1
-
sequence = (sequence + 1) & sequenceMask;
-
// 毫秒内序列溢出
-
if (sequence == 0) {
-
// 阻塞到下一个毫秒,获得新的时间戳
-
timestamp = tilNextMillis(lastTimestamp);
-
}
-
}
-
// 时间戳改变,毫秒内序列重置
-
else {
-
sequence = 0L;
-
}
-
-
// 上次生成ID的时间截
-
lastTimestamp = timestamp;
-
-
// 移位并通过或运算拼到一起组成64位的ID
-
// 为啥时间戳减法向左移动22 位 因为 5位datacenterid
-
// 为啥 datCenterID向左移动17位 因为 前面有5位workid 还有12位序列号 就是17位
-
//为啥 workerId向左移动12位 因为 前面有12位序列号 就是12位
-
System.out.println(((timestamp - twepoch) << timestampLeftShift) //
-
| (dataCenterId << dataCenterIdShift) //
-
| (workerId << workerIdShift) //
-
| sequence);
-
return ((timestamp - twepoch) << timestampLeftShift) //
-
| (dataCenterId << dataCenterIdShift) //
-
| (workerId << workerIdShift) //
-
| sequence;
-
}
-
-
/**
-
* 阻塞到下一个毫秒,直到获得新的时间戳
-
* @param lastTimestamp 上次生成ID的时间截
-
* @return 当前时间戳
-
*/
-
protected long tilNextMillis(long lastTimestamp) {
-
long timestamp = timeGen();
-
while (timestamp <= lastTimestamp) {
-
timestamp = timeGen();
-
}
-
return timestamp;
-
}
-
-
/**
-
* 返回以毫秒为单位的当前时间
-
* @return 当前时间(毫秒)
-
*/
-
protected long timeGen() {
-
return System.currentTimeMillis();
-
}
-
-
// ==============================Test=============================================
-
/** 测试 */
-
public static void main(String[] args) {
-
System.out.println(System.currentTimeMillis());
-
SnowflakeIdWorker idWorker = new SnowflakeIdWorker(1, 1);
-
long startTime = System.nanoTime();
-
for (int i = 0; i < 50000; i++) {
-
long id = idWorker.nextId();
-
System.out.println(id);
-
}
-
System.out.println((System.nanoTime() - startTime) / 1000000 + "ms");
-
}
-
}
因为机器的原因会发生时间回拨,我们的雪花算法是强依赖我们的时间的,如果时间发生回拨,有可能会生成重复的ID
普通的算法会直接抛出异常,这里我们可以对其进行优化,一般分为两个情况:
-
如果时间回拨时间较短,比如配置5ms以内,那么可以直接等待一定的时间,让机器的时间追上来。
-
如果时间的回拨时间较长,我们不能接受这么长的阻塞等待,那么又有两个策略:
-
直接拒绝,抛出异常,打日志,通知RD时钟回滚。
-
利用扩展位,上面我们讨论过不同业务场景位数可能用不到那么多,那么我们可以把扩展位数利用起来了,比如当这个时间回拨比较长的时候,我们可以不需要等待,直接在扩展位加1。2位的扩展位允许我们有3次大的时钟回拨,一般来说就够了,如果其超过三次我们还是选择抛出异常,打日志。