資料同步利器 - canal

清幽之地發表於2020-02-26

前言

大約兩年以前,筆者在一個專案中遇到了資料同步的難題。

當時,系統部署了幾十個例項,分為1箇中心平臺和N個分中心平臺,而每一個系統都對應一個單獨的資料庫例項。

在資料庫層面,有這樣一個需求:

  • 中心平臺資料庫要包含所有系統平臺的資料。
  • 分中心資料庫只包含本系統平臺的資料。
  • 在中心平臺可以新增或修改 中心平臺的資料,但要講資料實時同步到對應的分中心平臺資料庫。

這幾十個資料庫例項之間,沒有明確的主從關係,是否同步還要看資料的來源,所以並不能用MySQL的主從同步來做。

當時,筆者實驗了幾種方式,最後採用的方式是基於Mybatis攔截器機制 + 訊息佇列的方式來做的。

大概原理是通過Mybatis攔截器,攔截到事務操作,比如新增、修改和刪除,根據自定義的資料主鍵(標識資料來源和去向),封裝成物件,投遞到訊息佇列對應的topic中去。然後,每個系統監聽不同的topic,消費資料並同步到資料庫。

在此後的一段時間裡,知道了canal這個開源元件。發現它更直接,它可以從MySQL的binlog中解析資料,投遞到訊息佇列或其它地方。

一、canal簡介

說起canal,也是阿里巴巴存在資料同步的業務需求。所以從2010年開始,阿里系公司開始逐步的嘗試基於資料庫的日誌解析,獲取增量變更進行同步,由此衍生出了增量訂閱&消費的業務。

基於日誌增量訂閱&消費支援的業務:

  • 資料庫映象
  • 資料庫實時備份
  • 多級索引 (賣家和買家各自分庫索引)
  • search build
  • 業務cache重新整理
  • 價格變化等重要業務訊息

我們正可以基於canal的機制,來完成一系列如資料同步、快取重新整理等業務。

二、啟動canal

1、修改MySQL配置

對於自建的MySQL服務, 需要先開啟 Binlog 寫入功能,配置 binlog-format 為 ROW 模式,my.cnf 中配置如下:

[mysqld]
log-bin=mysql-bin # 開啟 binlog
binlog-format=ROW # 選擇 ROW 模式
server_id=1 # 配置 MySQL replaction 需要定義,不要和 canal 的 slaveId 重複
複製程式碼

然後建立一個賬戶,用來連結MySQL,作為 MySQL slave 的許可權。

CREATE USER canal IDENTIFIED BY 'canal';  
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
-- GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' ;
FLUSH PRIVILEGES;
複製程式碼

2、下載

下載canal非常簡單,訪問 releases頁面選擇需要的包下載,然後將下載的包解壓到指定的目錄即可。

tar -zxvf canal.deployer-1.1.4.tar.gz -C /canal

解壓完成後,我們可以看到這樣一個目錄:

資料同步利器 - canal

3、修改配置

在啟動之前,還需要修改一些配置資訊。

首先,定位到canal/conf/example ,編輯instance.properties配置檔案,重點有幾項:

canal.instance.mysql.slaveId=1234               # canal模擬slaveid
canal.instance.master.address=127.0.0.1:3306    # MySQL資料庫地址
canal.instance.dbUsername=canal                 # 作為slave角色的賬戶
canal.instance.dbPassword=canal                 # 作為slave角色的賬戶密碼
canal.instance.connectionCharset = UTF-8        # 資料庫編碼方式對應Java中的編碼型別
canal.instance.filter.regex=.*\\..*             # 表過濾的表示式
canal.mq.topic=example                          # MQ 主題名稱
複製程式碼

我們希望canal監聽到的資料,要傳送到訊息佇列中,還需要修改canal.properties檔案,在這裡主要是MQ的配置。在這裡筆者使用的是阿里雲版RocketMQ,引數如下:

# 配置ak/sk
canal.aliyun.accessKey = XXX
canal.aliyun.secretKey = XXX
# 配置topic
canal.mq.accessChannel = cloud
canal.mq.servers = 內網接入點
canal.mq.producerGroup = GID_**group(在後臺建立)
canal.mq.namespace = rocketmq例項id
canal.mq.topic=(在後臺建立)
複製程式碼

4、啟動

直接執行啟動指令碼即可執行:./canal/bin/startup.sh 。 然後開啟logs/canal/canal.log檔案,可以看到啟動效果。

2020-02-26 21:12:36.715 [main] INFO  com.alibaba.otter.canal.deployer.CanalStarter - ## start the canal server.
2020-02-26 21:12:36.746 [main] INFO  com.alibaba.otter.canal.deployer.CanalController - ## start the canal server[192.168.44.128(192.168.44.128):11111]
2020-02-26 21:12:37.406 [main] INFO  com.alibaba.otter.canal.deployer.CanalStarter - ## the canal server is running now ......
複製程式碼

