使用canal偷取MySQL的二進位制日誌

翻身碼農把歌唱發表於2018-09-13

前提

安裝完MySQL(我安裝的是5.7),安裝JDK(canal依賴)

開啟MySQL的binlog

開啟binlog,並且將binlog的格式改為Row,這樣就可以獲取到CURD的二進位制內容。配置/etc/my.cnf,在[mysqld]增加

log-bin=mysql-bin #新增這一行就ok
binlog-format=ROW #選擇row模式
server_id=1 # 唯一,不能和其他叢集MySQL的server_id一樣
複製程式碼

驗證binlog是否開啟

登入MySQL,使用命令:

show variables like 'log_%';
複製程式碼

若 log_bin顯示為 on ,則說明開啟。

給canal分配MySQL的賬號

給canal分配一個MySQL的賬號,方便canal偷取MySQL的binlog。

CREATE USER canal IDENTIFIED BY 'canal';
GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;
複製程式碼

檢視是否給canal賬號分配許可權

show grants for 'canal' 
複製程式碼

下載解壓canal

地址:github.com/alibaba/can… ,目前穩定版是 v1.1.0,下載 canal.deployer-1.1.0.tar.gz。解壓到 canal目錄下(沒有該目錄 就新建)

注:canal 是純Java寫的,所有需要依賴JDK環境,我這邊使用的是:1.8.0_65-b17

# 下載
wget https://github.com/alibaba/canal/releases/download/canal-1.1.0/canal.deployer-1.1.0.tar.gz
# 建立canal目錄
mkdir canal
# 解壓
tar -zxvf https://github.com/alibaba/canal/releases/download/canal-1.1.0/canal.deployer-1.1.0.tar.gz 
複製程式碼

canal和instance配置檔案

一個canal裡面可能會有多個instance,也就說一個instance可以監控一個mysql例項,多個instance也就可以對應多臺伺服器的mysql例項。也就是一個canal就可以監控分庫分表下的多機器MySQL。

(1)canal.properties

canal/config 中的canal.properties檔案,是全域性性的canal伺服器配置 ,修改內容如下:

#################################################
######### 		common argument		############# 
#################################################
# id唯一,不可與mysql的server_id重複
canal.id= 2		
canal.ip=
canal.port=11111
canal.metrics.pull.port=11112
canal.zkServers=
# flush data to zk
canal.zookeeper.flush.period = 1000
canal.withoutNetty = false
# flush meta cursor/parse position to file
canal.file.data.dir = ${canal.conf.dir}
canal.file.flush.period = 1000
## memory store RingBuffer size, should be Math.pow(2,n)
canal.instance.memory.buffer.size = 16384
## memory store RingBuffer used memory unit size , default 1kb
canal.instance.memory.buffer.memunit = 1024 
## meory store gets mode used MEMSIZE or ITEMSIZE
canal.instance.memory.batch.mode = MEMSIZE

## detecing config
canal.instance.detecting.enable = false
#canal.instance.detecting.sql = insert into retl.xdual values(1,now()) on duplicate key update x=now()
canal.instance.detecting.sql = select 1
canal.instance.detecting.interval.time = 3
canal.instance.detecting.retry.threshold = 3
canal.instance.detecting.heartbeatHaEnable = false

# support maximum transaction size, more than the size of the transaction will be cut into multiple transactions delivery
canal.instance.transaction.size =  1024
# mysql fallback connected to new master should fallback times
canal.instance.fallbackIntervalInSeconds = 60

# network config
canal.instance.network.receiveBufferSize = 16384
canal.instance.network.sendBufferSize = 16384
canal.instance.network.soTimeout = 30

# binlog filter config
canal.instance.filter.druid.ddl = true
canal.instance.filter.query.dcl = false
canal.instance.filter.query.dml = false
canal.instance.filter.query.ddl = false
canal.instance.filter.table.error = false
canal.instance.filter.rows = false
canal.instance.filter.transaction.entry = false

# binlog format/image check
canal.instance.binlog.format = ROW,STATEMENT,MIXED 
canal.instance.binlog.image = FULL,MINIMAL,NOBLOB

# binlog ddl isolation
canal.instance.get.ddl.isolation = false

