一.什么是雪花算法
Snowflake算法是Twitter开源的分布式ID生成算法。核心思想是,使用一个64 bit 的 long 类型的数字作为全局唯一ID。算法中还引入了时间戳,基本保证了自增特性。
二.为什么需要 Snowflake
在分布式系统中,如果使用的是数据库的自增ID,会出现这些问题:
-
多个节点无法共享同一自增序列
数据库的自增 ID 通常是由单个数据库实例内部维护的。当系统演化为分布式架构,部署了多个数据库或服务节点时,各节点的自增计数器相互独立,都会从 1 开始递增。这样会导致不同节点生成的 ID 出现重复,从而无法保证全局唯一性。
-
数据库成为性能瓶颈
如果让所有服务节点都访问同一个数据库的自增序列来获取 ID,那么每次生成 ID 都需要一次数据库访问。随着并发量提升,数据库就会成为全局的性能瓶颈,甚至在高并发场景下可能因过载而崩溃。
-
ID 不具备时间含义
数据库自增 ID 虽然是递增的,但只反映插入顺序,而不包含任何时间信息。因此,当需要根据 ID 判断数据生成时间或顺序(例如订单创建顺序、日志时间顺序)时,无法直接实现。
而 Snowflake 可以让多个节点独立生成唯一 ID,不需要依赖中心数据库。
三.Snowflake ID的结构
Snowflake算法生成ID的结果是一个64bit大小的整数,结构如下:

