【Spring Cloudシリーズ】Snowflakeアルゴリズムの原理と実装

【Spring Cloudシリーズ】Snowflakeアルゴリズムの原理と実装

I. 概要

分散型の同時実行性の高い環境では、一般的なチケット予約は 12306 の休日です。多数のユーザーが同じ方向のチケットを購入しようと殺到すると、ミリ秒単位で数万件の注文が生成される可能性があります。生成された注文 ID が一意であることを確認することが重要です。このフラッシュセール環境では、IDの一意性を確保するだけでなく、ID生成の優先順位も確保する必要があります。

2. ID ルールを生成するための厳格な要件の一部

  1. グローバルに一意: ID 番号が重複してはいけない 一意の識別子であるため、これは最も基本的な要件です。
  2. 増加傾向: クラスター化インデックスは MySQL の InnoDB エンジンに適用可能です。ほとんどの RDBMS はインデックス データの保存に B+Tree データ構造を使用するため、主キーの選択では、書き込みパフォーマンスを確保するために順序付けされた主キーを使用するよう努めます。
  3. 単調増加: トランザクションのバージョン番号、並べ替え、その他の特別な要件など、次の ID が前の ID より大きくなければならないことを確認します。
  4. 情報セキュリティ:IDが連続していると悪意のあるユーザーに簡単に捕捉されてしまい、指定されたURLを順番にダウンロードするだけで済みますが、注文番号だと危険です。
  5. タイムスタンプを含む: 生成された ID には完全なタイムスタンプ情報が含まれます。

3. ID番号生成システムの利用要件

  1. 高可用性: 分散 ID を取得するリクエストを送信すると、サーバーは 99.9999% のケースで一意の分散 ID を作成することが保証されます。
  2. 低遅延: 分散 ID を取得するリクエストを送信するには、サーバーが非常に高速である必要があります。
  3. 高 QPS : 100,000 個の分散 ID が一度に要求された場合、サーバーは耐えて 100,000 個の分散 ID を正常に作成する必要があります。

4. 分散IDの一般的な解決策

4.1 UUID

UUID (Universally Unique Identifier) の標準形式には、8-4-4-4-12 の 36 文字の形式で、ハイフンで 5 つのセグメントに分割された 32 桁の 16 進数が含まれます。例: 1E785B2B-111C-752A-997B -3346E7495CE2; UUID のパフォーマンスは非常に高く、ネットワークに依存せず、ローカルで生成されます。

UUID の欠点:

  1. 順序付けされていないため、生成される順序を予測することができず、昇順に数値を生成することもできません。MySql は主キーをできるだけ短くすることを公式に推奨していますが、UUID は 32 ビット文字列であるため、推奨されません。

  2. インデックス、B+Tree インデックスの分割

    分散 ID は主キーであり、主キーはクラスター化インデックスです。Mysql のインデックスは B+Tree によって実装されています。新しい UUID データの挿入とクエリの最適化のために、新しい UUID データが挿入されるたびに、インデックスの下部にある B+Tree が変更されます。UUID データは順序付けされていないため、そのため、UUID データが挿入されるたびに、主キーのクラスタード インデックスが大幅に変更されます。データの挿入を実行すると、主キーが順序どおりに挿入されないため、いくつかの中間ノードが分割され、多数のノードが生成されます。不飽和ノードの。これにより、データベース挿入のパフォーマンスが大幅に低下します。

4.2 データベースの自動インクリメント主キー

スタンドアロン

分散システムでは、データベースの自己増加 ID メカニズムの主な原理は次のとおりです。データベースの自己増加 ID は、MySql データベースに置き換えることによって実装されます。

Replace into の意味は、レコードを挿入することです。テーブル内の一意のインデックスの値が競合した場合、古いデータが置き換えられます。

