binlog を設定し、Canal を使用して Mysql カスタム同期データ関数を実現する

ビンログの概要

Binlog はディスク上に保存されるバイナリ ファイルで、データベース テーブル構造の変更とテーブル データの変更を記録するために使用されるバイナリ ログです。実際、データレプリケーションに加えて、データリカバリや増分バックアップなどの機能も実装できます。

まず确保mysql服务已经启用ビンログが必要です

show variables like 'log_bin';

値が OFF の場合は、有効になっていないことを意味するため、まず binlog を有効にして、mysql の構成ファイルを変更する必要があります。

log_bin= /var/log/mysql/mysql-bin.log #指定binlog路径
binlog-format=ROW
server-id=1
expire_logs_days    = 10
max_binlog_size     = 100M

パラメータの簡単な説明:

  • 構成ファイルに log_bin 構成項目を追加すると、binlog が有効になります。
  • binlog-format は binlog のログ形式で、STATEMENT、ROW、MIXED の 3 つのタイプをサポートします。ここでは ROW モードを使用します。
  • server-id は、SQL ステートメントが書き込まれたサーバーを識別するために使用されます。ここで設定する必要があります。そうしないと、次のコードでイベントを正常にリッスンできなくなります。
  • 主にbinlogログファイルの保存期間を制御するために使用され、保存期間を超えたbinlogログは自動的に削除されます。

知らせ:

  1. デフォルトではファイルの変更は許可されていないため、右クリックして「管理者が所有権を取得」して変更を保存する必要があります。
  2. binlog ファイルのパスが絶対パスとして指定されている場合、それは指定されたパスです:
    log_bin=C:\mysql-binlog\mysql-bin
    絶対パスを指定しない場合、デフォルトで現在のディレクトリの Data フォルダーになります。 log_bin=mysql-bin

設定ファイルを変更した後、mysql サービスを再起動します。binlog が有効になっているかどうかを再度確認し、ON を返した場合は、正常に有効になったことを意味します。

このとき、特定のデータベース内のテーブルを任意に追加、削除、変更すると、対応するログがフォルダー /var/log/mysql/ に記録されます。このフォルダーの内容を見てみましょう。

ここに画像の説明を挿入
通常ここでファイルを表示する方法はありません。ファイルを表示するには、mysql が提供するコマンドを使用する必要があります。コマンドは次のようになります:

1. 查看
mysqlbinlog mysql-bin.000002
2. 指定位置查看
mysqlbinlog --start-position="120" --stop-position="332" mysql-bin.000002

ビンログ関連のコマンド

-- 查询binglog日志列表
show binary logs;

-- 查询第一个(最早)的binlog日志
show binlog events; 
 
-- 指定查询 mysql-bin.000077 日志
show binlog events in 'mysql-bin.000077';
 
-- 指定查询 mysql-bin.000077 日志,并且从pos=1024开始查
show binlog events in 'mysql-bin.000077' from 1024;
 
-- 指定查询 mysql-bin.000077 日志,并且从pos=1024开始查起,查询10条
show binlog events in 'mysql-bin.000077' from 1024 limit 10;
 
-- 指定查询 mysql-bin.000077 日志,并且从pos=1024开始查起,偏移2行,查询10条
show binlog events in 'mysql-bin.000077' from 1024 limit 2,10;

現在の binlog_format で指定されている形式は ROW (ROW に書き込まれていたことを覚えていますか?) であるため、いわゆる binlog ファイルの内容は次のようになり、通常は表示できません。

ここに画像の説明を挿入
この時点で、出力をデコードする必要があります

mysqlbinlog --base64-output=decode-rows -v mysql-bin.000001

このとき、表示結果は次のようになります。

ここに画像の説明を挿入
まだ通常の SQL ではありませんが、一定の形式はあります。

しかし、それでも自分で分析するのは非常に面倒なので、この操作はあきらめてください。
その過程で、アリババに利用できるオープンソースソフトウェアがあることを知りました。タイトル通り「運河」です。ウェブサイトの紹介文を読んだ後は、ただただ感激しました。

