前言
大約兩年以前,筆者在一個專案中遇到了資料同步的難題。
當時,系統部署了幾十個例項,分為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
解壓完成後,我們可以看到這樣一個目錄:
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的消費順序性相關解答。