# parallel parser config
canal.instance.parser.parallel = true
## concurrent thread number, default 60% available processors, suggest not to exceed Runtime.getRuntime().availableProcessors()
# parallelThreadSize預設是註釋掉的,原值為16,因為canal裝在本地VM上,分配了1個CPU,導致報錯,改為1
canal.instance.parser.parallelThreadSize = 1
## disruptor ringbuffer size, must be power of 2
canal.instance.parser.parallelBufferSize = 256

# table meta tsdb info
canal.instance.tsdb.enable=true
canal.instance.tsdb.dir=${canal.file.data.dir:../conf}/${canal.instance.destination:}
canal.instance.tsdb.url=jdbc:h2:${canal.instance.tsdb.dir}/h2;CACHE_SIZE=1000;MODE=MYSQL;
canal.instance.tsdb.dbUsername=canal
canal.instance.tsdb.dbPassword=canal

# rds oss binlog account
canal.instance.rds.accesskey =
canal.instance.rds.secretkey =

#################################################
######### 		destinations		############# 
#################################################
canal.destinations= example
# conf root dir
canal.conf.dir = ../conf
# auto scan instance dir add/remove and start/stop instance
canal.auto.scan = true
canal.auto.scan.interval = 5

canal.instance.tsdb.spring.xml=classpath:spring/tsdb/h2-tsdb.xml
#canal.instance.tsdb.spring.xml=classpath:spring/tsdb/mysql-tsdb.xml

canal.instance.global.mode = spring 
canal.instance.global.lazy = false
#canal.instance.global.manager.address = 127.0.0.1:1099
#canal.instance.global.spring.xml = classpath:spring/memory-instance.xml
canal.instance.global.spring.xml = classpath:spring/file-instance.xml
#canal.instance.global.spring.xml = classpath:spring/default-instance.xml

# position info,需要改成自己的資料庫資訊  
canal.instance.master.address = 127.0.0.1:3306   
canal.instance.master.journal.name =
canal.instance.master.position =
canal.instance.master.timestamp =

# username/password,需要改成自己的資料庫資訊  
canal.instance.dbUsername = canal
canal.instance.dbPassword = canal
canal.instance.defaultDatabaseName = test
canal.instance.connectionCharset = UTF-8  

# table regex  
canal.instance.filter.regex = .*\\..*

複製程式碼
(2)instance.properties

位於 canal/example/instance.properties,是具體的某個instances例項的配置,未涉及到的配置都會從canal.properties上繼承,內容如下:

#################################################
## mysql serverId , v1.0.26+ will autoGen 
# canal.instance.mysql.slaveId=0

# enable gtid use true/false
canal.instance.gtidon=false

# position info address修改為自己的mysql地址
canal.instance.master.address=192.168.204.128:3306
canal.instance.master.journal.name=
canal.instance.master.position=
canal.instance.master.timestamp=
canal.instance.master.gtid=

# rds oss binlog
canal.instance.rds.accesskey=
canal.instance.rds.secretkey=
canal.instance.rds.instanceId=

# table meta tsdb info
canal.instance.tsdb.enable=true
#canal.instance.tsdb.url=jdbc:mysql://127.0.0.1:3306/canal_tsdb
#canal.instance.tsdb.dbUsername=canal
#canal.instance.tsdb.dbPassword=canal

#canal.instance.standby.address =
#canal.instance.standby.journal.name =
#canal.instance.standby.position = 
#canal.instance.standby.timestamp =
#canal.instance.standby.gtid=

# username/password  修改為在mysql中給canal同步資料的賬號 密碼
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
# 監聽的資料庫
canal.instance.defaultDatabaseName=test
canal.instance.connectionCharset=UTF-8

# table regex
canal.instance.filter.regex=.*\\..*
# table black regex
canal.instance.filter.black.regex=
#################################################
複製程式碼

建立test資料庫

檢視MySQL上是否有test資料庫,沒有則建立

開啟canal

進入canal/bin,執行:./startup.sh。

使用 ps -ef|grep canal 驗證是否開啟。

Java client程式碼

建立SpringBoot工程,引入依賴:

<dependency>
     <groupId>com.alibaba.otter</groupId>
     <artifactId>canal.client</artifactId>
     <version>1.1.0</version>
 </dependency>
複製程式碼

建立TestCanal類:

package com.xbq.canal.test;

