一、環境準備
準備兩個MySQL 5.7的數據庫,一個作爲源數據庫(A庫),一個作爲目標數據庫(B庫)。
源數據庫需要開啓binlog,設置binlog_format爲ROW模式。登錄MySQL,編輯my.cnf文件(位於/etc/my.cnf或/etc/mysql/my.cnf):
[mysqld]
log-bin=mysql-bin # 開啓binlog
binlog-format=ROW # 設置binlog_format爲ROW模式
server-id=1 # 設置服務器ID
保存後,重啓MySQL服務。
二、安裝Canal
從Canal的Github頁面下載源代碼:https://github.com/alibaba/canal ,也可以用Git克隆到本地:
git clone https://github.com/alibaba/canal.git
進入到下載的源代碼目錄,執行mvn clean install -Dmaven.test.skip進行編譯。
編譯完成後,會在canal.deployer/target目錄下生成canal.deployer-版本號.zip文件,解壓到你想要安裝的目錄,如/usr/local/canal/。
三、配置Canal
編輯Canal的配置文件/usr/local/canal/conf/example/instance.properties,將以下內容替換爲你的實際配置
canal.instance.master.address = 數據源地址:端口
canal.instance.dbUsername = 數據源用戶名
canal.instance.dbPassword = 數據源密碼
canal.instance.defaultDatabaseName = 數據庫名
canal.instance.filter.regex = 數據庫名.表名
配置Canal連接MySQL的賬號權限,確保具備以下權限:SELECT,RELOAD,SHOW DATABASES,REPLICATION SLAVE,REPLICATION CLIENT等。
四、啓動Canal服務
進入到Canal的bin目錄:cd /usr/local/canal/bin/
運行startup.sh腳本啓動Canal服務:sh startup.sh
使用ps -ef | grep canal命令,確認Canal服務已經啓動。
五、編寫數據同步程序
以Java爲例,使用Canal的Java客戶端庫,編寫數據同步程序。以下是一個簡單的示例:
```java
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.alibaba.otter.canal.protocol.Message;
import java.net.InetSocketAddress;
import java.util.List;
public class CanalClient {
public static void main(String[] args) {
// 創建Canal連接
CanalConnector connector = CanalConnectors.newSingleConnector(
new InetSocketAddress("Canal服務地址", 11111), "example", "", "");
// 連接Canal服務
connector.connect();
// 訂閱數據表
connector.subscribe("數據庫名.表名");
// 循環讀取數據變更消息
while (true) {
// 獲取1000條數據變更消息
Message message = connector.getWithoutAck(1000);
long batchId = message.getId();
if (batchId == -1 || message.getEntries().isEmpty()) {
continue;
}
printEntry(message.getEntries());
// 確認消息
connector.ack(batchId);
}
}
private static void printEntry(List<CanalEntry.Entry> entries) {
for (CanalEntry.Entry entry : entries) {
if (entry.getEntryType() == CanalEntry.EntryType.ROWDATA) {
// 解析數據變更消息
CanalEntry.RowChange rowChange;
try {
rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
} catch (Exception e) {
throw new RuntimeException("ERROR ## parser of eromanga-event has an error, data:" + entry.toString(), e);
}
CanalEntry.EventType eventType = rowChange.getEventType();
String tableName = entry.getHeader().getTableName();
System.out.println(String.format("================> binlog[%s:%s] , name[%s,%s] , eventType : %s",
entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),
entry.getHeader().getSchemaName(), tableName, eventType));
// 打印行變更信息
for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
if (eventType == CanalEntry.EventType.DELETE) {
printColumn(rowData.getBeforeColumnsList());
} else if (eventType == CanalEntry.EventType.INSERT) {
printColumn(rowData.getAfterColumnsList());
} else {
System.out.println("-------> before");
printColumn(rowData.getBeforeColumnsList());
System.out.println("-------> after");
printColumn(rowData.getAfterColumnsList());
}
}
}
}
}
private static void printColumn(List<CanalEntry.Column> columns) {
for (CanalEntry.Column column : columns) {
System.out.println(column.getName() + " : " + column.getValue() + " update=" + column.getUpdated());
}
}
}
**如果你正在使用Spring Boot,可以按照以下步驟編寫Canal數據同步程序:**
添加Canal客戶端庫的依賴。在pom.xml文件中添加以下依賴:
<dependencies>
<!-- Canal客戶端依賴 -->
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.5</version>
</dependency>
</dependencies>
創建一個CanalClient類,作爲數據同步的入口。可以參考之前給出的示例代碼。這個類可以是一個Spring Bean,可以加上@Component註解。
在CanalClient類中注入DataSource或者JdbcTemplate,用於連接目標數據庫B庫。可以使用Spring Boot的自動配置來簡化數據源的配置。
在CanalClient類中編寫數據處理邏輯,將變更數據同步到目標數據庫B庫。可以使用JdbcTemplate或其他ORM框架來執行SQL語句。
以下是一個示例的CanalClient類的代碼:
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.alibaba.otter.canal.protocol.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import java.net.InetSocketAddress;
import java.util.List;
[@Component](https://my.oschina.net/u/3907912)
public class CanalClient {
@Autowired
private DataSource dataSource; // 注入目標數據庫B庫的數據源
private JdbcTemplate jdbcTemplate;
@PostConstruct
public void init() {
jdbcTemplate = new JdbcTemplate(dataSource);
start();
}
public void start() {
// 創建Canal連接
CanalConnector connector = CanalConnectors.newSingleConnector(
new InetSocketAddress("Canal服務地址", 11111), "example", "", "");
// 連接Canal服務
connector.connect();
// 訂閱數據表
connector.subscribe("數據庫名.表名");
// 循環讀取數據變更消息
while (true) {
// 獲取1000條數據變更消息
Message message = connector.getWithoutAck(1000);
long batchId = message.getId();
if (batchId == -1 || message.getEntries().isEmpty()) {
continue;
}
processEntry(message.getEntries());
// 確認消息
connector.ack(batchId);
}
}
private void processEntry(List<CanalEntry.Entry> entries) {
for (CanalEntry.Entry entry : entries) {
if (entry.getEntryType() == CanalEntry.EntryType.ROWDATA) {
// 解析數據變更消息
CanalEntry.RowChange rowChange;
try {
rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
} catch (Exception e) {
throw new RuntimeException("ERROR ## parser of eromanga-event has an error, data:" + entry.toString(), e);
}
CanalEntry.EventType eventType = rowChange.getEventType();
// 處理不同類型的數據變更
if (eventType == CanalEntry.EventType.DELETE) {
processDeleteEvent(entry.getHeader().getTableName(), rowChange.getRowDatasList());
} else if (eventType == CanalEntry.EventType.INSERT) {
processInsertEvent(entry.getHeader().getTableName(), rowChange.getRowDatasList());
} else if (eventType == CanalEntry.EventType.UPDATE) {
processUpdateEvent(entry.getHeader().getTableName(), rowChange.getRowDatasList());
}
}
}
}
private void processDeleteEvent(String tableName, List<CanalEntry.RowData> rowDatas) {
// 處理刪除事件
for (CanalEntry.RowData rowData : rowDatas) {
// 構造SQL語句,執行刪除操作
String sql = "DELETE FROM " + tableName + " WHERE id = ?";
jdbcTemplate.update(sql, getColumnValue(rowData.getBeforeColumnsList(), "id"));
}
}
private void processInsertEvent(String tableName, List<CanalEntry.RowData> rowDatas) {
// 處理插入事件
for (CanalEntry.RowData rowData : rowDatas) {
// 構造SQL語句,執行插入操作
String sql = "INSERT INTO " + tableName + " (id, name) VALUES (?, ?)";
jdbcTemplate.update(sql, getColumnValue(rowData.getAfterColumnsList(), "id"), getColumnValue(rowData.getAfterColumnsList(), "name"));
}
}
private void processUpdateEvent(String tableName, List<CanalEntry.RowData> rowDatas) {
// 處理更新事件
for (CanalEntry.RowData rowData : rowDatas) {
// 構造SQL語句,執行更新操作
String sql = "UPDATE " + tableName + " SET name = ? WHERE id = ?";
jdbcTemplate.update(sql, getColumnValue(rowData.getAfterColumnsList(), "name"), getColumnValue(rowData.getBeforeColumnsList(), "id"));
}
}
private String getColumnValue(List<CanalEntry.Column> columns, String columnName) {
for (CanalEntry.Column column : columns) {
if (column.getName().equalsIgnoreCase(columnName)) {
return column.getValue();
}
}
return null;
}
}
這樣,你可以啓動Spring Boot應用程序,CanalClient會自動連接到Canal服務,並實時同步變更數據到目標數據庫B庫。
請注意替換示例代碼中的"Canal服務地址"、"數據庫名.表名"等佔位符爲實際的值。此外,需要根據具體的表結構和業務邏輯調整SQL語句和處理邏輯。
六、運行同步程序
將編寫好的同步程序部署到服務器,並運行。至此,當源數據庫的表發生變化時,Canal會將這些變化實時地同步到目標數據庫。
注意:實際的數據同步操作需要你自行編寫,包括連接目標數據庫,創建相應的SQL語句,並執行這些SQL語句。這需要你對Java的JDBC API或者其他ORM框架(如MyBatis、Hibernate等)有一定了解。這部分內容涉及的知識點較多,無法在這裏一一詳述。如果需要,你可以查閱相關的開發文檔或教程,學習如何使用這些API或框架進行數據庫操作。
當使用Spring Boot時,你可以按照以下步驟將歷史數據同步到目標數據庫B庫中:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import java.util.List;
import java.util.Map;
@Component
public class HistoryDataSync {
@Autowired
private DataSource dataSource; // 注入目標數據庫B庫的數據源
private JdbcTemplate jdbcTemplate;
@PostConstruct
public void init() {
jdbcTemplate = new JdbcTemplate(dataSource);
syncHistoryData();
}
public void syncHistoryData() {
// 獲取歷史數據
List<Map<String, Object>> historyData = getHistoryDataFromSourceDB();
// 插入歷史數據
insertDataToTargetDB(historyData);
}
private List<Map<String, Object>> getHistoryDataFromSourceDB() {
// 查詢源數據庫A庫中的歷史數據
String sql = "SELECT * FROM source_table";
return jdbcTemplate.queryForList(sql);
}
private void insertDataToTargetDB(List<Map<String, Object>> historyData) {
// 插入歷史數據到目標數據庫B庫的相應表中
String sql = "INSERT INTO target_table (column1, column2, ...) VALUES (?, ?, ...)";
jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
Map<String, Object> data = historyData.get(i);
ps.setObject(1, data.get("column1"));
ps.setObject(2, data.get("column2"));
// 設置其他列的值
}
@Override
public int getBatchSize() {
return historyData.size();
}
});
}
}
在上面的示例代碼中,getHistoryDataFromSourceDB()方法用於從源數據庫A庫中獲取歷史數據。你需要根據實際的表結構和查詢條件編寫相應的SQL語句。
insertDataToTargetDB()方法用於將歷史數據插入到目標數據庫B庫的相應表中。使用jdbcTemplate.batchUpdate()方法批量插入數據,通過實現BatchPreparedStatementSetter接口來設置插入SQL語句的參數。
請確保替換示例代碼中的表名、列名等佔位符爲實際的值。