運河の概要

運河の紹介

canal [kə'næl]、水路/パイプ/溝と訳されるこの主な目的は、MySQL データベースの増分ログ解析に基づいて増分データのサブスクリプションと消費を提供することです。
初期の頃、杭州と米国にデュアル コンピューター ルームが導入されたため、コンピューター ルーム間での同期に対するビジネス要件があり、その実装方法は主にビジネス トリガーに基づいて段階的な変更を取得していました。2010 年以来、このビジネスは同期のための増分変更を取得するためにデータベース ログを解析することを徐々に試みており、そこから多数の増分データベース サブスクリプションおよび消費ビジネスが派生しました。

canal公式サイトアドレス: https: //github.com/alibaba/canal

ログの増分サブスクリプションと消費に基づくサービスには次のものがあります。

データベースのミラーリング
リアルタイムのデータベース バックアップ
インデックスの構築とリアルタイムのメンテナンス (分割異種インデックス、逆インデックスなど)
ビジネス キャッシュの更新
ビジネス ロジックによる増分データ処理

canal がしなければならないことは、 binlog に基づいて完全なデータ同期ではなく増分データ同期を実現することです

テクノロジーの選択

binlog に基づくデータ同期には 2 つのスキームがあります。

mysql-binlog-コネクタ アリの運河
依存するjarパッケージの導入により、解析を自分で実装する必要がありますが、比較的軽量です。 別途導入・保守が必要なデータ同期ミドルウェアであり、データベースとMQの同期をサポートする強力な機能を備えていますが、保守コストが高くなります。

実際のビジネス シナリオによれば、オンデマンド リクエスト、小規模なビジネス量、シンプルなビジネス、軽量は mysql-binlog-connector を通過可能、大規模なビジネス量、複雑なロジック、専任の運用保守チーム、カナルを考慮できます。 Ali の高い同時実行性検証は比較的安定しています。

Canal は mysql の binlog ログを監視してデータ同期を実現します: https://blog.csdn.net/m0_37583655/article/details/119517336
Java が mysql の binlog を監視する詳細な説明 (mysql-binlog-connector): https://blog.csdn.net /m0_37583655/記事/詳細/119148470

原理分析

MySQL マスターおよびバックアップ レプリケーションの原則

ここに画像の説明を挿入

  1. MySQL マスターは、データの変更をバイナリ ログに書き込みます (バイナリ ログ。レコードはバイナリ ログ イベントと呼ばれ、show binlog events で表示できます)。
  2. MySQL スレーブは、マスターのバイナリ ログ イベントをリレー ログ (リレー ログ) にコピーします。
  3. MySQL スレーブはリレー ログ内のイベントを再生し、データの変更を自身のデータに反映します。

運河の原理

  1. canal は MySQL スレーブのインタラクティブ プロトコルをシミュレートし、MySQL スレーブのふりをして、MySQL マスターにダンプ プロトコルを送信します。
  2. MySQL マスターはダンプ リクエストを受信し、バイナリ ログをスレーブ (つまり運河) にプッシュし始めます。
  3. canal はバイナリ ログ オブジェクト (元はバイト ストリーム) を解析します。
    ここに画像の説明を挿入

運河の設置構成

mysql環境の準備

  1. mysql バージョン
    現在の運河は、5.1.x、5.5.x、5.6.x、5.7.x、8.0.x を含むソース MySQL バージョンをサポートしています。
  2. mysqlオープンバイナリログ

運河構成の開始

構成ファイル conf/example/instance.properties を開き、データベース接続およびその他の情報を次のように構成します。

#################################################
## mysql serverId , v1.0.26+ will autoGen
# canal.instance.mysql.slaveId=0

# enable gtid use true/false
canal.instance.gtidon=false

# position info
canal.instance.master.address=127.0.0.1:3306
canal.instance.master.journal.name=
canal.instance.master.position=
canal.instance.master.timestamp=
canal.instance.master.gtid=

# rds oss binlog
canal.instance.rds.accesskey=
canal.instance.rds.secretkey=
canal.instance.rds.instanceId=

# table meta tsdb info
canal.instance.tsdb.enable=true
#canal.instance.tsdb.url=jdbc:mysql://127.0.0.1:3306/canal_tsdb
#canal.instance.tsdb.dbUsername=canal
#canal.instance.tsdb.dbPassword=canal

#canal.instance.standby.address =
#canal.instance.standby.journal.name =
#canal.instance.standby.position =
#canal.instance.standby.timestamp =
#canal.instance.standby.gtid=

# username/password
# 在MySQL服务器授权的账号密码字符集
canal.instance.dbUsername=root
canal.instance.dbPassword=123456
canal.instance.connectionCharset = UTF-8
# enable druid Decrypt database password
canal.instance.enableDruid=false
#canal.instance.pwdPublicKey=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALK4BUxdDltRRE5/zXpVEVPUgunvscYFtEip3pmLlhrWpacX7y7GCMo2/JM6LeHmiiNdH1FWgGCpUfircSwlWKUCAwEAAQ==

# table regex.*\\..*表示监听所有表 也可以写具体的表名,用,隔开
canal.instance.filter.regex=.*\\..*
# table black regex
# mysql 数据解析表的黑名单,多个表用,隔开
canal.instance.filter.black.regex=mysql\\.slave_.*
# table field filter(format: schema1.tableName1:field1/field2,schema2.tableName2:field1/field2)
#canal.instance.filter.field=test1.t_product:id/subject/keywords,test2.t_company:id/name/contact/ch
# table field black filter(format: schema1.tableName1:field1/field2,schema2.tableName2:field1/field2)
#canal.instance.filter.black.field=test1.t_product:subject/product_image,test2.t_company:id/name/contact/ch

# mq config
canal.mq.topic=example
# dynamic topic route by schema or table regex
#canal.mq.dynamicTopic=mytest1.user,mytest2\\..*,.*\\..*
canal.mq.partition=0
# hash partition config
#canal.mq.partitionsNum=3
#canal.mq.partitionHash=test.table:id^name,.*\\..*
#canal.mq.dynamicTopicPartitionNum=test.*:4,mycanal:6
#################################################

canal.deployer-1.1.5\bin パス:startup.bat を開始します。

開発する

準備

  1. プロジェクトが開始されたら、運河リンクを開いていくつかの構成を初期化します。
@Bean
public CanalConnector canalConnector() {
    
    
    CanalConnector connector = CanalConnectors.newSingleConnector(
            //对应canal服务的链接
            new InetSocketAddress(canalConf.getIp(), canalConf.getPort()),
            //链接的目标,这里对应canal服务中的配置,需要查阅文档
            canalConf.getDestination(),
            //不知道是什么用户,使用“”
            canalConf.getUser(),
            //不知道是什么密码,使用“”
            canalConf.getPassword()
    );
    return connector;
}

2) まずスレッドを開始し、運河サービスからバイナリログ内のメッセージを取得するための無限ループをその中に記述します。メッセージ クラスは、com.alibaba.otter.canal.protocol.Message です。

