面接官:どうやってリポジトリのテーブルを移動しますか.


需要説明
受注表のように、ユーザー表のような将来の規模が億から十億までの膨大なデータ表は、プロジェクトの初期に迅速にオンラインになるために、一般的には単表設計にすぎず、分庫分表を考慮する必要はありません.業務の発展に従って、単表の容量は千万を超えて甚だしきに至っては億級以上に達して、この時分庫分表という問題を考慮する必要があります.面接官にこの移転案を話して、面接官はどう思いますか.
codisを参考に
筆者はちょうどこの問題に直面したことがあり、codisのいくつかの思想を参考にして、無停止の分庫分表移行案を実現した.codisはこの文章の重点ではありません.ここではcodisを参考にした場所--rebalanceについてだけ言及します.
移行中にデータアクセスが発生すると、Proxyは「SLOTSGRTTAGSLOT」移行コマンドをRedisに送信し、クライアントがアクセスするKeyをすぐに移行させ、クライアントの要求を処理します.(SLOTS GRTTAGSLOTはcodisがredisに基づいてカスタマイズされている)
ライブラリ表
このシナリオを理解したら、ダウンタイムのないリポジトリのテーブル移行を理解するのが容易になります.次に、筆者がinstalled_について詳しく紹介します.appテーブルの実施形態;すなわち、ユーザーがインストールしたAPP情報テーブルである.
1.sharding columnの決定
sharding columnを決定することは、ライブラリ分割テーブルの最も重要な一環であり、1つもありません.sharding columnは、ライブラリ全体のテーブルスキームが最終的に成功するかどうかを直接決定します.適切なsharding columnの選択は、このテーブルに関連するほとんどのトラフィックインタフェースが、このsharding columnを介してライブラリ分割テーブルの後の単一テーブルにアクセスできるようにすることができます.ライブラリ間テーブルを必要としません.最も一般的なsharding columnはuser_です.id、メモここで選んだのもuser_id;
2.分庫表案
自分のビジネスに応じて最適なsharding columnを選択すると、ライブラリ・テーブル・スキームが決定されます.筆者はアクティブ移行とパッシブ移行を組み合わせた方案を採用する:
プロアクティブ移行は独立したプログラムであり、ライブラリ・テーブルが必要なinstalled_を巡回します.appテーブルは、ライブラリ分割テーブル後のターゲットテーブルにデータを移行します.
パッシブ移行はinstalled_appテーブルに関連するビジネスコード自体は、ライブラリ分割テーブルの後に対応するテーブルにデータを移行します.
次に、この2つの案を詳しく紹介します.
2.1プロアクティブマイグレーションプロアクティブマイグレーションは独立した外部サスペンションマイグレーションプログラムであり、その役割は、ライブラリ分割テーブルが必要なinstalled_を遍歴することである.appテーブルは、ここのデータをライブラリ分割テーブルのターゲットテーブルにコピーし、アクティブ移行とパッシブ移行が一緒に実行されるため、アクティブ移行とパッシブ移行の衝突の問題を処理する必要があります.筆者のアクティブ移行偽コードは以下の通りです.
public void migrate(){
    //          ID,           
    long maxId = execute("select max(id) from installed_app");
    long tempMinId = 0L;
    long stepSize = 1000;
    long tempMaxId = 0L;
    do{
        try {
            tempMaxId = tempMinId + stepSize;
            //   InnoDB    , where id>=? and id=#{tempMinId} and id installedApps = executeSql(scanSql);
            Iterator iterator = installedApps.iterator();
            while (iterator.hasNext()) {
                InstalledApp installedApp = iterator.next();
                // help GC
                iterator.remove();
                long userId = installedApp.getUserId();
                String status = executeRedis("get MigrateStatus:${userId}");
                if ("COMPLETED".equals(status)) {
                    // migration finish, nothing to do
                    continue;
                }
                if ("MIGRATING".equals(status)) {
                    // "    " migrating, nothing to do
                    continue;
                }
                //        : set MigrateStatus:18 MIGRATING ex 3600 nx
                String result = executeRedis("set MigrateStatus:${userId} MIGRATING ex 86400 nx");
                if ("OK".equals(result)) {
                    //       ,             app    [        ID      ]
                    String sql = "select * from installed_app where user_id=#{user_id}";
                    List userInstalledApps = executeSql(sql);
                    //            app           ( user_id              )
                    shardingInsertSql(userInstalledApps);
                    //      ,       
                    executeRedis("setex MigrateStatus:${userId} 864000 COMPLETED");
                } else {
                    //         ,             ,             [      ]
                    //           , "    "           ,                    
                    logger.info("Migration conflict. userId = {}", userId);
                }
            }
            if (tempMaxId >= maxId) {
                //   max(id),          
                maxId = execute("select max(id) from installed_app");
            }
            logger.info("Migration process id = {}", tempMaxId);
        }catch (Throwable e){
            //             (        redis mysql    ),     ,         
            //    tempMinId    logger.info("Migration process id="+tempMaxId);         id,       
            System.exit(0);
        }
        tempMinId += stepSize;
    }while (tempMaxId < maxId);
}

