SNOWFLAKE
SNOWFLAKE
(雪花算法)是默认使用的主键生成方案,生成一个 64bit的长整型(Long
)数据。
sharding-jdbc
中雪花算法生成的主键主要由 4部分组成,1bit
符号位、41bit
时间戳位、10bit
工作进程位以及 12bit
序列号位。
符号位(1bit位)
Java 中 Long 型的最高位是符号位,正数是0,负数是1,一般生成ID都为正数,所以默认为0
时间戳位(41bit)
41位的时间戳可以容纳的毫秒数是 2 的 41次幂,而一年的总毫秒数为 1000L * 60 * 60 * 24 * 365
,计算使用时间大概是69年,额~,我有生之间算是够用了。
Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L) = = 69年
工作进程位(10bit)
表示一个唯一的工作进程id,默认值为 0,可通过 key-generator.props.worker.id
属性设置。
spring.shardingsphere.sharding.tables.t_order.key-generator.props.worker.id=0000
序列号位(12bit)
同一毫秒内生成不同的ID。
时钟回拨
了解了雪花算法的主键 ID 组成后不难发现,这是一种严重依赖于服务器时间的算法,而依赖服务器时间的就会遇到一个棘手的问题:时钟回拨
。
为什么会出现时钟回拨呢?
互联网中有一种网络时间协议 ntp
全称 (Network Time Protocol
) ,专门用来同步、校准网络中各个计算机的时间。
这就是为什么,我们的手机现在不用手动校对时间,可每个人的手机时间还都是一样的。
我们的硬件时钟可能会因为各种原因变得不准( 快了
或 慢了
),此时就需要 ntp
服务来做时间校准,做校准的时候就会发生服务器时钟的 跳跃
或者 回拨
的问题。
雪花算法如何解决时钟回拨
服务器时钟回拨会导致产生重复的 ID,SNOWFLAKE
方案中对原有雪花算法做了改进,增加了一个最大容忍的时钟回拨毫秒数。
如果时钟回拨的时间超过最大容忍的毫秒数阈值,则程序直接报错;如果在可容忍的范围内,默认分布式主键生成器,会等待时钟同步到最后一次主键生成的时间后再继续工作。
最大容忍的时钟回拨毫秒数,默认值为 0,可通过属性 max.tolerate.time.difference.milliseconds
设置。
# 最大容忍的时钟回拨毫秒数
spring.shardingsphere.sharding.tables.t_order.key-generator.max.tolerate.time.difference.milliseconds=5
下面是看下它的源码实现类 SnowflakeShardingKeyGenerator
,核心流程大概如下:
最后一次生成主键的时间 lastMilliseconds
与 当前时间currentMilliseconds
做比较,如果 lastMilliseconds
> currentMilliseconds
则意味着时钟回调了。
那么接着判断两个时间的差值(timeDifferenceMilliseconds
)是否在设置的最大容忍时间阈值 max.tolerate.time.difference.milliseconds
内,在阈值内则线程休眠差值时间 Thread.sleep(timeDifferenceMilliseconds)
,否则大于差值直接报异常。
/**
* @author xiaofu
*/
public final class SnowflakeShardingKeyGenerator implements ShardingKeyGenerator{
@Getter
@Setter
private Properties properties = new Properties();
public String getType() {
return "SNOWFLAKE";
}
public synchronized Comparable<?> generateKey() {
/**
* 当前系统时间毫秒数
*/
long currentMilliseconds = timeService.getCurrentMillis();
/**
* 判断是否需要等待容忍时间差,如果需要,则等待时间差过去,然后再获取当前系统时间
*/
if (waitTolerateTimeDifferenceIfNeed(currentMilliseconds)) {
currentMilliseconds = timeService.getCurrentMillis();
}
/**
* 如果最后一次毫秒与 当前系统时间毫秒相同,即还在同一毫秒内
*/
if (lastMilliseconds == currentMilliseconds) {
/**
* &位与运算符:两个数都转为二进制,如果相对应位都是1,则结果为1,否则为0
* 当序列为4095时,4095+1后的新序列与掩码进行位与运算结果是0
* 当序列为其他值时,位与运算结果都不会是0
* 即本毫秒的序列已经用到最大值4096,此时要取下一个毫秒时间值
*/
if (0L == (sequence = (sequence + 1) & SEQUENCE_MASK)) {
currentMilliseconds = waitUntilNextTime(currentMilliseconds);
}
} else {
/**
* 上一毫秒已经过去,把序列值重置为1
*/
vibrateSequenceOffset();
sequence = sequenceOffset;
}
lastMilliseconds = currentMilliseconds;
/**
* XX......XX XX000000 00000000 00000000 时间差 XX
* XXXXXX XXXX0000 00000000 机器ID XX
* XXXX XXXXXXXX 序列号 XX
* 三部分进行|位或运算:如果相对应位都是0,则结果为0,否则为1
*/
return ((currentMilliseconds - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (getWorkerId() << WORKER_ID_LEFT_SHIFT_BITS) | sequence;
}
/**
* 判断是否需要等待容忍时间差
*/
@SneakyThrows
private boolean waitTolerateTimeDifferenceIfNeed(final long currentMilliseconds) {
/**
* 如果获取ID时的最后一次时间毫秒数小于等于当前系统时间毫秒数,属于正常情况,则不需要等待
*/
if (lastMilliseconds <= currentMilliseconds) {
return false;
}
/**
* ===>时钟回拨的情况(生成序列的时间大于当前系统的时间),需要等待时间差
*/
/**
* 获取ID时的最后一次毫秒数减去当前系统时间毫秒数的时间差
*/
long timeDifferenceMilliseconds = lastMilliseconds - currentMilliseconds;
/**
* 时间差小于最大容忍时间差,即当前还在时钟回拨的时间差之内
*/
Preconditions.checkState(timeDifferenceMilliseconds < getMaxTolerateTimeDifferenceMilliseconds(),
"Clock is moving backwards, last time is %d milliseconds, current time is %d milliseconds", lastMilliseconds, currentMilliseconds);
/**
* 线程休眠时间差
*/
Thread.sleep(timeDifferenceMilliseconds);
return true;
}
// 配置的机器ID
private long getWorkerId() {
long result = Long.valueOf(properties.getProperty("worker.id", String.valueOf(WORKER_ID)));
Preconditions.checkArgument(result >= 0L && result < WORKER_ID_MAX_VALUE);
return result;
}
private int getMaxTolerateTimeDifferenceMilliseconds() {
return Integer.valueOf(properties.getProperty("max.tolerate.time.difference.milliseconds", String.valueOf(MAX_TOLERATE_TIME_DIFFERENCE_MILLISECONDS)));
}
private long waitUntilNextTime(final long lastTime) {
long result = timeService.getCurrentMillis();
while (result <= lastTime) {
result = timeService.getCurrentMillis();
}
return result;
}
}