Message message = connector.getWithoutAck(100);
  • コネクタ: 運河リンクのインスタンス化されたオブジェクト。
  • Connector.getWithoutAck(100): 接続から 100 個のバイナリログ データを取得します。

3) Message に設定されているイベント、つまり binlog 内の各データを取り出します。追加、削除、変更の種類のデータを取り出し、それぞれのデータをスレッドに入れてスレッドプールを利用して実行します。

List<Entry> entries = message.getEntries();
message.getEntries():从链接中获取的数据集合,每一条代表1条binlog数据

4) 各スレッドで、Entry 内のデータを取り出し、その種類に応じて各種 SQL をつなぎ合わせて実行します。

Header header = entry.getHeader();
//获取发生变化的表名称,可能会没有
String tableName = header.getTableName();

//获取发生变化的数据库名称,可能会没有
String schemaName = header.getSchemaName();

//获取事件类型
EventType eventType = rowChange.getEventType();
/**
这里我们只是用其中的三种类型:
    EventType.DELETE 删除
    EventType.INSERT 插入
    EventType.UPDATE 更新
*/
//获取发生变化的数据
RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());

//遍历其中的数据
int rowDatasCount = rowChange.getRowDatasCount();
for (int i = 0; i < rowDatasCount; i++) {
    
    
    //每一行中的数据
    RowData rowData = rowChange.getRowDatas(i);
}

