1 はじめに
Snowflake は Twitter のオープン ソースの分散 ID 生成アルゴリズムであり、非反復で順序付けされた自己増加する 64 ビット ID を生成でき、分散システムでの ID 生成のニーズに適しています。
スノーフレーク アルゴリズムの中心となるアイデアは、特定のルールに従って 64 ビット ID を分割することであり、そのうち 41 ビットがタイムスタンプとして使用され、10 ビットがマシン ID として使用され、12 ビットがシリアル番号として使用され、次のことが保証されます。生成された ID はグローバルに一意で、順序付けされており、単独で増やすことができます。
スノーフレーク アルゴリズムの ID は次の部分で構成されます。
-
符号ビット: 1 ビット (常に 0)。正と負の数値を区別するために使用されます。
-
タイムスタンプ: 41 ビット、ミリ秒レベルまで正確です。時差は現在時刻から固定開始時刻を引くことで求められます。タイムスタンプは 41 ビットを占めるため、表現できる最大時間は 2^41 / (1000 * 60 * 60 * 24 * 365) = 約 69 年になります。
-
データセンター ID: 5 ビット。異なるデータセンターを区別するために使用されます。複数のデータセンターがない場合は、0 に設定できます。
-
マシン ID: 5 ビット。同じデータセンター内の異なるマシンを区別するために使用されます。同様に、複数のマシンがない場合は、0 に設定できます。
-
シリアル番号: 12 ビット。同じミリ秒内に生成された異なる ID を区別するために使用されます。シリアル番号は 12 ビットしかないため、表現できる最大シリアル番号は 2^12 - 1 = 4095 です。同じミリ秒内に生成されたシーケンス番号が 4095 を超える場合、新しい ID が生成されるまで次のミリ秒まで待つ必要があります。
要約すると、スノーフレーク アルゴリズムによって生成される ID の長さは 64 ビットであり、分散システム内で確実に一意の ID を生成できます。
2. スノーフレークアルゴリズムの長所と短所
2.1. スノーフレーク アルゴリズムには次のような利点があります。
-
効率: Snowflake アルゴリズムは非常に迅速に ID を生成し、同時実行性の高いシナリオのニーズを満たすことができます。
-
一意性: タイムスタンプやマシン ID などの情報を使用して ID を生成することで、生成された ID の一意性が保証されます。
-
使いやすさ: Snowflake アルゴリズムを使用して ID を生成するコードは比較的シンプルで、既存のシステムに簡単に統合できます。
2.2. スノーフレーク アルゴリズムの欠点は次のとおりです。
-
クロックへの依存: スノーフレーク アルゴリズムによって生成される ID の一意性と正確性は、クロックの正確さに依存します。マシンのクロックに問題がある場合、生成される ID が一意でなくなるか、正しくなくなる可能性があります。
-
マシンの数による制限: Snowflake アルゴリズムの一意性はマシンの数によっても制限されます。クラスター内のマシンの数がアルゴリズム内のマシン ID の範囲を超える場合、生成される ID は一意ではない可能性があります。
-
マシンのパフォーマンスによる制限: マシンのパフォーマンスが低い場合、ID 生成の速度が遅くなり、高い同時実行要件を満たせない可能性があります。
3. Java はスノーフレーク アルゴリズムを実装します
以下は、Java でスノーフレーク アルゴリズムを実装するためのサンプル コードです。
/**
* <h1>Java 雪花算法</h1>
* Created by woniu
*/
public class SnowFlake {
// 起始的时间戳,这个时间戳可以是你的系统初始时间,一般取当前时间戳
private final static long START_TIMESTAMP = 1672502400000L; // 2023-01-01 00:00:00
// 每一部分占用的位数,可以根据自己的需求进行调整,这里是按照默认的占位数进行分配
private final static long SEQUENCE_BIT = 12; // 序列号占用的位数
private final static long MACHINE_BIT = 5; // 机器标识占用的位数
private final static long DATA_CENTER_BIT = 5; // 数据中心占用的位数
// 每一部分的最大值,可以根据占用的位数进行计算得到
private final static long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);
private final static long MAX_MACHINE_NUM = ~(-1L << MACHINE_BIT);
private final static long MAX_DATA_CENTER_NUM = ~(-1L << DATA_CENTER_BIT);
// 每一部分向左的位移,计算出来的值是为了后面生成 ID 做准备
private final static long MACHINE_LEFT = SEQUENCE_BIT;
private final static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;
private long dataCenterId; // 数据中心 ID
private long machineId; // 机器 ID
private long sequence = 0L; // 序列号
private long lastTimeStamp = -1L; // 上一次时间戳
/**
* <h2>构造方法</h2>
* @param dataCenterId 数据中心 ID
* @param machineId 机器 ID
* */
public SnowFlake(long dataCenterId, long machineId) {
if (dataCenterId > MAX_DATA_CENTER_NUM || dataCenterId < 0) {
throw new IllegalArgumentException("数据中心标识不能大于等于 " + MAX_DATA_CENTER_NUM + " 或小于 0");
}
if (machineId > MAX_MACHINE_NUM || machineId < 0) {
throw new IllegalArgumentException("机器标识不能大于等于 " + MAX_MACHINE_NUM + " 或小于 0");
}
this.dataCenterId = dataCenterId;
this.machineId = machineId;
}
/**
* <h2>雪花算法核心方法</h2>
* 通过调用 nextId() 方法,让当前这台机器上的 snowflake 算法程序生成一个全局唯一的 id
* */
public synchronized long nextId() {
// 获取系统当前时间戳
long currentTimeStamp = getSystemCurrentTimeMillis();
if (currentTimeStamp < lastTimeStamp) {
throw new RuntimeException("时钟向后移动,拒绝生成雪花算法ID");
}
if (currentTimeStamp == lastTimeStamp) {
// 当前毫秒内,序列号自增
sequence = (sequence + 1) & MAX_SEQUENCE;
// 序列号超出范围,需要等待下一毫秒
if (sequence == 0L) {
// 获取下一毫秒
currentTimeStamp = getNextMill(lastTimeStamp);
}
} else {
// 不同毫秒内,序列号置为 0
sequence = 0L;
}
lastTimeStamp = currentTimeStamp;
// 使用位运算生成最终的 ID
return (currentTimeStamp - START_TIMESTAMP) << TIMESTAMP_LEFT
| dataCenterId << DATA_CENTER_LEFT
| machineId << MACHINE_LEFT
| sequence;
}
/**
* <h2>获取系统当前时间戳</h2>
* @return 当前时间(毫秒)
*/
private long getSystemCurrentTimeMillis() {
return System.currentTimeMillis();
}
/**
* <h2>获取下一毫秒</h2>
* 当某一毫秒的时间,产生的 id 数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生 ID
* @param lastTimestamp 上次生成 ID 的时间截
* @return 当前时间戳
*/
private long getNextMill(long lastTimestamp) {
long timeMillis = getSystemCurrentTimeMillis();
while(timeMillis <= lastTimestamp){
timeMillis = getSystemCurrentTimeMillis();
}
return timeMillis;
}
/**
* <h2>测试类</h2>
*/
public static void main(String[] args) {
SnowFlake worker1 = new SnowFlake(1,1);
SnowFlake worker2 = new SnowFlake(2,1);
SnowFlake worker3 = new SnowFlake(3,1);
for (int i = 0; i < 30; i++){
System.out.println("数据中心1,雪花算法 ID:" + worker1.nextId());
System.out.println("数据中心2,雪花算法 ID:" + worker2.nextId());
System.out.println("数据中心3,雪花算法 ID:" + worker3.nextId());
}
}
実行結果は次のとおりです。
数据中心1,雪花算法 ID:32120788563922944
数据中心2,雪花算法 ID:32120788568248320
数据中心3,雪花算法 ID:32120788568379392
数据中心1,雪花算法 ID:32120788568117248
数据中心2,雪花算法 ID:32120788568248321
数据中心3,雪花算法 ID:32120788568379393
数据中心1,雪花算法 ID:32120788568117249
数据中心2,雪花算法 ID:32120788568248322
数据中心3,雪花算法 ID:32120788568379394
数据中心1,雪花算法 ID:32120788568117250
数据中心2,雪花算法 ID:32120788568248323
数据中心3,雪花算法 ID:32120788568379395
数据中心1,雪花算法 ID:32120788572311552
数据中心2,雪花算法 ID:32120788572442624
数据中心3,雪花算法 ID:32120788572573696
数据中心1,雪花算法 ID:32120788572311553
数据中心2,雪花算法 ID:32120788572442625
数据中心3,雪花算法 ID:32120788572573697
数据中心1,雪花算法 ID:32120788572311554
数据中心2,雪花算法 ID:32120788572442626
数据中心3,雪花算法 ID:32120788572573698
数据中心1,雪花算法 ID:32120788572311555
数据中心2,雪花算法 ID:32120788572442627
数据中心3,雪花算法 ID:32120788572573699
数据中心1,雪花算法 ID:32120788572311556
数据中心2,雪花算法 ID:32120788572442628
数据中心3,雪花算法 ID:32120788576768000
数据中心1,雪花算法 ID:32120788576505856
従来の自己増加シーケンスまたは UUID と比較して、Snowflake アルゴリズムは分散システムにおける ID の一意性を保証でき、中央ノードの生成に依存せず、マシン ID とタイムスタンプの組み合わせを通じて分散 ID 生成を実現します。これにより、単一障害点とパフォーマンスのボトルネックが回避され、システムのスケーラビリティとパフォーマンスが向上します。
スノーフレーク アルゴリズムの実装も比較的簡単で、各マシンに個別の ID ジェネレーターをデプロイするだけでよく、異なるマシン ID とデータセンター ID を構成することで、グローバルな一意性を保証できます。現在、スノーフレーク アルゴリズムは、Hadoop、Zookeeper、Kafka、Elasticsearch などのさまざまな分散システムで広く使用されています。
Snowflake アルゴリズムは絶対に安全というわけではなく、タイムスタンプの精度やマシン ID の割り当て方法の制限により、タイム ダイヤルバックやマシン ID の重複などの問題が発生する可能性があることに注意してください。したがって、実際のアプリケーションでは、ID のグローバルな一意性と正確性を確保するために、特定のシナリオに従って適切な調整と最適化を行う必要があります。