システムの各テーブルには、ユニークな主キー ID を格納するための列が必要です。また、このシステムが分散型であり、複数の分散データベースがある場合、各データベース内の ID が重複しないことを保証する必要があります。これにはユニーク ID の特性が求められます:
- システム全体で ID がユニークであること
- ID は数値型であり、トレンドに従って増加すること
- ID は短く、クエリ効率が高いこと
ID を生成する方法はいくつかあり、大規模な企業はもっと複雑な方法を使用していますが、私たちの小さなシステムではこれで十分です。以下にそれぞれ紹介します。
UUID#
これは最も一般的な方法で、ツールクラスのメソッドを使用して UUID を生成します。
利点:
- コードの実装が簡単。
- ローカルで生成されるため、パフォーマンスの問題がない。
- 世界的にユニークな ID であるため、データ移行が容易。
欠点:
- 毎回生成される ID は無秩序で、トレンドに従って増加することが保証されない。
- UUID の文字列ストレージは、クエリ効率が遅い。
- ストレージスペースが大きい。
- ID 自体にビジネス上の意味がなく、可読性がない。
適用シーン:
- トークン生成のようなシーン。
- トレンドに従って増加する ID が必要なシーンには不向き。
MySQL 主キー自増#
この方法も非常に一般的で、設定が簡単で、MySQL の主キー自増 auto_increment を利用して、デフォルトで毎回 ID が 1 ずつ増加します。
利点:
- 数値化され、ID が増加する。
- クエリ効率が高い。
- 一定のビジネス可読性がある。
欠点:
- 単一障害点の問題が存在し、MySQL がダウンすると ID を生成できなくなる。
- データベースの負荷が大きく、高い同時接続には耐えられない。
MySQL 多インスタンス主キー自増#
各インスタンスの初期値はそれぞれ 1,2,3...N で、ステップサイズは N(このケースではステップサイズは 4)。
利点:単一障害点の問題を解決。
欠点:ステップサイズを決定した後は拡張できなくなる。また、単一のデータベースの負荷が大きく、高い同時接続に耐えられない。
適用シーン:データの拡張が必要ないシーン。
雪花 snowflake アルゴリズム#
雪花アルゴリズムは 64 ビットのバイナリ正整数を生成し、それを 10 進数に変換します。64 ビットのバイナリ数は以下の部分で構成されています:
- 1 ビットの識別子:常に 0
- 41 ビットのタイムスタンプ:41 ビットのタイムスタンプは現在の時間を保存するものではなく、タイムスタンプの差(現在のタイムスタンプ - 開始タイムスタンプ)を保存します。ここでの開始タイムスタンプは、一般的に私たちの ID 生成器が使用を開始した時間で、プログラムによって指定されます。
- 10 ビットのマシン識別コード:1024 ノードに展開可能で、マシンがデータセンター(IDC)に分散されている場合、この 10 ビットは 5 ビットのデータセンター ID + 5 ビットのマシン ID で構成されます。
- 12 ビットのシーケンス:ミリ秒内のカウントで、12 ビットのカウント順序番号は、各ノードが毎ミリ秒(同じマシン、同じタイムスタンプ)で 4096 個の ID 番号を生成することをサポートします。
実装方法 Java 版:
/**
* Twitter_Snowflake<br>
* SnowFlakeの構造は以下の通りです(各部分は-で区切られています):<br>
* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
* 1ビットの識別子。Javaのlong基本型は符号付きで、最上位ビットが符号ビットで、正数は0、負数は1です。したがって、IDは一般に正数で、最上位ビットは0です。<br>
* 41ビットのタイムスタンプ(ミリ秒単位)。注意:41ビットのタイムスタンプは現在の時間を保存するものではなく、タイムスタンプの差(現在のタイムスタンプ - 開始タイムスタンプ)を保存します。
* ここでの開始タイムスタンプは、一般的に私たちのID生成器が使用を開始した時間で、プログラムによって指定されます(以下のプログラムIdWorkerクラスのstartTime属性)。41ビットのタイムスタンプは69年間使用できます。年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
* 10ビットのデータマシンビットは、1024ノードに展開可能で、5ビットのdatacenterIdと5ビットのworkerIdを含みます。<br>
* 12ビットのシーケンスは、ミリ秒内のカウントで、12ビットのカウント順序番号は、各ノードが毎ミリ秒(同じマシン、同じタイムスタンプ)で4096個のID番号を生成することをサポートします。<br>
* 合計でちょうど64ビットで、Long型になります。<br>
* SnowFlakeの利点は、全体として時間に従って自増順に並び、全体の分散システム内でIDの衝突が発生しない(データセンターIDとマシンIDで区別される)ことです。また、効率が高く、テストによると、SnowFlakeは毎秒約26万IDを生成できます。
*/
public class SnowflakeIdWorker {
// ==============================Fields===========================================
/** 開始タイムスタンプ (2015-01-01) */
private final long twepoch = 1420041600000L;
/** マシンIDが占めるビット数 */
private final long workerIdBits = 5L;
/** データ識別IDが占めるビット数 */
private final long datacenterIdBits = 5L;
/** サポートされる最大マシンID、結果は31(このビットシフトアルゴリズムは、何ビットのバイナリ数が表現できる最大の10進数を迅速に計算できます) */
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;
//==============================Constructors=====================================
/**
* コンストラクタ
* @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;
}
// ==============================Methods==========================================
/**
* 次の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;
//シフトしてOR演算で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();
}
//==============================Test=============================================
/** テスト */
public static void main(String[] args) {
SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
for (int i = 0; i < 1000; i++) {
long id = idWorker.nextId();
System.out.println(Long.toBinaryString(id));
System.out.println(id);
}
}
}
利点:
- この方法は毎秒 409.6 万 ID を生成でき、パフォーマンスが高い。
- タイムスタンプが上位にあり、自増シーケンスが下位にあるため、全体の ID はトレンドに従って増加し、時間に従って順序がある。
- 柔軟性が高く、ビジネスニーズに応じてビットの割り当てを調整でき、さまざまなニーズに対応できる。
欠点:
- マシンの時計に依存しているため、サーバーの時計が戻ると、重複 ID が生成される可能性がある。
分散シーンでは、サーバーの時計が戻ることがよくあり、一般的には 10ms の間に戻ることがあります。皆さんは「この 10ms は短いから考慮しなくてもいいのでは?」と言うかもしれません。しかし、このアルゴリズムはミリ秒単位の生成方法に基づいているため、一度戻ると重複 ID が存在する可能性が非常に高くなります。
Redis 生成方案#
Redis の incr 原子性操作を利用して自増します。一般的なアルゴリズムは:年 + 当日がその年の何日目か + 日数 + 時間 + Redis 自増。
利点:順序に従って増加し、可読性が高い。
欠点:帯域幅を占有し、毎回 Redis にリクエストを送る必要がある。