//获取修改前的数据
List<Column> before = rowData.getBeforeColumnsList();

//获取修改后的数据
List<Column> after = rowData.getAfterColumnsList();

Column には、変更があるかどうか、キーがいつであるか、null であるかどうかなどの一連のメソッドがあるため、詳細は説明しません。拡張子: 阿里Canal框架(数据同步中间件)初步实践
https://mp.weixin.qq.com/s?__biz=MzU2MTI4MjI0MQ==&mid=2247486253&idx=1&sn=28112ccd3f5b20e93a3a98836f70948b&scene=45#wechat_redirect

開発する

1) まず、運河サービスからメッセージを継続的に取得するために使用されるスレッドをここに記述し、次に新しいスレッドを作成してその中でデータを処理させます。コードは以下のように表示されます。

@Override
public void run() {
    
    
    while (true) {
    
    
        //主要用于在链接失败后用于再次尝试重新链接
        try {
    
    
            if (!run) {
    
    
                //打开链接,并设置 run=true
                startCanal();
            }
        } catch (Exception e) {
    
    
            System.err.println("连接失败,尝试重新链接。。。");
            threadSleep(3 * 1000);
        }
        System.err.println("链接成功。。。");
        //不停的从CanalConnector中获取消息
        try {
    
    
            while (run) {
    
    
                //获取一定数量的消息,这里为线程池数量×3
                Message message = connector.getWithoutAck(batchSize * 3);
                long id = message.getId();

                //处理获取到的消息
                process(message);
                connector.ack(id);
            }
        } catch (Exception e) {
    
    
            System.err.println(e.getMessage());
        } finally {
    
    
            //如果发生异常,最终关闭连接,并设置run=false
            stopCanal();
        }
    }

}
void process(Message message) {
    
    
    List<Entry> entries = message.getEntries();
    if (entries.size() <= 0) {
    
    
        return;
    }
    log.info("process message.entries.size:{}", entries.size());
    for (Entry entry : entries) {
    
    
        Header header = entry.getHeader();
        String tableName = header.getTableName();
        String schemaName = header.getSchemaName();

        //这里判断是否可以取出数据库名称和表名称,如果不行,跳过循环
        if (StringUtils.isAllBlank(tableName, schemaName)) {
    
    
            continue;
        }

        //创建新的线程,并执行
        jobList.stream()
                .filter(job -> job.isMatches(tableName, schemaName))
                .forEach(job -> executorService.execute(job.newTask(entry)));
    }
}

ここでの jobList は私自身の List 定義であり、コードは次のとおりです。

package com.hebaibai.miner.job;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.google.protobuf.InvalidProtocolBufferException;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;

import static com.alibaba.otter.canal.protocol.CanalEntry.Entry;

@Slf4j
@Data
public abstract class Job {
    
    


    /**
     * 数据库链接
     */
    protected JdbcTemplate jdbcTemplate;

    /**
     * 额外配置
     */
    protected JSONObject prop;

    /**
     * 校验目标是否为合适的数据库和表
     *
     * @param table
     * @param database
     * @return
     */
    abstract public boolean isMatches(String table, String database);

    /**
     * 实例化一个Runnable
     *
     * @param entry
     * @return
     */
    abstract public Runnable newTask(final Entry entry);