-
第一部分:
1个bit,无意义,固定为0。二进制中最高位是符号位,1表示负数,0表示整数。ID都是正数,所以固定为0。 -
第二部分:
41个bit,表示相对于某个基准时间的毫秒数,用于体现时间顺序。时间戳部分具备天然的自增特性,是整个 ID 的主排序依据。 -
第三部分:
10个bit,表示10位的机器标识,最多支持2^10=1024个节点。一般分为5位datacenterId和5位workerId。datacenterId表示数据中心ID,workerId表示机器ID。 -
第四部分:
12个bit,表示同一毫秒内的自增序列号,通过这个递增的序列号区分。即对于同一台机器而言,同一毫秒时间戳下,可以生成2^12=4096个不重复的ID。
四.Snowflake算法特性
由于在Java中64bit的整数是long类型,所以在Java中SnowFlake算法生成的ID就是long来存储的。
优点
-
高并发分布式环境下生成不重复ID,每个节点都有独立标识,通过时间戳+机器号+序列号组合生成唯一ID。每秒可以生成百万个不重复ID
-
基于时间戳,以及同一时间戳下序列号自增,基本保证ID有序递增
-
不依赖第三方库或者中间件
-
算法简单,在内存中进行,效率高
缺点
依赖服务器时间,当服务器发生时钟回拨(即系统时间被人为调整或出现异常跳变)时,可能导致生成的时间戳比上一次生成ID的时间戳更小,从而造成ID重复的风险。算法中可通过记录最后一个生成ID时的时间戳来解决,在每次生成新ID之前比较当前服务器时钟是否被回拨,避免生成重复ID。
-
若当前时间 ≥ 上次时间戳,则正常生成ID;
-
若当前时间 < 上次时间戳(即发生了时钟回拨),则采取容错策略,如:
-
等待时钟追上(自旋等待)
while (timestampNow < this.lastTimestamp) { timestampNow = System.currentTimeMillis(); }实现简单不引入额外的复杂逻辑和状态,但若回拨时间较长,线程会一直阻塞,对高并发服务很不友好,会非常容易造成线程阻塞或雪崩。
-
抛出异常并暂停服务
-
通过引入额外的时间偏移量来校正
-
五.Snowflake算法实现
算法说明
-
自动识别机器信息(IP、主机名)生成dataCenterId和WorkerId
-
支持手动传参自定义节点标识
-
加入时钟回拨容错机制
-
生成Id保证线程安全(synchronized)
1、关键参数配置
起始时间戳、每部分所需bit位、机器表示部分和序列号、每一部分向左的偏移量(用于拼接id)
2、构造函数
提供无参构造和有参构造两种方式
3、自动获取数据中心ID和机器ID
getDataCenterId() 和 getWorkerId() 方法
4、判断用户输入数据中心ID和机器ID是否合法
isVaildDataCenterId() 和 isVaildWorderId() 方法
5、雪花算法核心方法
nextId(),该方法通过时间戳、数据中心ID、机器ID和序列号的组合,生成全局唯一的64位长整型ID,确保在分布式系统中不同节点生成的ID不重复。
核心流程:
-
获取当前时间戳
使用
System.currentTimeMillis()获取当前毫秒时间。 -
检测时钟回拨
若当前时间小于上次生成ID的时间(即发生时钟回拨),根据回拨的时间差(
timestampOffset)采取不同的容错策略:-
≤5ms:短暂回拨,自旋等待。
-
≤1000ms:中度回拨,通过时间偏移补偿。
-
1000ms:严重回拨,直接抛出异常中止服务。
-
-
序列号处理
-
若当前时间与上次相同(同一毫秒内),序列号自增,避免ID冲突。
-
若序列号达到最大值,则等待进入下一毫秒再继续生成。
-
若时间不同(进入下一毫秒),序列号重置为0。
-
-
更新时间戳记录
将当前时间戳保存为
lastTimestamp,用于下次判断是否跨毫秒。 -
通过位移与或运算,将各个部分拼接为最终ID
public class Snowflake {
/*
起始时间戳 2003-01-11 00:00:00(自定义)
*/
private static final long START_TIMESTAMP = 1042214400000L;
/*
时间戳部分,占用41位
*/
private static final long TIMESTAMP_BIT = 41L;
/*
机器标识,占用10位
分为两部分 dateCenter 和 worker
*/
private static final long DATECENTER_BIT = 5L;
private static final long WORKER_BIT = 5L;
/*
序列号,占用12位
*/
private static final long SEQUENCE_BIT = 12L;
/*
机器标识部分、序列号最大值
-1L表示long类型的每一位都是1
左移,相当于右边补几个0
~表示取反,就得到了最大值
*/
private static final long MAX_DATECENTER_ID = ~(-1L << DATECENTER_BIT);
private static final long MAX_WORKER_ID = ~(-1L << WORKER_BIT);
private static final long MAX_SEQUENCE_ID = ~(-1L << SEQUENCE_BIT);
// 数据中心id
private final long dataCenterId;
// 机器id
private final long workerId;
// 序列号,毫秒内序列
private long sequenceId = 0L;
// 上一次生成id的时间戳
private long lastTimestamp = -1L;
// 时间戳偏移量
private long timestampOffset = 0L;
/*
每一部分向左的位移
*/
private final static long WORKER_LEFT = SEQUENCE_BIT;
private final static long DATECENTER_LEFT = WORKER_LEFT + WORKER_BIT;
private final static long TIMESTAMP_LEFT = DATECENTER_LEFT + DATECENTER_BIT;
/**
* 无参构造函数
* 自动获取数据中心id和机器id,不需要使用方传参
*/
public Snowflake() throws UnknownHostException {
this.dataCenterId = getDataCenterId();
this.workerId = getWorkerId();
}
/**
* 提供有参构造函数,可以自己传递数据中心id和机器id
* 如果传递的数据中心id或机器id超出范围或者不合法,则使用自动获取
*
* @param dataCenterId 数据中心id
* @param workerId 机器id
*/
public Snowflake(long dataCenterId, long workerId) throws UnknownHostException {
this.dataCenterId = isVaildDataCenterId(dataCenterId) ? dataCenterId : getDataCenterId();
this.workerId = isVaildWorderId(workerId) ? workerId : getWorkerId();
}
/**
* 判断数据中心id是否合法
* 是否超出最大值或不大于0
*
* @param dataCenterId 数据中心id
* @return true/false
*/
private boolean isVaildDataCenterId(long dataCenterId) {
return dataCenterId <= MAX_DATECENTER_ID && dataCenterId > 0;
}
/**
* 判断机器id是否合法
* 是否超出最大值或不大于0
*
* @param workerId 机器id
* @return true/false
*/
private boolean isVaildWorderId(long workerId) {
return workerId <= MAX_WORKER_ID && workerId > 0;
}
/**
* 获取数据中心id
* 获取机器的ip地址,截取ip地址的末两位,并转换为long类型
* 截取的ip地址末两位可能为0,所以需要判断
* 将获取的ipNum与MAX_DATECENTER_ID进行与运算,保证不超过MAX_DATECENTER_ID
*
* @return 数据中心id
*/
private static long getDataCenterId() throws UnknownHostException {
String ip = InetAddress.getLocalHost().getHostAddress();
char[] ipCharArray = ip.toCharArray();
// 取IP地址的后两位,取非0的
long ipNum = 0L;
int count = 1;
for (int i = ip.length() - 1; i >= 0; i--) {
if (ipCharArray[i] != '.' && ipCharArray[i] != '0') {
ipNum += (long) ((ipCharArray[i] - '0') * Math.pow(10, count));
if (count++ == 2) {
break;
}
}
}
return ipNum & MAX_DATECENTER_ID;
}
/**
* 获取机器id
* 获取主机名,并计算为hashCode
* 将获取的hashCode与MAX_WORKER_ID进行与运算,保证不超过MAX_WORKER_ID
*
* @return 机器id
*/
private static long getWorkerId() throws UnknownHostException {
// 获取主机名
String hostName = InetAddress.getLocalHost().getHostName();
return hostName.hashCode() & MAX_WORKER_ID;
}
/**
* 生成id————雪花算法
* 时间戳+数据中心Id+机器Id+序列号
* 时间戳部分:41bit位,从起始时间戳开始,当前时间戳减去起始时间戳
* 数据中心Id部分:5bit位,使用方传递或者自动获取
* 机器Id部分:5bit位,使用方传递或者自动获取
* 序列号部分:12bit位,从0开始,每毫秒内自增1
* 最后将这些part拼接到一起,采用移位运算拼接组成一个long类型的Id
*
* @return 返回生成的雪花id
*/
public synchronized long nextId() {
// 获取当前时间戳
long timestampNow = System.currentTimeMillis();
// 发生了时钟回拨
if (timestampNow < this.lastTimestamp) {
// 计算偏移量,来判断要采用哪种容错策略
this.timestampOffset = this.lastTimestamp - timestampNow;
if (this.timestampOffset <= 5) { // 小于等于5ms
// 自旋等待
try {
Thread.sleep(this.timestampOffset << 1); // 等待2倍偏移量
timestampNow = System.currentTimeMillis() + this.timestampOffset;
} catch (InterruptedException e) {
throw new RuntimeException("时钟回拨,等待后未恢复");
}
} else if (this.timestampOffset <= 1000) {
// 中度回拨,增加偏移量
this.timestampOffset += this.lastTimestamp - timestampNow;
timestampNow = timestampNow + this.timestampOffset;
} else {
// 回拨时间大于1s,严重回拨,直接抛出异常中断服务
throw new RuntimeException("时钟回拨了" + this.timestampOffset + "毫秒,服务终止!");
}
}
// 判断处理生成序列号
if (timestampNow == this.lastTimestamp) {
// 同一毫秒内,序列号自增(并保证不超过最大值)
this.sequenceId = (this.sequenceId + 1) & MAX_SEQUENCE_ID;
if (this.sequenceId == 0) {// 同一毫秒内序列号已满,等待下一毫秒
while (timestampNow <= this.lastTimestamp) {
timestampNow = System.currentTimeMillis();
}
}
} else {
// 不同毫秒内,序列号置为0
this.sequenceId = 0L;
}
// 记录当前时间戳,作为下一次生成id时间戳的判断依据
this.lastTimestamp = timestampNow;
// 拼接id,或运算、左移
return (timestampNow - START_TIMESTAMP) << TIMESTAMP_LEFT
| (dataCenterId << DATECENTER_LEFT)
| (workerId << WORKER_LEFT)
| (sequenceId);
}
public static void main(String[] args) throws UnknownHostException {
// 测试
int i = 20;
Snowflake snowflake = new Snowflake();
while (i-- > 0) {
System.out.println(snowflake.nextId());
}
}
}
需要注意的是:每次new Snowflake(),会创建一个新的Snowflake 实例。这会导致时间戳(lastTimestamp)会重新初始化为 -1,会使不同的Snowflake 对象创建出来的Id会有重复的。在spring项目中,应将Snowflake交由spring容器统一管理,将其注册成为一个单例Bean,并在需要使用的地方采取依赖注入(@Autowired)的方式使用。
959

被折叠的 条评论
为什么被折叠?