import java.awt.Event;
import java.net.InetSocketAddress;
import java.util.List;
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry.Column;
import com.alibaba.otter.canal.protocol.CanalEntry.Entry;
import com.alibaba.otter.canal.protocol.CanalEntry.EntryType;
import com.alibaba.otter.canal.protocol.CanalEntry.EventType;
import com.alibaba.otter.canal.protocol.CanalEntry.Header;
import com.alibaba.otter.canal.protocol.CanalEntry.RowChange;
import com.alibaba.otter.canal.protocol.Message;
import com.google.protobuf.InvalidProtocolBufferException;

/**
 * @Auther: xbq
 * @Date: 2018/9/11 19:16
 * @Description:
 */
public class TestCanal {

    public static void main(String[] args)  throws InterruptedException {
        // 第一步:與canal進行連線
        CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("192.168.204.128", 11111),
                "example", "", "");
        connector.connect();
        // 第二步:開啟訂閱
        connector.subscribe();
        // 第三步:迴圈訂閱
        while (true) {
            try {
                // 每次讀取 1000 條
                Message message = connector.getWithoutAck(1000);
                long batchID = message.getId();
                int size = message.getEntries().size();
                if (batchID == -1 || size == 0) {
                    System.out.println("當前暫時沒有資料");
                    Thread.sleep(1000);
                } else {
                    System.out.println("-------------------------- 有資料啦 -----------------------");
                    PrintEntry(message.getEntries());
                }
                // position id ack (方便處理下一條)
                connector.ack(batchID);
            } catch (Exception e) {
                // TODO: handle exception
            } finally {
                Thread.sleep(1000);
            }
        }
    }

    /**
     * 獲取每條列印的記錄
     * @param entrys
     */
    public static void PrintEntry(List<Entry> entrys) {
        for (Entry entry : entrys) {
            // 第一步:拆解entry 實體
            Header header = entry.getHeader();
            EntryType entryType = entry.getEntryType();

            // 第二步: 如果當前是RowData,那就是我需要的資料
            if (entryType == EntryType.ROWDATA) {
                String tableName = header.getTableName();
                String schemaName = header.getSchemaName();
                RowChange rowChange = null;
                try {
                    rowChange = RowChange.parseFrom(entry.getStoreValue());
                } catch (InvalidProtocolBufferException e) {
                    e.printStackTrace();
                }
                EventType eventType = rowChange.getEventType();
                System.out.println(String.format("當前正在操作 %s.%s, Action= %s", schemaName, tableName, eventType));

                // 如果是‘查詢’ 或者 是 ‘DDL’ 操作,那麼sql直接打出來
                if (eventType == EventType.QUERY || rowChange.getIsDdl()) {
                    System.out.println("rowchange sql ----->" + rowChange.getSql());
                    return;
                }
                // 第三步:追蹤到 columns 級別
                rowChange.getRowDatasList().forEach((rowData) -> {
                    // 獲取更新之前的column情況
                    List<Column> beforeColumns = rowData.getBeforeColumnsList();
                    // 獲取更新之後的 column 情況
                    List<Column> afterColumns = rowData.getAfterColumnsList();
                    // 當前執行的是 刪除操作
                    if (eventType == EventType.DELETE) {
                        PrintColumn(beforeColumns);
                    }
                    // 當前執行的是 插入操作
                    if (eventType == EventType.INSERT) {
                        PrintColumn(afterColumns);
                    }
                    // 當前執行的是 更新操作
                    if (eventType == EventType.UPDATE) {
                        PrintColumn(afterColumns);
                    }
                });
            }
        }
    }

    /**
     * 每個row上面的每一個column 的更改情況
     * @param columns
     */
    public static void PrintColumn(List<Column> columns) {
        columns.forEach((column) -> {
            String columnName = column.getName();
            String columnValue = column.getValue();
            String columnType = column.getMysqlType();
            // 判斷 該欄位是否更新
            boolean isUpdated = column.getUpdated();
            System.out.println(String.format("columnName=%s, columnValue=%s, columnType=%s, isUpdated=%s", columnName,
                    columnValue, columnType, isUpdated));
        });
    }
}
複製程式碼

執行此類。在MySQL test資料庫中建立student表,對其進行增刪改,可以發現控制檯上列印:有資料庫啦……

參考

[快取一致性和跨伺服器查詢的資料異構解決方案canal

歡迎關注我的公眾號~ 搜尋公眾號: 翻身碼農把歌唱 或者 掃描下方二維碼:

img

相關文章