一、基本概念
雪花算法(Snowflake)是一种生成全局唯一ID的分布式算法。它的主要功能是在分布式系统中生成一个全局唯一的ID,且ID是按照时间有序递增的。
1.核心思想
Snowflake算法使用一个64位的二进制数字作为ID。这64位long型ID被分割成四个部分:符号位、时间戳、工作机器ID、序列号。通过这几部分来表示不同的信息,将数据映射到具有特定结构的分布式系统中,实现数据的存储和查询。
该算法由一系列节点组成,每个节点负责存储数据的一部分。这些节点通过哈希函数将数据映射到特定的位置,形成类似于雪花结构的分布式系统。通过这种方式,雪花算法能够在分布式系统中保证ID的唯一性和有序性。
2.应用场合
雪花算法广泛应用于分布式系统中的唯一ID生成。它可以保证在分布式环境中生成的ID是唯一且有序的。常见的应用场合包括订单号生成、分布式数据库中的数据主键、分布式锁等。通过使用雪花算法生成全局唯一ID,可以方便地进行分布式系统的数据管理和查询。
二、实现原理
雪花算法将64位的long型ID分为四个部分:
1.符号位(1位)
始终为0,用于标识ID是正数。
2.时间戳(41位)
时间戳占据了整个ID的41位,精确到毫秒级,可以支持69年的时间戳。这使得雪花算法能够支持未来数十年的唯一性。时间戳部分还提供了排序的功能,可以根据时间戳来对数据进行排序。
3.机器ID(10位)
工作机器ID占据了ID的10位,可以支持最多1024个工作节点。这使得在同一台机器上运行的不同应用程序实例可以使用不同的工作机器ID来生成唯一的ID。
4.序列号(12位)
序列号占据了ID的12位,可以支持每个节点每毫秒产生4096个唯一的ID。这使得在同一台机器上运行的不同应用程序实例可以生成唯一的ID,即使在毫秒级别内也能保证唯一性。
注:上述划分中的工作机器ID位数(10位或5位)可能会因不同的实现方式而有所差异。在Twitter原始的雪花算法设计中,可能并没有直接使用10位来表示工作机器ID,而是将机器ID部分进一步细分为数据中心ID和工作机器ID,其中每个部分可能占用更少的位数(如各5位)。
三、代码实现
以下是雪花算法的Java代码实现示例:
public class SnowflakeIdWorker{
/** 开始时间截 (2015-01-01) */
private final long twepoch = 1288834974657L;
/** 机器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;
/**
* 构造函数
*
* @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(
"worker Id can't be greater than %d or less than 0",
maxWorkerId));
}
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;
}
/**
* 获得下一个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)));
}
// 如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
// 毫秒内序列溢出
if (sequence == 0) {
// 阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
// 时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
// 上次生成ID的时间截
lastTimestamp = timestamp;
// 移位并通过或运算拼到一起组成64位的ID
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();
}
//测试方法
public static void main(String[] args) {
// 假设我们有一个工作机器ID为1,数据中心ID为1的环境
long workerId = 1L;
long datacenterId = 1L;
// 创建一个SnowflakeIdWorker实例
SnowflakeIdWorker idWorker = new SnowflakeIdWorker(workerId, datacenterId);
// 生成并打印10个ID作为示例
for (int i = 0; i < 10; i++) {
long id = idWorker.nextId();
System.out.println(id);
}
}
}
在这个代码中,首先定义了雪花算法的各个组成部分,包括时间戳、工作机器id、数据中心id和序列号。然后,根据这些组成部分计算出一个唯一的ID。在生成ID的过程中,需要考虑时间戳的回拨问题,如果当前时间小于上一次生成ID的时间戳,那么就抛出一个异常。同时,在同一毫秒内,如果生成的ID数量达到上限(2^12个),那么就等待下一毫秒再生成。