    /**
     * 获取RowChange
     *
     * @param entry
     * @return
     */
    protected CanalEntry.RowChange getRowChange(Entry entry) {
    
    
        try {
    
    
            return CanalEntry.RowChange.parseFrom(entry.getStoreValue());
        } catch (InvalidProtocolBufferException e) {
    
    
            e.printStackTrace();
        }
        return null;
    }

}

jobList には、ジョブの実装クラスが含まれます。

Job 実装クラスを作成し、それを使用してテーブルを同期し、フィールド名を変換します。要件では、2 つの同期されたデータのフィールド名が一致しない可能性があることが必要なため、2 つのテーブル間のフィールドの対応関係を構成する josn を作成しました。

//省略其他配置
"prop": {
    
    
//来源数据库
  "database": "pay",
//来源表
  "table": "p_pay_msg",
//目标表(目标库在其他地方配置)
  "target": "member",
//字段对应关系
//key  :来源表的字段名
//value:目标表的字段名
  "mapping": {
    
    
    "id": "id",
    "mch_code": "mCode",
    "send_type": "mName",
    "order_id": "phone",
    "created_time": "create_time",
    "creator": "remark"
  }
}
//省略其他配置

以下はすべてのコードです。主に行うべきことは、変更されたデータを取り出し、対応するフィールド名に従って SQL を再アセンブルしてから実行することです。拡張子: 基于canal进行日志的订阅和转换
https://mp.weixin.qq.com/s?__biz=MzI4Njc5NjM1NQ==&mid=2247489997&idx=2&sn=c28567f0248601443c066ed63f4fcedb&chksm=ebd626e1dca1aff7ae57cfa1c5ebe7f1 2 1c5e85e463eae713621c12b6773d851ed94e4ec6bf2 &scene=21#wechat_redirect

import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import static com.alibaba.otter.canal.protocol.CanalEntry.*;

/**
 * 单表同步,表的字段名称可以不同,类型需要一致
 * 表中需要有id字段
 */
@SuppressWarnings("ALL")
@Slf4j
public class TableSyncJob extends Job {
    
    


    /**
     * 用于校验是否适用于当前的配置
     *
     * @param table
     * @param database
     * @return
     */
    @Override
    public boolean isMatches(String table, String database) {
    
    
        return prop.getString("database").equals(database) &&
                prop.getString("table").equals(table);
    }

    /**
     * 返回一个新的Runnable
     *
     * @param entry
     * @return
     */
    @Override
    public Runnable newTask(final Entry entry) {
    
    
        return () -> {
    
    
            RowChange rowChange = super.getRowChange(entry);
            if (rowChange == null) {
    
    
                return;
            }
            EventType eventType = rowChange.getEventType();
            int rowDatasCount = rowChange.getRowDatasCount();
            for (int i = 0; i < rowDatasCount; i++) {
    
    
                RowData rowData = rowChange.getRowDatas(i);
                if (eventType == EventType.DELETE) {
    
    
                    delete(rowData.getBeforeColumnsList());
                }
                if (eventType == EventType.INSERT) {
    
    
                    insert(rowData.getAfterColumnsList());
                }
                if (eventType == EventType.UPDATE) {
    
    
                    update(rowData.getBeforeColumnsList(), rowData.getAfterColumnsList());
                }
            }
        };
    }

    /**
     * 修改后的数据
     *
     * @param after
     */
    private void insert(List<Column> after) {
    
    
        //找到改动的数据
        List<Column> collect = after.stream().filter(column -> column.getUpdated() || column.getIsKey()).collect(Collectors.toList());
        //根据表映射关系拼装更新sql
        JSONObject mapping = prop.getJSONObject("mapping");
        String target = prop.getString("target");
        List<String> columnNames = new ArrayList<>();
        List<String> columnValues = new ArrayList<>();
        for (int i = 0; i < collect.size(); i++) {
    
    
            Column column = collect.get(i);
            if (!mapping.containsKey(column.getName())) {
    
    
                continue;
            }
            String name = mapping.getString(column.getName());
            columnNames.add(name);
            if (column.getIsNull()) {
    
    
                columnValues.add("null");
            } else {
    
    
                columnValues.add("'" + column.getValue() + "'");
            }
        }
        StringBuilder sql = new StringBuilder();
        sql.append("REPLACE INTO ").append(target).append("( ")
                .append(StringUtils.join(columnNames, ", "))
                .append(") VALUES ( ")
                .append(StringUtils.join(columnValues, ", "))
                .append(");");
        String sqlStr = sql.toString();
        log.debug(sqlStr);
        jdbcTemplate.execute(sqlStr);
    }

