システムの各テーブルには、ユニークな主キー 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 にリクエストを送る必要がある。