1.1、什麼是 Canal
Canal 是用 Java 開發的基於資料庫增量日誌解析,提供增量資料訂閱&消費的中介軟體。目前。Canal 主要支援了 MySQL 的 Binlog 解析,解析完成後才利用 Canal Client 來處理獲得的相關資料。(資料庫同步需要阿里的Otter中介軟體,基於Canal)。
1.2、MySQL 的 Binlog
1.2.1、什麼是 Binglog
MySQL 的二進位制日誌可以說 MySQL 最重要的日誌了,它記錄了所有的 DDL 和 DML (除了資料查詢語句)語句,以事件形式記錄,還包含語句所執行的消耗的時間,MySQL的二進位制日誌是事務安全型的。
一般來說開啟二進位制日誌大概會有1%的效能損耗。二進位制有兩個最重要的使用場景:
- 其一: MySQL Replication在Master端開啟Binlog, Master把它的二進位制日誌傳遞給Slaves 來達到Master-Slave資料一致的目的。
- 其二:自然就是資料恢復了,透過使用MySQL Binlog工具來使恢復資料。
二進位制日誌包括兩類檔案:二進位制日誌索引檔案(檔名字尾為.index
)用於記錄所有
的二進位制檔案,二進位制日誌檔案(檔名字尾為.00000*
)記錄資料庫所有的DDL和DML(除
了資料查詢語句)語句事件。
1.2.2、Binglog 分類
MySQL Binlog 的格式有三種,分別是STATEMENT
,MIXED
,ROW
。在配置檔案中可以選擇配
置binlog_ format= statement| mixed |row
。三種格式的區別:
statement
:語句級,binlog會記錄每次一執行寫操作的語句。相對row模式節省空,但是可能會會產生不一致,比如一些函式,例如update t set create_date = now()
,如果使用 binlog 日誌,進行恢復,由於執行時間不同,可能產生的資料就不同- 優點:節省空間
- 缺點:又可能造成資料不一致
row
:行級,binglog 會記錄每次操作後每行記錄的變化- 優點:保持資料的絕對一致。因為不管 sql 是什麼,引用了什麼函式,它只記錄執行後的結果
- 缺點:佔用空間較大
mixed
:stetement 的升級版,一定程度上解決了一些情況而造成的 statement 模式不一致問題,預設還是 statement,在某些情況下譬如:當函式中包含UUID() 時;包含AUTO_ INCREMENT
欄位的表被更新時;執行INSERT DELAYED
語句時;用UDF時;會按照 ROW 的方式進行處理。- 優點:節省空間,同時兼顧了一定的一致性。
- 缺點:還有些極個別情況依舊會造成不一致,另外 stetement 和 mixed 對於需要 binlog 的監控情況都不方便。
綜上所述,Canal 先做監控分析,選擇 row 格式比較合適。
1.3、Canal 工作原理
- canal 模擬 MySQL slave 的互動協議,偽裝自己為 MySQL slave ,向 MySQL master 傳送 dump 協議
- MySQL master 收到 dump 請求,開始推送 binary log 給 slave (即 canal )
- canal 解析 binary log 物件(原始為 byte 流)
1.4、使用場景
1.4.1、原始場景
阿里 Otter 中介軟體的一部分,Otter 是阿里用於進行非同步資料庫之間同步框架,Canal 是其中一部分
1.4.2、常用場景一
更新快取,例如當資料寫入到 MySQL 中時,將增加或修改的資料同步到快取中,使用者每次直接從快取中拿取資料,如果沒獲取到,再從資料庫查詢。
1.4.3、常用場景二
抓取業務表的新增變化資料,用於製作實時統計
這裡以mysql8.0.28
,canal-1.1.7-alpha-1
,為例,由於使用的是 mysql8,如果 canal 版本過低會導致各種 bug。
2.1、建立資料庫
CREATE DATABASE `canal_test` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci */ /*!80016 DEFAULT ENCRYPTION='N' */
2.2、建立資料庫表
CREATE TABLE user_info(
`id` VARCHAR(255),
`name` VARCHAR(255),
`sex` VARCHAR(255)
)
2.3、配置檔案開啟 Binglog
$ vim /etc/mysql/my.cnf
[mysqld]
log-bin=mysql-bin # 開啟 binlog
binlog-format=ROW # 選擇 ROW 模式
binglog-do-db=canal_test
server_id=1 # 配置 MySQL replaction 需要定義,不要和 canal 的 slaveId 重複
注意: binglog-do-db
根據自己情況進行修改,指定具體要同步的資料庫,如果不配置,表示所有的資料均開啟 Binglog
2.4、重啟 MySQL 生效
$ sudo systemctl restart mysqld
2.5、建立使用者並賦權
-- 由於預設密碼比較嚴格,降低密碼策略嚴格程度
SHOW VARIABLES LIKE 'validate_password%';
SET GLOBAL validate_password.policy = LOW;
SET GLOBAL validate_password.length = 4;
CREATE USER 'canal'@'%' IDENTIFIED by 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;
3.1、下載並解壓
$ mkdir canal
$ tar -zxvf canal.deployer-1.1.7-SNAPSHOT.tar.gz -C canal
3.2、修改 canal.properties 檔案
修改canal.properties
檔案
canal.serverMode = tcp
說明:這個檔案是 canal 的基本迪用配置,canal 埠號預設就是 11111,修改 canal 的輸出 model,預設tcp,改為輸出到 kafka 等訊息中介軟體。
多例項配置如果建立多個例項,透過前面canal架構,我們可以知道,一個canal服務中可以有多個instance,conf
下的每一個 example 即是一個例項,每個例項下面都有獨立的配置檔案。預設只有一個例項 example,如果需要多個例項處理不同的 MySQL 資料的話,直接複製出多個 example,並對其重新命名,命名和配置檔案中指定的名稱一致,然後修改 canal.properties
中的 canal.destinations=例項1,例項2,例項3
canal.destinations = example
3.3、修改 instance.properties
這裡按一個資料庫為例
$ cd canal/conf/example
$ vim instance.properties
3.3.1、配置 MySQL 伺服器地址
canal.instance.mysql.slaveId=20
canal.instance.master.address=ip:port
# 關閉 tsdb
canal.instance.tsdb.enable=false
3.3.2、配置連線 MySQL 使用者名稱和密碼
# username/password
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
canal.instance.connectionCharset = UTF-8
# enable druid Decrypt database password
canal.instance.enableDruid=false
3.3.3、 開放埠
# canal admin 埠
$ firewall-cmd --zone=public --add-port=11110/tcp --permanent
# canal 監聽埠
$ firewall-cmd --zone=public --add-port=11111/tcp --permanent
# canal 指標短褲哦
$ firewall-cmd --zone=public --add-port=11112/tcp --permanent
$ firewall-cmd --reload
4.1、拉取映象
# 拉取映象
$ docker pull canal/canal-server:v1.1.6
# 下載指令碼
$ wget https://raw.githubusercontent.com/alibaba/canal/master/docker/run.sh
# 構建一個destination name為test的佇列
sh run.sh -e canal.auto.scan=false
-e canal.destinations=test
-e canal.instance.master.address=127.0.0.1:3306
-e canal.instance.dbUsername=canal
-e canal.instance.dbPassword=canal
-e canal.instance.connectionCharset=UTF-8
-e canal.instance.tsdb.enable=true
-e canal.instance.gtidon=false
問題說明
canal 啟動時,報錯
因為自MySQL 8.0.3開始,身份驗證外掛預設使用caching_sha2_password
問題解決
修改canal使用者對應的身份驗證外掛為 mysql_native_password
mysql> select host,user,plugin from mysql.user ;
mysql> ALTER USER 'canal'@'%' IDENTIFIED WITH mysql_native_password BY 'password';
再次啟動即可。
6.1、引入依賴
<dependencies>
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.6</version>
</dependency>
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.protocol</artifactId>
<version>1.1.6</version>
</dependency>
</dependencies>
6.2、客戶端程式碼
public class CanalClient {
private static final Logger log = LoggerFactory.getLogger(CanalClient.class);
public static void main(String[] args) throws InterruptedException, InvalidProtocolBufferException {
// 獲取連線
CanalConnector canalConnector =
CanalConnectors.newSingleConnector(new InetSocketAddress("192.168.33.81", 11111), "example", "", "");
// 連線
canalConnector.connect();
// 訂閱資料庫,這裡一定要是world.*否則無法獲取到資料
canalConnector.subscribe("world.*");
while (true) {
// 獲取資料,一次拉取 100 條資料
Message message = canalConnector.get(100);
List<CanalEntry.Entry> entries = message.getEntries();
// 判斷集合是否為空,如果為空則等待之後再拉取
if (entries.isEmpty()) {
log.warn("當此抓取沒有資料,等待片刻");
TimeUnit.SECONDS.sleep(5);
} else {
// 遍歷 entries
for (CanalEntry.Entry entry : entries) {
// 1.獲取表名
String tableName = entry.getHeader().getTableName();
// 2.獲取型別
CanalEntry.EntryType entryType = entry.getEntryType();
// 3.獲取序列化的資料
ByteString storeValue = entry.getStoreValue();
// 4.判斷 entryType 是否為 ROWDATA 型別
if (CanalEntry.EntryType.ROWDATA.equals(entryType)) {
// 5.反序列化資料
CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(storeValue);
//6.獲取當前時間的操作型別
CanalEntry.EventType eventType = rowChange.getEventType();
//7.獲取行資料集
List<CanalEntry.RowData> rowDatesList = rowChange.getRowDatasList();
//8.遍歷 rowDatesList 並列印資料集
for (CanalEntry.RowData rowData : rowDatesList) {
JSONObject beforeDate = new JSONObject();
List<CanalEntry.Column> beforeColumnsList = rowData.getBeforeColumnsList();
for (CanalEntry.Column column : beforeColumnsList) {
beforeDate.put(column.getName(), column.getValue());
}
JSONObject afterDate = new JSONObject();
List<CanalEntry.Column> afterColumnsList = rowData.getAfterColumnsList();
for (CanalEntry.Column column : afterColumnsList) {
afterDate.put(column.getName(), column.getValue());
}
// 資料的列印
log.info("Table: {},EventType: {},Before: {},After: {}", tableName, eventType, beforeDate, afterDate);
}
} else {
log.warn("當前操作型別為: {}", entryType);
}
}
}
}
}
}
6.3、測試
6.3.1、新增資料
INSERT INTO user_info VALUES('1002','test2','female'),('1003','test3','male');
效果如下
6.3.2、刪除資料
DELETE FROM user_info WHERE id = '1001';
6.3.3、修改資料
UPDATE user_info SET name = 'female' WHERE id = '1003';
本作品採用《CC 協議》,轉載必須註明作者和本文連結