實時同步工具canal入門

canal主要用途是基於 MySQL 數據庫增量日誌解析,提供增量數據訂閱和消費。在大數據中廣泛用於實時數據的採集。

1.canal原理

mysql並沒有實現增量數據的查閱消費功能,先來說說mysql主備複製原理。

mysql主節點對數據庫做了任何寫操作,都會寫入Binary log文件。而slave備份節點會主動去master節點讀取Binary log文件,拷貝到自己的節點上,變爲Relay log。最後slave節點讀取Relay log看看它對數據庫做了哪些操作,按照順序執行日誌的操作,達到同步更新的結果。

而canal正是利用了mysql主備複製原理。canal將自己僞裝成一個mysql的備份節點,從master拉取binary log拷貝到自己節點上,然後根據用戶的自定義的程序執行操作,可以寫入mysql,kafka,es,hbase等。

 

插曲:

其實最開始canal並不是用於大數據的,而是用於緩存的。比如一些併發訪問量大的程序可能會做一些緩存處理,而不是直接從mysql讀取。比如客戶端查詢蘋果手機價格,先從redis查詢,沒有查詢到,繼續從mysql查詢。查詢到價格後是5000元,將蘋果手機價格5000保存在redis中,下次客戶端直接從redis緩存中讀取數據。過了一段時間,保存在mysql的蘋果手機降價了,價格是4500元,如果現在再從redis緩存讀取,那麼價格是錯誤的。可是客戶端發現緩存中已經有數據了,它不可能再從mysql讀取數據了。所以我們需要實時更新redis緩存的數據,因此canal就起到了至關重要的作用。

2.canal搭建

1.開啓mysql的binlog。

默認情況下mysql的binlog是沒有開啓的,所以我們首先要開啓binlog。

修改 /etc/my.cnf 文件,在文件末尾加入以下幾句話。

server-id相當於每一個mysql服務器的唯一標識,後面的canal也要設定,和mysql的不一樣即可。

log-bin是設置日誌文件的前綴名。設置完成後操作數據庫,在 /var/lib/mysql/ 目錄下就可以看到以mysql-bin開頭的文件。

binlog_format是設置以什麼樣的形式來存儲數據庫變更的數據。這裏詳細解釋下mysql的幾種記錄方式。

(1)binlog_format=statement

直接保存mysql語句。比如你執行了一句sql,update user_info set update_time='xxxx' where create_time='xxxx';日誌文件會直接保存這句sql,大大減少了日誌文件存儲的內容。但是有一個缺點。比如update user_info set age=rand() where   update_time='xxxx';其中age的賦值是rand(),取值是隨機的。所以第二次執行sql的時候賦值會出問題。

(2)binlog_format=row

保存每行記錄的變化。也就是隻要有一行變化就記錄一行,這樣做的確規避了rand()類似的問題。但是也有一個缺點,就是update user_info set update_time='xxxx' where create_time='xxxx';這句sql如果有100萬的數據改變,那麼binlog就要記錄100萬行的數據,這顯然不是很方便。

(3)binlog_format=mixed

一般情況下mysql會用statement來記錄,遇到特殊情況就用row來記錄,相當於結合了statement和row。

介紹完3種情況,結合我們採集實時數據,是需要知道每行的數據變化的,所以這裏我們需要將binlog_format設置爲row。

binlog-do-db是設置要監控的數據庫名稱。

 

2.安裝canal

(1)下載解壓canal。

(2)修改 conf/canal.properties。

//設置canal端口號

canal.port=11111

//取消註釋

canal.instance.parser.parallelThreadSize = 16

(3)修改 conf/example/instance.properties

//slaveId和mysql的server-id要不一樣

canal.instance.mysql.slaveId=10

//設置要監控的mysql地址

canal.instance.master.address=hadoop102:3306

//由於canal要讀取mysql的內容,所以要給予canal一個權限。這裏在mysql裏面創建了一個canal賬戶。(在庫mysql執行的sql:GRANT  ALL PRIVILEGES ON *.* TO canal@'%' IDENTIFIED BY 'canal'  ,執行完可以在user表查詢是否添加成功)

canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
canal.instance.connectionCharset = UTF-8

 

3.啓動canal

./bin/startup.sh

啓動完成後可以在/bigdata/canal/logs/example.log中查看日誌是否啓動成功。

 

3.Canal客戶端消費。

1.maven引入canal客戶端,kafka客戶端(我這裏直接將結果保存在kafka中)。

<dependency>
    <groupId>com.alibaba.otter</groupId>
    <artifactId>canal.client</artifactId>
    <version>1.1.2</version>
</dependency>

<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>0.11.0.2</version>
</dependency>

2.編寫客戶端代碼

類CanalApp

import com.alibaba.fastjson.JSONObject;
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.RowChange;
import com.alibaba.otter.canal.protocol.CanalEntry.RowData;
import com.alibaba.otter.canal.protocol.Message;
import com.google.common.base.CaseFormat;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;

import java.net.InetSocketAddress;
import java.util.List;

public class CanalApp {
    public static void main(String[] args) {
        //創建連接器
        CanalConnector canalConnector = CanalConnectors.newSingleConnector(new InetSocketAddress("192.168.1.128", 11111), "example", "", "");

        while(true){
            //連接,訂閱,抓取
            canalConnector.connect();
            //監控數據庫中某張表
            canalConnector.subscribe("gmall.order_info");

            //message:一次canal從日誌中抓取的信息,一個message包含多個sql(event)
            Message message = canalConnector.get(100);
            int size = message.getEntries().size();
            if(size == 0){
                try {
                    System.out.println("沒有數據,暫停5秒");
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else{
                //entry:  相當於一個sql命令,一個sql可能會對多行記錄造成影響
                for (Entry entry : message.getEntries()){
                    //判斷事件類型,只處理行變化的數據,過濾掉事務變化的數據
                    if(entry.getEntryType().equals(EntryType.ROWDATA)){
                        ByteString storeValue = entry.getStoreValue();
                        RowChange rowChange = null;
                        try {
                            //rowchange : entry經過反序列化得到的對象,包含了多行記錄的變化值
                            rowChange = RowChange.parseFrom(storeValue);
                        } catch (InvalidProtocolBufferException e) {
                            e.printStackTrace();
                        }
                        //獲得行集
                        List<RowData> rowDataList = rowChange.getRowDatasList();
                        //eventtype  數據的變化類型  insert  update  delete  create  alter  drop
                        EventType eventType = rowChange.getEventType();
                        String tableName = entry.getHeader().getTableName();

                        //這裏只監控order_info表新增的數據
                        if("order_info".equals(tableName) && EventType.INSERT.equals(eventType)){
                            for (RowData rowData : rowDataList){
                                List<Column> afterColumnsList = rowData.getAfterColumnsList();
                                JSONObject jsonObject = new JSONObject();
                                for (Column column : afterColumnsList){
                                    System.out.println(column.getName() + ":" + column.getValue());
                                    //駝峯命名處理
                                    String propertyName = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, column.getName());
                                    jsonObject.put(propertyName, column.getValue());
                                }
                                MyKafkaSender.send("GMALL_ORDER", jsonObject.toJSONString());
                            }
                        }
                    }
                }
            }
        }
    }
}

類MyKafkaSender

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;

import java.util.Properties;

public class MyKafkaSender {

    public static KafkaProducer<String, String> kafkaProducer = null;

    public static void send(String topic, String msg) {
        if(kafkaProducer == null){
            kafkaProducer = createKafkaProducer();
        }

        kafkaProducer.send(new ProducerRecord<String, String>(topic, msg));
    }

    private static KafkaProducer<String, String> createKafkaProducer() {
        Properties properties = new Properties();
        properties.put("bootstrap.servers", "hadoop102:9092,hadoop103:9092,hadoop104:9092");
        properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
        return producer;
    }
}

 

3.運行程序查看結果。

運行zookeeper

運行kafka集羣

開啓kafka-console消費

啓動canal服務端

運行CanalApp

向表gmall.order_info插入數據後控制檯kafka消費如下:

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章