シングルアプリケーションでは自己増加IDが使用されますが、クラスタ分散アプリケーションではシングルアプリケーションは適していません。

  1. システムを横方向に拡張するのが難しく、例えば、成長ステップサイズやマシン数を定義した後、大量のサーバを追加する場合には初期値に戻す必要があり、操作性が悪いため、システムが水平方向の拡張ソリューションは非常に複雑で、実装が困難です。
  2. データベースには大きな負荷がかかっています。ID を取得するたびにデータベースの読み取りと書き込みが必要になり、パフォーマンスに大きな影響を与えます。分散 ID の低遅延と高 QPS のルールに準拠しません (高同時実行下では、データベースにアクセスして ID を取得すると、パフォーマンスに大きな影響を受けます。)

4.3 Redis に基づいたグローバル ID 戦略の生成

Redis クラスターの場合、MySql と同様に異なる成長ステップを設定する必要があり、キーには有効期間が必要です。Redis クラスターを使用すると、より高いスループットを得ることができます。

5. SnowFlake(スノーフレークアルゴリズム)

Twitter の SnowFlake はこのニーズを解決しました。当初、Twitter はストレージ システムを MySQL から Cassandra (Facebook が開発したオープン ソースの分散型 NoSQL データベース システム) に移行しました。Cassandra にはシーケンシャル ID 生成メカニズムがなかったため、そのようなグローバルに一意な ID のセットを開発しました。 . サービスを生成します。SnowFlake は、1 秒あたり 260,000 の自動増加するソート可能な ID を生成できます。

5.1 SnowFakeの機能

  1. Twitter の SnowFlake 生成 ID は時間順に生成できます。
  2. SnowFlake アルゴリズムによって生成された ID の結果は、Long 型の 64 ビット整数です (文字列への変換後の最大長は 19 です)。
  3. 分散システムでは ID の衝突 (データセンターとワーカー ID によって区別される) が発生せず、効率が高くなります。

5.2 スノーフレークの構造

ここに画像の説明を挿入します

5.3 スノーフレークアルゴリズムの原理

スノーフレーク アルゴリズムの原理は、64 ビット長の型の一意の ID を生成することです。

  1. 生成される ID は正の整数であるため、最上位の 1 ビットは固定値 0 になります。1 の場合は負の値になります。
  2. ミリ秒のタイムスタンプを格納する 41 ビットが続きます (2^41/(1000 * 60 * 24 * 365) = 69)。これは約 69 年間使用できます。
  3. 次の 10 桁には、5 桁の DataCenterId と 5 桁の WorkerId を含むマシン コードが格納されます。最大 2^10=1024 台のマシンをデプロイできます。
  4. 最後の 12 ビットにはシーケンス番号が格納されます。同じミリ秒タイムスタンプが使用される場合、この増分シーケンス番号によって区別されます。つまり、同じマシンの同じミリ秒タイムスタンプでは、2^12=4096 個の一意の ID を使用できます。生成された。

Snowflake アルゴリズムは別のサービスとしてデプロイでき、グローバルに一意の ID が必要なシステムの場合は、Snowflake アルゴリズム サービスに ID を取得するようリクエストするだけです。

Snowflake アルゴリズム サービスごとに、最初に 10 桁のマシン コードを指定する必要がありますが、これは自社のビジネスに応じて設定できます。たとえば、コンピュータ室番号 + マシン番号、マシン番号 + サービス番号、または識別子を区別するその他の 10 桁の整数です。

5.4 アルゴリズムの実装

package com.goyeer;
import java.util.Date;

/**
 * @ClassName: SnowFlakeUtil
 * @Author: goyeer
 * @Date: 2023/09/09 19:34
 * @Description:
 */
public class SnowFlakeUtil {
    
    

    private static SnowFlakeUtil snowFlakeUtil;
    static {
    
    
        snowFlakeUtil = new SnowFlakeUtil();
    }

    // 初始时间戳(纪年),可用雪花算法服务上线时间戳的值
    //
    private static final long INIT_EPOCH = 1694263918335L;

    // 时间位取&
    private static final long TIME_BIT = 0b1111111111111111111111111111111111111111110000000000000000000000L;

