Java代碼中,如何監控Mysql的binlog?

最近在工作中,遇到了這樣一個業務場景,我們需要關注一個業務系統數據庫中某幾張表的數據,當數據發生新增或修改時,將它同步到另一個業務系統數據庫中的表中。

一提到數據庫的同步,估計大家第一時間想到的就是基於binlog的主從複製了,但是放在我們的場景中,還有幾個問題:

  • 第一,並不是需要複製所有表的數據,複製對象只有少量的幾張表
  • 第二,也是比較麻煩的,兩個業務系統數據庫表結構可能不一致。例如,要同步數據庫1的A表中的某些字段到數據庫2的B表中,在這一過程中,A表和B表的字段並不是完全相同

這樣的話,我們只能通過代碼的方式,首先獲取到數據庫1表中數據的變動,再通過手動映射的方式,插入到數據庫2的表中。但是,獲取變動數據的這一過程,還是離不開binlog,因此我們就需要在代碼中對binlog進行一下監控。

先說結論,我們最終使用了一個開源工具mysql-binlog-connector-java,用來監控binlog變化並獲取數據,獲取數據後再手動插入到另一個庫的表中,基於它來實現了數據表的同步。項目的git地址如下:

https://github.com/shyiko/mysql-binlog-connector-java

https://github.com/osheroff/mysql-binlog-connector-java

在正式開始前,還是先簡單介紹一下mysqlbinlogbinlog是一個二進制文件,它保存在磁盤中,是用來記錄數據庫表結構變更、表數據修改的二進制日誌。其實除了數據複製外,它還可以實現數據恢復、增量備份等功能。

啓動項目前,首先需要確保mysql服務已經啓用了binlog

show variables like 'log_bin';

如果爲值爲OFF,表示沒有啓用,那麼需要首先啓用binlog,修改配置文件:

log_bin=mysql-bin
binlog-format=ROW
server-id=1

對參數做一個簡要說明:

  • 在配置文件中加入了log_bin配置項後,表示啓用了binlog
  • binlog-formatbinlog的日誌格式,支持三種類型,分別是STATEMENTROWMIXED,我們在這裏使用ROW模式
  • server-id用於標識一個sql語句是從哪一個server寫入的,這裏一定要進行設置,否則我們在後面的代碼中會無法正常監聽到事件

在更改完配置文件後,重啓mysql服務。再次查看是否啓用binlog,返回爲ON,表示已經開啓成功。

在Java項目中,首先引入maven座標:

<dependency>
    <groupId>com.github.shyiko</groupId>
    <artifactId>mysql-binlog-connector-java</artifactId>
    <version>0.21.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.zendesk/mysql-binlog-connector-java -->
<dependency>
    <groupId>com.zendesk</groupId>
    <artifactId>mysql-binlog-connector-java</artifactId>
    <version>0.25.5</version>
</dependency>

寫一段簡單的示例,看看它的具體使用方式:

