雪花算法原理
雪花算法是一个分布式的ID生成算法,生成的ID是一个64bit
的Long
类型的数字
64位组成部分 = 符号位(1bit) + 时间戳(41bit) + 机器ID(10bit) + 序列号(12bit)
符号位
第一部分是符号位, 因为是正数, 所以始终为0
时间戳
时间戳(Unix时间戳是从1970年1月1日(UTC/GMT的午夜)开始所经过的秒数, 不考虑闰秒)
第二部分是41bit
长度的时间戳, 可以表示的范围是: 0
到(2^41 - 1)
, 也就是说41bit
可以表示的最大值=(2^41 - 1)
将毫秒数转化成年 : (2^41 - 1) / (1L*1000*60*60*24*365) ≈ 69
注意: (2^41-1) = 2199023255551
毫秒, 将其转换成时间是2039-09-07 23:47:35
, 距离现在都不到20年, 为什么前面算出来的是69年?
我们会设置一个起始时间, 比如2022-01-01 00:00:00
, 生成ID的时候, 计算当前时间戳与起始时间戳的差值, 这个差值最多能保存69年
机器ID
第三部分是10bit
的机器ID, 取值范围为: 0
到(2^10-1)
, 即0-1023
, 最多支持1024
台机器
序列号
第四部分是12bit
的序列ID, 取值范围为: 0到(2^12 - 1)
, 即0-4095
, 就是同一毫秒内支持4096
个id
优缺点
优点
- 基于时间戳,同一时间戳下序列号自增,保证了有序性
- 不依赖第三方库或者中间件,算法简单
缺点
依赖服务器时间,服务器时钟回拨时可能会生成重复 id
Java代码
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class MySnowFlake {
/**
* 起始的时间戳
* 2022-10-10 08:00:00
*/
private final static long START_TIMESTAMP = 1665360000000L;
/**
* 每一部分占用的位数
*/
// 序列号占用的位数
private final static long SEQUENCE_BIT = 12;
// 机器ID占用的位数
private final static long MACHINE_BIT = 10;
/**
* 每一部分的最大值
*/
// 序列号最大值为 4095
private final static long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);
// 机器ID最大值为 1023
private final static long MAX_MACHINE_NUM = ~(-1L << MACHINE_BIT);
/**
* 每一部分向左的位移
*/
// 机器ID向左位移的位数 = 序列号的位数
private final static long MACHINE_SHIFT = SEQUENCE_BIT;
// 时间戳向左位移的位数 = 机器ID向左位移的位数 + 机器ID的位数
private final static long TIMESTAMP_SHIFT = MACHINE_SHIFT + MACHINE_BIT;
// 机器ID
private long machineId;
// 序列号
private long sequence = 0L;
// 上一次时间戳
private long lastTimeStamp = -1L;
/**
* 根据机器ID生成指定的序列号
* @param machineId 机器ID
*/
public MySnowFlake(long machineId) {
if (machineId > MAX_MACHINE_NUM || machineId < 0) {
throw new IllegalArgumentException("MachineId can't be greater than MAX_MACHINE_NUM or less than 0!");
}
this.machineId = machineId;
}
/**
* 产生下一个ID
* 使用 synchronized 保证同一时刻只能有一个线程访问
*/
public synchronized long nextId() {
long currTimeStamp = getNewTimeStamp();
if (currTimeStamp < lastTimeStamp) {
// 出现时钟回拨
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
if (currTimeStamp == lastTimeStamp) {
// 相同毫秒内, 序列号自增, 这里相当于是取模操作
sequence = (sequence + 1) & MAX_SEQUENCE;
// 同一毫秒的序列数已经达到最大
if (sequence == 0L) {
currTimeStamp = getNextMill();
}
} else {
// 不同毫秒内,序列号置为0
sequence = 0L;
}
lastTimeStamp = currTimeStamp;
/**
* 这里作用主要是通过左移将数据移到对应的位置, 再通过 | 操作将数据拼接起来
* | 操作 如果相对应位都是 0, 则结果为 0, 否则为 1
*/
return (currTimeStamp - START_TIMESTAMP) << TIMESTAMP_SHIFT // 时间戳部分
| machineId << MACHINE_SHIFT // 机器标识部分
| sequence; // 序列号部分
}
/**
* 当同一毫秒内的序列号达到最大时,使用while循环延迟时间使得 currTimeStamp > lastTimeStamp
*/
private long getNextMill() {
long mill = getNewTimeStamp();
while (mill <= lastTimeStamp) {
mill = getNewTimeStamp();
}
return mill;
}
/**
* 获取当前时间戳
*/
private long getNewTimeStamp() {
return System.currentTimeMillis();
}
public static String formatByDateTimeMsPattern(Date date) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
return sdf.format(date);
}
public static String parseId(long id) {
long _sequence = id & MAX_SEQUENCE;
long _machineId = (id & (MAX_MACHINE_NUM << MACHINE_SHIFT)) >> MACHINE_SHIFT;
long _time_stamp = id >>> (MACHINE_SHIFT + MACHINE_BIT) ;
String thatTimeStr = formatByDateTimeMsPattern(new Date(START_TIMESTAMP + _time_stamp));
return String.format("{\"ID\":\"%d\",\"timestamp\":\"%s\",\"workerId\":\"%d\",\"sequence\":\"%d\"}",
id, thatTimeStr, _machineId, _sequence);
}
public static void main(String[] args) {
System.out.println(~(-1L << MACHINE_BIT));
MySnowFlake snowFlake = new MySnowFlake( 1023);
List<Long> idList = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
idList.add(snowFlake.nextId());
}
idList.forEach(id -> {
System.out.println(parseId(id));
});
}
}
UidGenerator
文档地址
UidGenerator
是Java实现的, 基于Snowflake算法的唯一ID生成器。UidGenerator
以组件形式工作在应用项目中, 支持自定义workerId
位数和初始化策略, 从而适用于docker等虚拟化环境下实例自动重启、漂移等场景。 在实现上, UidGenerator
通过借用未来时间来解决sequence
天然存在的并发限制; 采用RingBuffer
来缓存已生成的UID
, 并行化UID
的生产和消费, 同时对CacheLine
补齐,避免了由RingBuffe
r带来的硬件级「伪共享」问题. 最终单机QPS可达600万。
依赖版本:Java8及以上版本, MySQL(内置WorkerID分配器, 启动阶段通过DB进行分配; 如自定义实现, 则DB非必选依赖)
Snowflake算法
Snowflake算法描述:指定机器 & 同一时刻 & 某一并发序列,是唯一的。据此可生成一个64 bits的唯一ID(long)。默认采用上图字节分配方式:
-
sign(1bit)
固定1bit符号标识,即生成的UID为正数。 -
delta seconds (28 bits)
当前时间,相对于时间基点"2016-05-20"的增量值,单位:秒,最多可支持约8.7年 -
worker id (22 bits)
机器id,最多可支持约420w次机器启动。内置实现为在启动时由数据库分配,默认分配策略为用后即弃,后续可提供复用策略。 -
sequence (13 bits)
每秒下的并发序列,13 bits可支持每秒8192个并发。
以上参数均可通过Spring进行自定义
CachedUidGenerator
RingBuffer环形数组,数组每个元素成为一个slot。RingBuffer容量,默认为Snowflake算法中sequence最大值,且为2^N。可通过boostPower配置进行扩容,以提高RingBuffer 读写吞吐量。
Tail指针、Cursor指针用于环形数组上读写slot:
-
Tail指针
表示Producer生产的最大序号(此序号从0开始,持续递增)。Tail不能超过Cursor,即生产者不能覆盖未消费的slot。当Tail已赶上curosr,此时可通过rejectedPutBufferHandler指定PutRejectPolicy -
Cursor指针
表示Consumer消费到的最小序号(序号序列与Producer序列相同)。Cursor不能超过Tail,即不能消费未生产的slot。当Cursor已赶上tail,此时可通过rejectedTakeBufferHandler指定TakeRejectPolicy
CachedUidGenerator采用了双RingBuffer,Uid-RingBuffer用于存储Uid、Flag-RingBuffer用于存储Uid状态(是否可填充、是否可消费)
由于数组元素在内存中是连续分配的,可最大程度利用CPU cache以提升性能。但同时会带来「伪共享」FalseSharing问题,为此在Tail、Cursor指针、Flag-RingBuffer中采用了CacheLine 补齐方式。
RingBuffer填充时机
-
初始化预填充
RingBuffer初始化时,预先填充满整个RingBuffer. -
即时填充
Take消费时,即时检查剩余可用slot量(tail - cursor),如小于设定阈值,则补全空闲slots。阈值可通过paddingFactor来进行配置,请参考Quick Start中CachedUidGenerator配置 -
周期填充
通过Schedule线程,定时补全空闲slots。可通过scheduleInterval配置,以应用定时填充功能,并指定Schedule时间间隔