    // 记录最后使用的毫秒时间戳,主要用于判断是否同一毫秒,以及用于服务器时钟回拨判断
    private long lastTimeMillis = -1L;

    // dataCenterId占用的位数
    private static final long DATA_CENTER_ID_BITS = 5L;

    // dataCenterId占用5个比特位,最大值31
    // 0000000000000000000000000000000000000000000000000000000000011111
    private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS);

    // dataCenterId
    private long dataCenterId;

    // workId占用的位数
    private static final long WORKER_ID_BITS = 5L;

    // workId占用5个比特位,最大值31
    // 0000000000000000000000000000000000000000000000000000000000011111
    private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);

    // workId
    private long workerId;

    // 最后12位,代表每毫秒内可产生最大序列号,即 2^12 - 1 = 4095
    private static final long SEQUENCE_BITS = 12L;

    // 掩码(最低12位为1,高位都为0),主要用于与自增后的序列号进行位与,如果值为0,则代表自增后的序列号超过了4095
    // 0000000000000000000000000000000000000000000000000000111111111111
    private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);

    // 同一毫秒内的最新序号,最大值可为 2^12 - 1 = 4095
    private long sequence;

    // workId位需要左移的位数 12
    private static final long WORK_ID_SHIFT = SEQUENCE_BITS;

    // dataCenterId位需要左移的位数 12+5
    private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;

    // 时间戳需要左移的位数 12+5+5
    private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;

    /**
     * 无参构造
     */
    public SnowFlakeUtil() {
    
    
        this(1, 1);
    }

    /**
     * 有参构造
     * @param dataCenterId
     * @param workerId
     */
    public SnowFlakeUtil(long dataCenterId, long workerId) {
    
    
        // 检查dataCenterId的合法值
        if (dataCenterId < 0 || dataCenterId > MAX_DATA_CENTER_ID) {
    
    
            throw new IllegalArgumentException(
                    String.format("dataCenterId 值必须大于 0 并且小于 %d", MAX_DATA_CENTER_ID));
        }
        // 检查workId的合法值
        if (workerId < 0 || workerId > MAX_WORKER_ID) {
    
    
            throw new IllegalArgumentException(String.format("workId 值必须大于 0 并且小于 %d", MAX_WORKER_ID));
        }
        this.workerId = workerId;
        this.dataCenterId = dataCenterId;
    }

    /**
     * 获取唯一ID
     * @return
     */
    public static Long getSnowFlakeId() {
    
    
        return snowFlakeUtil.nextId();
    }

    /**
     * 通过雪花算法生成下一个id,注意这里使用synchronized同步
     * @return 唯一id
     */
    public synchronized long nextId() {
    
    
        long currentTimeMillis = System.currentTimeMillis();
        System.out.println(currentTimeMillis);
        // 当前时间小于上一次生成id使用的时间,可能出现服务器时钟回拨问题
        if (currentTimeMillis < lastTimeMillis) {
    
    
            throw new RuntimeException(
                    String.format("可能出现服务器时钟回拨问题,请检查服务器时间。当前服务器时间戳:%d,上一次使用时间戳:%d", currentTimeMillis,
                            lastTimeMillis));
        }
        if (currentTimeMillis == lastTimeMillis) {
    
    
            // 还是在同一毫秒内,则将序列号递增1,序列号最大值为4095
            // 序列号的最大值是4095,使用掩码(最低12位为1,高位都为0)进行位与运行后如果值为0,则自增后的序列号超过了4095
            // 那么就使用新的时间戳
            sequence = (sequence + 1) & SEQUENCE_MASK;
            if (sequence == 0) {
    
    
                currentTimeMillis = getNextMillis(lastTimeMillis);
            }
        } else {
    
     // 不在同一毫秒内,则序列号重新从0开始,序列号最大值为4095
            sequence = 0;
        }
        // 记录最后一次使用的毫秒时间戳
        lastTimeMillis = currentTimeMillis;
        // 核心算法,将不同部分的数值移动到指定的位置,然后进行或运行
        // <<:左移运算符, 1 << 2 即将二进制的 1 扩大 2^2 倍
        // |:位或运算符, 是把某两个数中, 只要其中一个的某一位为1, 则结果的该位就为1
        // 优先级:<< > |
        return
                // 时间戳部分
                ((currentTimeMillis - INIT_EPOCH) << TIMESTAMP_SHIFT)
                        // 数据中心部分
                        | (dataCenterId << DATA_CENTER_ID_SHIFT)
                        // 机器表示部分
                        | (workerId << WORK_ID_SHIFT)
                        // 序列号部分
                        | sequence;
    }

    /**
     * 获取指定时间戳的接下来的时间戳,也可以说是下一毫秒
     * @param lastTimeMillis 指定毫秒时间戳
     * @return 时间戳
     */
    private long getNextMillis(long lastTimeMillis) {
    
    
        long currentTimeMillis = System.currentTimeMillis();
        while (currentTimeMillis <= lastTimeMillis) {
    
    
            currentTimeMillis = System.currentTimeMillis();
        }
        return currentTimeMillis;
    }

    /**
     * 获取随机字符串,length=13
     * @return
     */
    public static String getRandomStr() {
    
    
        return Long.toString(getSnowFlakeId());
    }

    /**
     * 从ID中获取时间
     * @param id 由此类生成的ID
     * @return
     */
    public static Date getTimeBySnowFlakeId(long id) {
    
    
        return new Date(((TIME_BIT & id) >> 22) + INIT_EPOCH);
    }

    public static void main(String[] args) {
    
    
        SnowFlakeUtil snowFlakeUtil = new SnowFlakeUtil();
        long id = snowFlakeUtil.nextId();

        System.out.println(id);
        Date date = SnowFlakeUtil.getTimeBySnowFlakeId(id);
        System.out.println(date);
        long time = date.getTime();
        System.out.println(time);
        System.out.println(getRandomStr());

    }

}

