binlog を監視し、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ログは自動的に削除されます。
知らせ:
- デフォルトではファイルの変更は許可されていないため、右クリックして「管理者が所有権を取得」して変更を保存する必要があります。
- 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 マスターおよびバックアップ レプリケーションの原則
- MySQL マスターは、データの変更をバイナリ ログに書き込みます (バイナリ ログ。レコードはバイナリ ログ イベントと呼ばれ、show binlog events で表示できます)。
- MySQL スレーブは、マスターのバイナリ ログ イベントをリレー ログ (リレー ログ) にコピーします。
- MySQL スレーブはリレー ログ内のイベントを再生し、データの変更を自身のデータに反映します。
運河の原理
- canal は MySQL スレーブのインタラクティブ プロトコルをシミュレートし、MySQL スレーブのふりをして、MySQL マスターにダンプ プロトコルを送信します。
- MySQL マスターはダンプ リクエストを受信し、バイナリ ログをスレーブ (つまり運河) にプッシュし始めます。
- canal はバイナリ ログ オブジェクト (元はバイト ストリーム) を解析します。
運河の設置構成
mysql環境の準備
- mysql バージョン
現在の運河は、5.1.x、5.5.x、5.6.x、5.7.x、8.0.x を含むソース MySQL バージョンをサポートしています。 - 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 を開始します。
開発する
準備
- プロジェクトが開始されたら、運河リンクを開いていくつかの構成を初期化します。
@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