    /**
     * 更新数据
     *
     * @param before 原始数据
     * @param after  更新后的数据
     */
    private void update(List<Column> before, List<Column> after) {
    
    
        //找到改动的数据
        List<Column> updataCols = after.stream().filter(column -> column.getUpdated()).collect(Collectors.toList());
        //找到之前的数据中的keys
        List<Column> keyCols = before.stream().filter(column -> column.getIsKey()).collect(Collectors.toList());
        //没有key,执行更新替换
        if (keyCols.size() == 0) {
    
    
            return;
        }
        //根据表映射关系拼装更新sql
        JSONObject mapping = prop.getJSONObject("mapping");
        String target = prop.getString("target");
        //待更新数据
        List<String> updatas = new ArrayList<>();
        for (int i = 0; i < updataCols.size(); i++) {
    
    
            Column updataCol = updataCols.get(i);
            if (!mapping.containsKey(updataCol.getName())) {
    
    
                continue;
            }
            String name = mapping.getString(updataCol.getName());
            if (updataCol.getIsNull()) {
    
    
                updatas.add("`" + name + "` = null");
            } else {
    
    
                updatas.add("`" + name + "` = '" + updataCol.getValue() + "'");
            }
        }
        //如果没有要修改的数据,返回
        if (updatas.size() == 0) {
    
    
            return;
        }
        //keys
        List<String> keys = new ArrayList<>();
        for (Column keyCol : keyCols) {
    
    
            String name = mapping.getString(keyCol.getName());
            keys.add("`" + name + "` = '" + keyCol.getValue() + "'");
        }
        StringBuilder sql = new StringBuilder();
        sql.append("UPDATE ").append(target).append(" SET ");
        sql.append(StringUtils.join(updatas, ", "));
        sql.append(" WHERE ");
        sql.append(StringUtils.join(keys, "AND "));
        String sqlStr = sql.toString();
        log.debug(sqlStr);
        jdbcTemplate.execute(sqlStr);
    }

    /**
     * 删除数据
     *
     * @param before
     */
    private void delete(List<Column> before) {
    
    
        //找到改动的数据
        List<Column> keyCols = before.stream().filter(column -> column.getIsKey()).collect(Collectors.toList());
        if (keyCols.size() == 0) {
    
    
            return;
        }
        //根据表映射关系拼装更新sql
        JSONObject mapping = prop.getJSONObject("mapping");
        String target = prop.getString("target");
        StringBuilder sql = new StringBuilder();
        sql.append("DELETE FROM `").append(target).append("` WHERE ");
        List<String> where = new ArrayList<>();
        for (Column column : keyCols) {
    
    
            String name = mapping.getString(column.getName());
            where.add(name + " = '" + column.getValue() + "' ");
        }
        sql.append(StringUtils.join(where, "and "));
        String sqlStr = sql.toString();
        log.debug(sqlStr);
        jdbcTemplate.execute(sqlStr);
    }
}

引用

mysql-binlog-connector-java
https://github.com/shyiko/mysql-binlog-connector-java
mysql 原則 ~ binlog シリーズの table_id の詳細https://www.cnblogs.com/danhuangpai/p/11484256.html

Canal は mysql の binlog ログを監視してデータ同期を実現しますhttps://blog.csdn.net/m0_37583655/article/details/119517336

canal を使用して binlog を監視し、mysql カスタム同期データ関数を実現するhttps://blog.51cto.com/u_12302929/3294157

おすすめ

転載: blog.csdn.net/qq_43961619/article/details/127674569