5.4 スノーフレークアルゴリズムの利点

  1. 同時実行性の高い分散環境で一意の ID を生成し、1 秒あたり数百万の一意の ID を生成できます。
  2. タイムスタンプと、同じタイムスタンプの下でのシーケンス番号の自動増分に基づいて、基本的に ID が規則正しく増加することが保証されます。
  3. サードパーティのライブラリやミドルウェアに依存しません。
  4. アルゴリズムはシンプルで、メモリ内で実行され、非常に効率的です。

5.5 スノーフレーク アルゴリズムの欠点:

  1. サーバーの時刻によっては、サーバーの時刻を戻すと重複した ID が生成される場合があります。このアルゴリズムは、最後の ID が生成されたときのタイムスタンプを記録することで解決できます。各 ID を生成する前に、重複した ID の生成を避けるために現在のサーバーのクロックが設定されているかどうかを比較します。

6. まとめ

実際、スノーフレーク アルゴリズムの各部分が占めるビット数は固定されていません。たとえば、ビジネスが 69 年を経ていない可能性があるため、タイムスタンプが占有するビット数を減らすことができます。Snowflake アルゴリズム サービスが 1024 を超えるノードをデプロイする必要がある場合は、削減されたビット数をマシン コードに使用できます。

Snowflake アルゴリズムの 41 ビットは、現在のサーバーのミリ秒タイムスタンプを保存するために直接使用されるわけではありませんが、現在のサーバーのタイムスタンプから初期タイムスタンプ値を引いた値が必要であることに注意してください。一般に、サービスのオンライン時刻は、初期タイムスタンプ値として使用できます。

マシンコードは、コンピュータ室番号、サーバー番号、事業所番号、マシンIPなど、状況に応じて調整できます。デプロイされたさまざまなスノーフレーク アルゴリズム サービスについて、最終的に計算されたマシン コードを区別できます。

おすすめ

転載: blog.csdn.net/songjianlong/article/details/132782298