public static void main(String[] args) {
    BinaryLogClient client = new BinaryLogClient("127.0.0.1", 3306, "hydra", "123456");
    client.setServerId(2);

    client.registerEventListener(event -> {
        EventData data = event.getData();
        if (data instanceof TableMapEventData) {
            System.out.println("Table:");
            TableMapEventData tableMapEventData = (TableMapEventData) data;
            System.out.println(tableMapEventData.getTableId()+": ["+tableMapEventData.getDatabase() + "-" + tableMapEventData.getTable()+"]");
        }
        if (data instanceof UpdateRowsEventData) {
            System.out.println("Update:");
            System.out.println(data.toString());
        } else if (data instanceof WriteRowsEventData) {
            System.out.println("Insert:");
            System.out.println(data.toString());
        } else if (data instanceof DeleteRowsEventData) {
            System.out.println("Delete:");
            System.out.println(data.toString());
        }
    });

    try {
        client.connect();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

首先,創建一個BinaryLogClient客戶端對象,初始化時需要傳入mysql的連接信息,創建完成後,給客戶端註冊一個監聽器,來實現它對binlog的監聽和解析。在監聽器中,我們暫時只對4種類型的事件數據進行了處理,除了WriteRowsEventDataDeleteRowsEventDataUpdateRowsEventData對應增刪改操作類型的事件數據外,還有一個TableMapEventData類型的數據,包含了表的對應關係,在後面的例子中再具體說明。

在這裏,客戶端監聽到的是數據庫級別的所有事件,並且可以監聽到表的DML語句和DDL語句,所以我們只需要處理我們關心的事件數據就行,否則會收到大量的冗餘數據。

啓動程序,控制檯輸出:

com.github.shyiko.mysql.binlog.BinaryLogClient openChannelToBinaryLogStream
信息: Connected to 127.0.0.1:3306 at mysql-bin.000002/1046 (sid:2, cid:10)

連接mysql的binlog成功,接下來,我們在數據庫中插入一條數據,這裏操作的數據庫名字是tenant,表是dept

insert into dept VALUES(8,"人力","","1");

這時,控制檯就會打印監聽到事件的數據:

Table:
108: [tenant-dept]
Insert:
WriteRowsEventData{tableId=108, includedColumns={0, 1, 2, 3}, rows=[
    [8, 人力, , 1]
]}

我們監聽到的事件類型數據有兩類,第一類是TableMapEventData,通過它可以獲取操作的數據庫名稱、表名稱以及表的id。之所以我們要監聽這個事件,是因爲之後監聽的實際操作中返回數據中包含了表的id,而沒有表名等信息,所以如果我們想知道具體的操作是在哪一張表的話,就要先維護一個id與表的對應關係。

第二個打印出來的監聽事件數據是WriteRowsEventData,其中記錄了insert語句作用的表,插入涉及到的列,以及實際插入的數據。另外,如果我們只需要對特定的一張或幾張表進行處理的話,也可以提前設置表的名單,在這裏根據表id到表名的映射關係,實現數據的過濾,

接下來,我們再執行一條update語句:

update dept set tenant_id=3 where id=8 or id=9

控制檯輸出:

Table:
108: [tenant-dept]
Update:
UpdateRowsEventData{tableId=108, includedColumnsBeforeUpdate={0, 1, 2, 3}, includedColumns={0, 1, 2, 3}, rows=[
    {before=[8, 人力, , 1], after=[8, 人力, , 3]},
    {before=[9, 人力, , 1], after=[9, 人力, , 3]}
]}

在執行update語句時,可能會作用於多條數據,因此在實際修改的數據中,可能包含多行記錄,這一點體現在上面的rows中,包含了id爲8和9的兩條數據。

最後,再執行一條delete語句:

delete from dept where tenant_id=3

控制檯打印如下,rows中同樣返回了生效的兩條數據:

Table:
108: [tenant-dept]
Delete:
DeleteRowsEventData{tableId=108, includedColumns={0, 1, 2, 3}, rows=[
    [8, 人力, , 3],
    [9, 人力, , 3]
]}

簡單的使用原理介紹完成後,再回到我們原先的需求上,需要將一張表中新增或修改的數據同步到另一張表中,問題還有一個,就是如何將返回的數據對應到所在的列上。這時應該怎麼實現呢?以update操作爲例,我們要對提取的數據後進行一下處理,更改上面例子中的方法:

if (data instanceof UpdateRowsEventData) {
    System.out.println("Update:");
    UpdateRowsEventData updateRowsEventData = (UpdateRowsEventData) data;
    for (Map.Entry<Serializable[], Serializable[]> row : updateRowsEventData.getRows()) {
        List<Serializable> entries = Arrays.asList(row.getValue());
        System.out.println(entries);
        JSONObject dataObject = getDataObject(entries);
        System.out.println(dataObject);
    }
}

在將data類型強制轉換爲UpdateRowsEventData後,可以使用getRows方法獲取到更新的行數據,並且能夠取到每一列的值。

之後,調用了一個自己實現的getDataObject方法,用它來實現數據到列的綁定過程:

private static JSONObject getDataObject(List message) {
    JSONObject resultObject = new JSONObject();
    String format = "{\"id\":\"0\",\"dept_name\":\"1\",\"comment\":\"2\",\"tenant_id\":\"3\"}";
    JSONObject json = JSON.parseObject(format);
    for (String key : json.keySet()) {
        resultObject.put(key, message.get(json.getInteger(key)));
    }
    return resultObject;
}

format字符串中,提前維護了一個數據庫表的字段順序的字符串,標識了每個字段位於順序中的第幾個位置。通過上面這個函數,能夠實現數據到列的填裝過程,我們再執行一條update語句來查看一下結果:

update dept set tenant_id=3,comment="1" where id=8

控制檯打印結果如下:

Table:
108: [tenant-dept]
Update:
[8, 人力, 1, 3]
{"tenant_id":3,"dept_name":"人力","comment":"1","id":8}

可以看到,將修改後的這一條記錄中的屬性填裝到了它對應的列中,之後我們再根據具體的業務邏輯中,就可以根據字段名取出數據,將數據同步到其他的表了。

 

轉自:https://www.cnblogs.com/trunks2008/p/15098453.html

 

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
 
import com.github.shyiko.mysql.binlog.BinaryLogFileReader;
import com.github.shyiko.mysql.binlog.event.Event;
import com.github.shyiko.mysql.binlog.event.deserialization.ChecksumType;
import com.github.shyiko.mysql.binlog.event.deserialization.EventDeserializer;
 
class Sample {
	public static void main(String[] args) throws IOException {
		String filePath = "D:\\mysql-bin.000345";
		File binlogFile = new File(filePath);
		EventDeserializer eventDeserializer = new EventDeserializer();
		eventDeserializer.setChecksumType(ChecksumType.CRC32);
		BinaryLogFileReader reader = new BinaryLogFileReader(binlogFile,
				eventDeserializer);
		try {
			// 準備寫入的文件名稱
			/*
			 * File f1 = new File("D:\\mysql-bin.000845.sql"); if
			 * (f1.exists()==false){ f1.getParentFile().mkdirs(); }
			 */
			FileOutputStream fos = new FileOutputStream(
					"D:\\mysql-bin.000345.sql", true);
			for (Event event; (event = reader.readEvent()) != null;) {
				System.out.println(event.toString());
 
				// 把數據寫入到輸出流
				fos.write(event.toString().getBytes());
			}
			// 關閉輸出流
			fos.close();
			System.out.println("輸入完成");
		} finally {
			reader.close();
		}
 
	}
}
package demo;
 
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
 
import com.github.shyiko.mysql.binlog.BinaryLogFileReader;
import com.github.shyiko.mysql.binlog.event.Event;
import com.github.shyiko.mysql.binlog.event.deserialization.ChecksumType;
import com.github.shyiko.mysql.binlog.event.deserialization.EventDeserializer;
 
public class Readbinlog {
 
	public static void main(String[] args) throws IOException {
		String filePath = "D:\\mysql-bin.000007";
		File binlogFile = new File(filePath);
		EventDeserializer eventDeserializer = new EventDeserializer();
		eventDeserializer.setChecksumType(ChecksumType.CRC32);
		BinaryLogFileReader reader = new BinaryLogFileReader(binlogFile,
				eventDeserializer);
		try {
			// 準備寫入的文件名稱
			FileOutputStream fos = new FileOutputStream(
					"D:\\mysql-bin.000007.sql", true);
			for (Event event; (event = reader.readEvent()) != null;) {
				System.out.println(event.toString());
 
				// 把數據寫入到輸出流
				fos.write(event.toString().getBytes());
			}
			// 關閉輸出流
			fos.close();
			System.out.println("輸入完成");
		} finally {
			reader.close();
		}
 
	}
 
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章