ここで注意すべき点はいくつかあります.
  • 最初のステップでmax(id)をクエリーするのは、max(id)のクエリー回数をできるだけ減らすためであり、最初のクエリーmax(id)が1000000であれば、遍歴したidが1000000になるまでmax(id)を再度クエリーする必要はない.
  • id>=?and id=? limit nまたはlimit m,nは、limitの性能が一般的であり、遍歴が後になるにつれて性能が悪くなるため、遍歴する.id>=?and id
  • id区間範囲に基づいて照会されたListをIteratorに変換し、反復ごとにuserIdを処理し、removeを削除しなければならない.そうしないと、GC異常、さらにはOOMを招く可能性がある.

  • 2.2パッシブ移行パッシブ移行はinstalled_appテーブルに関連するビジネスロジックの前に移行ロジックが挿入され、新規ユーザーがインストールしたAPPを例にとると、その偽コードは以下の通りである.
    //            ,   `installed_app`                  ;
    public void migratePassive(long userId)throws Exception{
        String status = executeRedis("get MigrateStatus:${userId}");
        if ("COMPLETED".equals(status)) {
            //            , nothing to do
            logger.info("user's installed app migration completed. user_id = {}", userId);
        }else if ("MIGRATING".equals(status)) {
            // "    " migrating,         ;        ,             
            do{
                Thread.sleep(10);
                status = executeRedis("get MigrateStatus:${userId}");
            }while ("COMPLETED".equals(status));
        }else {
            //     
            String result = executeRedis("set MigrateStatus:${userId} MIGRATING ex 86400 nx");
            if ("OK".equals(result)) {
                //       ,             app    [        ID      ]
                String sql = "select * from installed_app where user_id=#{user_id}";
                List userInstalledApps = executeSql(sql);
                //            app           ( user_id              )
                shardingInsertSql(userInstalledApps);
                //      ,       
                executeRedis("setex MigrateStatus:${userId} 864000 COMPLETED");
            }else {
                //         ,                   ,       ,       
            }
        }
    }
    //  `installed_app`      --        APP
    public void addInstalledApp(InstalledApp installedApp) throws Exception{
        //        
        migratePassive(installedApp.getUserId());
        //       app  (installedApp)             
        shardingInsertSql(installedApp);
    }

    CRUDのいずれの操作も、キャッシュ内のMigrateStatus:${userId}の値に基づいて判断します.
  • 値がCOMPLETEDであり、移行が完了したことを示す場合、要求をライブラリ分割テーブル後のテーブルに移行して処理すればよい.
  • 値がMIGRATINGの場合、移行中であることを示し、COMPLETEDの値が移行完了するまでループして待機し、要求をライブラリ分割テーブルの後のテーブルに転送して処理処理することができる.
  • 以外の値が空の場合、ロックを取得してデータ移行を試みます.移行が完了した後、キャッシュ値をCOMPLETEDに更新し、最後にライブラリ分割テーブル後のテーブルに要求を転送して処理する.

  • 3.方案の完備
    すべてのデータ移行が完了すると、CRUD操作はキャッシュ内のMigrateStatusの値に基づいて判断され、データ移行が完了するまでのステップは余分です.総スイッチを追加して、すべてのデータ移行が完了した後、このスイッチの値をTOPICのような方法で送信し、すべてのサービスがTOPICを受信した後、スイッチをlocal cache化することができます.では、次のサービスのCRUDは、キャッシュ内のMigrateStatus:${userId}の値に基づいて判断する必要はありません.
    4.仕事を残す
    移行が完了したら、アクティブな移行プログラムをオフラインにし、パッシブな移行プログラムからmigratePassive()への呼び出しをすべて削除し、sharding-jdbcなどのサードパーティ製のライブラリ・スプリッタ・ミドルウェアを統合できます.sharding-jdbc統合の実戦を参照してください.
    回顧総括
    このスキームを振り返ると、sharding column(userIdなど)の合計レコード数が多く、アクティブな移行が進行中であり、アクティブな移行とアクティブな移行が衝突すると、パッシブな移行には長い時間がかかる可能性があるという最大の欠点がある.
    ただし、DB性能によると、一般的に1000個のデータを一括挿入するのは10 msレベルであり、同じsharding columnのレコードライブラリ分表の後は1枚のテーブルにのみ属し、クロステーブルには関与しない.したがって、移行前にsql統計で移行対象テーブルにこのような異常sharding columnがない限り、安心して移行できます.
    筆者はinstalledを移転しましたappテーブルの場合、ユーザーは最大200個以下のAPPしか持っていないので、衝突による性能問題をあまり考慮する必要はありません.万能な案はないが、自分に合った案がある.
    そのような万件以上の記録のsharding columnがあれば、これらのsharding columnを先にキャッシュすることができ、移行プログラムは夜間にオンラインになり、これらのキャッシュのsharding columnのデータを優先的に移行することができ、これらのユーザーに対する移行プログラムの体験をできるだけ低減することができる.もちろん、あなたが考えたより良い案を使うこともできます.