背景
在数据量指数级增长的公司中,单机数据库已经不能满足需求了,开始使用了分布式架构。但是分布式架构带来了一系列问题,ID的生成方式就变成了其中一个问题。传统的auto_crement在分布式中会造成id冲突,而UUID,又会造成广泛的页分裂。雪花算法便是广泛应用的解决方案。
结构
雪花算法是Twitter公司采用的开源的id生成算法。雪花算法晖生成一个64位的long行整数,这里的位是二进制位,不是十进制位。
1位的符号位:1表示负数,0表示正数,所以基本不用关心。
41位的时间戳:精确到毫秒,可以保障id的顺序性。
10位的机器码:最多可以支持配置1024台机器。包括5位的数据中心id和5位的机器id。
12位的序列号:每毫秒都会重置,表示1毫秒内生成的第几个id。
private Long twepoch = 1288834974657L;
...
public synchronized Long nextId() {
...
((timestamp - twepoch) << timestampLeftShift)
...
}
在计算时间戳的时候,引入了一个变量twepoch。引入twepoch是为了让算法可以使用的尽量久一些。如果单纯使用41位表示当前时间戳的话,41位的最大值转换为十进制是2199023255551。再转换成时间:2039-09-07 23:47:35,意味着这个算法只能使用到2039年。引入twepoch后,根据twepoch设置的值,41位时间戳足可以使用69年。
public synchronized Long nextId() {
...
// 出去当前毫秒内,序列号不需要重置
if (lastTimestamp == timestamp) {
// 确保序列号处于0~4095
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
...
}
...
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
这块是计算序列号逻辑的代码。对于当前时间戳与上次生成id的时间戳相等,sequence就会继续+1,否则sequence会重置为0。当然会出现序列号分配不够的问题,为了预防这种情况的发生,代码中使用(sequence + 1)与sequenceMask进行按位与运算,保证sequence处于0~4095之间。如果4095不够了,那么只能调用tilNextMillis函数等到下一毫秒再继续生成了。
代码
public class IdWorker {
// 机器id
private long workerId;
// 数据中心id
private long datacenterId;
// 12位的序列号
private long sequence;
// 初始时间戳-自定义
private Long twepoch = 1288834974657L;
// 机器ID二进制位数-5位
private Long workerIdBits = 5L;
// 数据中心ID二进制位数-5位
private Long datacenterIdBits = 5L;
// 机器ID的最大值-31
private Long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 数据中心ID的最大值-31
private Long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
// 序列号二进制位数-12位
private Long sequenceBits = 12L;
// 机器ID左移位数-12位
private Long workerIdShift = sequenceBits;
// 数据中心ID左移位数-17位
private Long datacenterIdShift = sequenceBits + workerIdBits;
// 时间戳左移位数-22位
private Long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
// 序列号最大值-4095
private Long sequenceMask = -1L ^ (-1L << sequenceBits);
// 上次时间戳
private Long lastTimestamp = -1L;
public IdWorker(long workerId, long datacenterId, long sequence) {
// 校验workId合法性
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
// 校验datacenterId合法性
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
this.sequence = sequence;
}
public synchronized Long nextId() {
// 获取当前时间戳
long timestamp = timeGen();
// 获取当前时间戳如果小于上次时间戳,则表示时间戳获取出现异常
if (timestamp < lastTimestamp) {
System.err.printf("clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
lastTimestamp - timestamp));
}
// 出去当前毫秒内,序列号不需要重置
if (lastTimestamp == timestamp) {
// 确保序列号处于0~4095
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
// 刷新上次时间戳。
lastTimestamp = timestamp;
return ((timestamp - twepoch) << timestampLeftShift) |
(datacenterId << datacenterIdShift) |
(workerId << workerIdShift) |
sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
public static void main(String[] args) {
IdWorker idWorker = new IdWorker(10, 20, 0);
for (int i = 0; i <10000; i++) {
log.info("" + idWorker.nextId());
}
}
}
雪花算法比UUID强在哪?
雪花算法是单机有序的,UUID是完全无序的。顺序ID的优势是减少页分裂。
代码方面参考自:理解分布式id生成算法SnowFlake - SegmentFault 思否