データ移行のためのローカル運河ミドルウェアの構築-キャッシュの内訳からのインスピレーション

一緒に書く習慣を身につけましょう!「ナゲッツデイリーニュープラン・4月アップデートチャレンジ」に参加して21日目です。クリックしてイベントの詳細をご覧ください


キャッシュの内訳から始めましょう

いわゆるキャッシュブレイクダウンとは、ホットデータのキャッシュにデータがなく、多数のユーザーリクエストがデータベースに直接アクセスしていることを意味します。これは非常に危険な状況であり、開発プロセスではこのプロセスを回避する必要があります。

現在私たちがよく使用しているソリューションは3つあります。

  • キャッシュ有効期限戦略スキーム:読み取り/書き込み分離アーキテクチャを使用し、読み取りは常にキャッシュから読み取られ、canalはDBとキャッシュ間の同期に使用されます

  • ホットスポットキャッシュ戦略:ホットスポットデータを特定した後、ホットスポットデータのリクエストを特別なエリアに送信します

  • 相互排除ロック:データの有効期限が切れる前に、キーにミューテックスロックを追加します。他のリクエストが来た場合、現在のキーがロックされていることが判明した場合は、データを取得するためのバックグラウンドへのリクエストがすでに存在することを意味します。有効期限が切れる前の値。これで、他のリクエストがキャッシュからデータを大胆に読み取ることができます。データベースからデータを取得するリクエストは、キャッシュを非同期で更新でき、更新の完了後にミューテックスロックを削除できます。

Guavaのソリューション:

  • Guava Cacheは、ロード中に同時実行制御を実行します。複数のスレッドが存在しない、または期限切れのキャッシュアイテムを要求すると、1つのスレッドのみがロードメソッドに入り、他のスレッドはキャッシュアイテムが生成されるまで待機するため、多数のスレッドを回避できます。ヒット。キャッシュを直接DBに渡します。
  • refreshAfterWriteを介して実装され、更新ポリシーを構成した後、対応するキャッシュアイテムは

スレッドのブロックを回避し、キャッシュアイテムが最新の状態にあることを確認するために、一定の時間に更新します。キャッシュアイテムが生成されていることが前提です。実際の本番環境では、トラフィックのピークによるスレッドの蓄積を回避するために、キャッシュを予熱してキャッシュアイテムを事前に生成できます。

選択するかどうかは、義務によって異なります。読み取り/書き込み分離アーキテクチャを使用する場合は、Canalミドルウェアを使用してデータを同期する必要があります。この記事では、ローカルデモを作成して、その再生方法を確認します。


MySQLデータ同期

データ同期の主なプロセスを次の図に示します。

ここに画像の説明を挿入

マスターはデータを更新するたびに、そのデータをローカルbinログに書き込みます。スレーブは、IOスレッドを介してマスターのbinログをローカルリレーログに同期し、SQLスレッドを介してデータベースへのリレーログを実行します。


運河のしくみ

Canalは、データをマスターと同期するためにDumpプロトコルを偽造することにより、スレーブになりすます。

ここに画像の説明を挿入


データ移行用のローカルCanalミドルウェアを構築する

MySQLのダウンロード-コミュニティバージョン dev.mysgl.com/downloads/m…

Canalgithub.com/alibaba/canをダウンロード…

请添加图片描述

変更する必要のあるウィンドウの下に小さなバグがあります(コンピューターのMacバージョンは無視できます)。binの下のstartup.batを編集します。

请添加图片描述

次に、ダブルクリックして開始startup.batします。ログフォルダの下のログにエラーメッセージがない場合、起動は成功しています。

MySQLのビンログを開き、MySQL構成ファイルmy.cnfを見つけて、次のコードスニペットを構成ファイルに入れます

[mysqld]
log-bin = mysql-bin			# 开启bing log
binlog-format = ROW			# 选择 ROW 模式
server_id = 1				# 配置MySQL replaction 需要定义,不要和canal的slaveId重复
复制代码

テストライブラリの作成テストライブラリテストでcanal_testテーブルを作成し、IDフィールドを1つだけ設定しますユーザーを请添加图片描述 作成ます

CREATE USER canal IDENTIFIED BY 'canal'; 
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%'; 
FLUSH PRIVILEGES;
复制代码

Canalはこのユーザーを使用してターゲットデータベースに接続し、binログへの変更を消費します

pom座標

<dependency>
    <groupId>com.alibaba.otter</groupId>
    <artifactId>canal.client</artifactId>
    <version>1.1.0</version>
</dependency>
复制代码

Javaコード

public class CanalStarter {
    public static void main(String[] args) throws InterruptedException, InvalidProtocolBufferException {
        CanalConnector connector = CanalConnectors.newSingleConnector(
                new InetSocketAddress(AddressUtils.getHostIp(), 11111),
                "example",  // destination
                "",          // username
                ""           // password
        );
        try{
            connector.connect();
            connector.subscribe(".*\\..*"); // .*.\.* 表示所有数据库的变更
            connector.rollback();   // 回到上次读取的位置,即回到上一个数据库的bin log消费到的这条记录的位置上

            while (true){
                Message msg = connector.getWithoutAck(100);     // 拿100条数据

                if (msg == null || msg.getId() < 0 || msg.getEntries().size() == 0){
                    System.out.println("nothing consumed");
                    Thread.sleep(1000);
                    continue;
                }
                // 对变更的数据做处理
                printEntry(msg.getEntries());

                connector.ack(msg.getId()); // 确认这条消息已经被消费掉
            }
        }finally {
            connector.disconnect();
        }
    }
}
复制代码

データ変更前後の内容を印刷する方法:

    private static void printEntry(List<CanalEntry.Entry> entries) throws InvalidProtocolBufferException {
        for (CanalEntry.Entry entry : entries) {
            // 过滤掉一些类型
            if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN ||
                    entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND
            ){
                continue;
            }

            CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());

            for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
                System.out.println("event type " + rowChange.getEventType());

                System.out.println("********** before change");
                printRowData(rowData.getBeforeColumnsList());

                System.out.println("********** after change");
                printRowData(rowData.getAfterColumnsList());
            }

        }
    }
复制代码

CanalEntry.EntryType.TRANSACTIONBEGINこれにより、データ自体CanalEntry.EntryType.TRANSACTIONENDの変更に関連しない変更が除外されます

データの行を印刷する方法:

    private static void printRowData(List<CanalEntry.Column> columns){
        if (CollectionUtils.isEmpty(columns)){
            return;
        }
        columns.forEach(e -> {
            System.out.println(e.getName() + " : " + e.getValue());
        });
    }
复制代码

ソースコードを開始した後、元のテーブルのデータを変更します。

INSERT INTO canal_test (id) VALUES (2022);
复制代码

请添加图片描述新しく挿入されたデータであるため、新しく挿入されたデータを印刷する前と後はありません

UPDATE canal_test SET id = 2023 WHERE id = 2022;
复制代码

请添加图片描述

おすすめ

転載: juejin.im/post/7088881644333400078