最近在工作中,遇到了這樣一個業務場景,我們需要關注一個業務系統資料庫中某幾張表的資料,當資料發生新增或修改時,將它同步到另一個業務系統資料庫中的表中。
一提到資料庫的同步,估計大家第一時間想到的就是基於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
在正式開始前,還是先簡單介紹一下mysql
的binlog
,binlog
是一個二進位制檔案,它儲存在磁碟中,是用來記錄資料庫表結構變更、表資料修改的二進位制日誌。其實除了資料複製外,它還可以實現資料恢復、增量備份等功能。
啟動專案前,首先需要確保mysql
服務已經啟用了binlog
:
show variables like 'log_bin';
如果為值為OFF
,表示沒有啟用,那麼需要首先啟用binlog
,修改配置檔案:
log_bin=mysql-bin
binlog-format=ROW
server-id=1
對引數做一個簡要說明:
- 在配置檔案中加入了
log_bin
配置項後,表示啟用了binlog
binlog-format
是binlog
的日誌格式,支援三種型別,分別是STATEMENT
、ROW
、MIXED
,我們在這裡使用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>
寫一段簡單的示例,看看它的具體使用方式:
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種型別的事件資料進行了處理,除了WriteRowsEventData
、DeleteRowsEventData
、UpdateRowsEventData
對應增刪改操作型別的事件資料外,還有一個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}
可以看到,將修改後的這一條記錄中的屬性填裝到了它對應的列中,之後我們再根據具體的業務邏輯中,就可以根據欄位名取出資料,將資料同步到其他的表了。
如果文章對您有所幫助,歡迎關注公眾號 碼農參上