banner
半米牙

半米牙的笔记

分享技术、记录生活
email

主キー生成方法の紹介と生成の長所と短所の比較

システムの各テーブルには、ユニークな主キー ID を格納するための列が必要です。また、このシステムが分散型であり、複数の分散データベースがある場合、各データベース内の ID が重複しないことを保証する必要があります。これにはユニーク ID の特性が求められます:

  1. システム全体で ID がユニークであること
  2. ID は数値型であり、トレンドに従って増加すること
  3. ID は短く、クエリ効率が高いこと

ID を生成する方法はいくつかあり、大規模な企業はもっと複雑な方法を使用していますが、私たちの小さなシステムではこれで十分です。以下にそれぞれ紹介します。

UUID#

これは最も一般的な方法で、ツールクラスのメソッドを使用して UUID を生成します。

利点:

  1. コードの実装が簡単。
  2. ローカルで生成されるため、パフォーマンスの問題がない。
  3. 世界的にユニークな ID であるため、データ移行が容易。

欠点:

  1. 毎回生成される ID は無秩序で、トレンドに従って増加することが保証されない。
  2. UUID の文字列ストレージは、クエリ効率が遅い。
  3. ストレージスペースが大きい。
  4. ID 自体にビジネス上の意味がなく、可読性がない。

適用シーン:

  1. トークン生成のようなシーン。
  2. トレンドに従って増加する ID が必要なシーンには不向き。

MySQL 主キー自増#

この方法も非常に一般的で、設定が簡単で、MySQL の主キー自増 auto_increment を利用して、デフォルトで毎回 ID が 1 ずつ増加します。

利点:

  1. 数値化され、ID が増加する。
  2. クエリ効率が高い。
  3. 一定のビジネス可読性がある。

欠点:

  1. 単一障害点の問題が存在し、MySQL がダウンすると ID を生成できなくなる。
  2. データベースの負荷が大きく、高い同時接続には耐えられない。

MySQL 多インスタンス主キー自増#

uuid1

各インスタンスの初期値はそれぞれ 1,2,3...N で、ステップサイズは N(このケースではステップサイズは 4)。

利点:単一障害点の問題を解決。

欠点:ステップサイズを決定した後は拡張できなくなる。また、単一のデータベースの負荷が大きく、高い同時接続に耐えられない。

適用シーン:データの拡張が必要ないシーン。

雪花 snowflake アルゴリズム#

雪花アルゴリズムは 64 ビットのバイナリ正整数を生成し、それを 10 進数に変換します。64 ビットのバイナリ数は以下の部分で構成されています:

uuid2

  • 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);
		}
	}
}

利点:

  1. この方法は毎秒 409.6 万 ID を生成でき、パフォーマンスが高い。
  2. タイムスタンプが上位にあり、自増シーケンスが下位にあるため、全体の ID はトレンドに従って増加し、時間に従って順序がある。
  3. 柔軟性が高く、ビジネスニーズに応じてビットの割り当てを調整でき、さまざまなニーズに対応できる。

欠点:

  1. マシンの時計に依存しているため、サーバーの時計が戻ると、重複 ID が生成される可能性がある。

分散シーンでは、サーバーの時計が戻ることがよくあり、一般的には 10ms の間に戻ることがあります。皆さんは「この 10ms は短いから考慮しなくてもいいのでは?」と言うかもしれません。しかし、このアルゴリズムはミリ秒単位の生成方法に基づいているため、一度戻ると重複 ID が存在する可能性が非常に高くなります。

Redis 生成方案#

Redis の incr 原子性操作を利用して自増します。一般的なアルゴリズムは:年 + 当日がその年の何日目か + 日数 + 時間 + Redis 自増。

利点:順序に従って増加し、可読性が高い。

欠点:帯域幅を占有し、毎回 Redis にリクエストを送る必要がある。

参考#

  1. 老顾聊技术 - あなたは一流の大企業の分散ユニーク ID 生成方法を知りたいですか?
  2. 永夜微光 - Twitter の分散自増 ID アルゴリズム snowflake (Java 版)
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。