三、啟動MQ監聽

我們把canal監聽到的資料,投送到了訊息佇列中,那麼接下來就是寫個監聽程式來消費其中的資料。

為了方便,筆者直接使用的是阿里雲版RocketMQ,測試程式碼如下:

public static void main(String[] args) {
	Properties properties = new Properties();
	// 您在控制檯建立的 Group ID
	properties.put(PropertyKeyConst.GROUP_ID, "GID_CANAL");
	// AccessKey 阿里雲身份驗證,在阿里雲伺服器管理控制檯建立
	properties.put(PropertyKeyConst.AccessKey, "accessKey");
	// SecretKey 阿里雲身份驗證,在阿里雲伺服器管理控制檯建立
	properties.put(PropertyKeyConst.SecretKey, "secretKey");
	// 設定 TCP 接入域名,到控制檯的例項基本資訊中檢視
	properties.put(PropertyKeyConst.NAMESRV_ADDR,"http://MQ_INST_xxx.mq-internet.aliyuncs.com:80");
	// 叢集訂閱方式(預設)
	// properties.put(PropertyKeyConst.MessageModel, PropertyValueConst.CLUSTERING);
	Consumer consumer = ONSFactory.createConsumer(properties);
	consumer.subscribe("example","*",new CanalListener());
	consumer.start();
	logger.info("Consumer Started");
}
複製程式碼

四、測試

把環境都部署好之後,我們進入測試階段來看一看實際效果。

我們以一張t_account表為例,這裡面記錄著賬戶id和賬戶餘額。

首先,我們新增一條記錄,insert into t_account (id,user_id,amount) values (4,4,200);

此時,MQ消費到資料如下:

{
	"data": [{
		"id": "4",
		"user_id": "4",
		"amount": "200.0"
	}],
	"database": "seata",
	"es": 1582723607000,
	"id": 2,
	"isDdl": false,
	"mysqlType": {
		"id": "int(11)",
		"user_id": "varchar(255)",
		"amount": "double(14,2)"
	},
	"old": null,
	"pkNames": ["id"],
	"sql": "",
	"sqlType": {
		"id": 4,
		"user_id": 12,
		"amount": 8
	},
	"table": "t_account",
	"ts": 1582723607656,
	"type": "INSERT"
}
複製程式碼

通過資料可以看到,這裡面詳細記錄了資料庫的名稱、表的名稱、表的欄位和新增資料的內容等。

然後,我們還可以把這條資料修改一下:update t_account set amount = 150 where id = 4;

此時,MQ消費到資料如下:

{
	"data": [{
		"id": "4",
		"user_id": "4",
		"amount": "150.0"
	}],
	"database": "seata",
	"es": 1582724016000,
	"id": 3,
	"isDdl": false,
	"mysqlType": {
		"id": "int(11)",
		"user_id": "varchar(255)",
		"amount": "double(14,2)"
	},
	"old": [{
		"amount": "200.0"
	}],
	"pkNames": ["id"],
	"sql": "",
	"sqlType": {
		"id": 4,
		"user_id": 12,
		"amount": 8
	},
	"table": "t_account",
	"ts": 1582724016353,
	"type": "UPDATE"
}
複製程式碼

可以看到,除了修改後的內容,canal還用old欄位記錄了修改前欄位的值。

最後,我們刪除這條資料:delete from t_account where id = 4;

相應的,MQ消費到資料如下:

{
	"data": [{
		"id": "4",
		"user_id": "4",
		"amount": "150.0"
	}],
	"database": "seata",
	"es": 1582724155000,
	"id": 4,
	"isDdl": false,
	"mysqlType": {
		"id": "int(11)",
		"user_id": "varchar(255)",
		"amount": "double(14,2)"
	},
	"old": null,
	"pkNames": ["id"],
	"sql": "",
	"sqlType": {
		"id": 4,
		"user_id": 12,
		"amount": 8
	},
	"table": "t_account",
	"ts": 1582724155370,
	"type": "DELETE"
}
複製程式碼

監聽到資料庫表的變化之後,就可以根據自己的業務場景,對這些資料進行業務上的處理啦。

五、總結

可以看到,利用canal元件可以很方便的完成對資料變化的監聽。如果利用訊息佇列來做資料同步的話,只有一點需要格外注意,即訊息順序性的問題。

binlog本身是有序的,但寫入到mq之後如何保障順序是值得關注的問題。

mq順序性問題這裡,可以看到canal的消費順序性相關解答。

相關文章