數據異構之 Canal 初探(技巧篇)

源碼分析 Canal 系列開始了,一個全新的系列,即能探討 canal 本身的實現原理,也是筆者源碼閱讀技巧的展示。

1、應用場景

提到 Canal,大家應該都能想到這是一個用於解析 MySQL binlog 日誌的工具,並將 MySQL 數據庫中數據同步到其他存儲介質中,例如 Elasticsearch。

即 Canal 一個非常常用的使用場景:數據異構,一種更高級別的數據讀寫分離架構設計方法。

隨着業務不斷的發展,企業發展到一定階段,發現單體的關係型數據庫已無法支撐業務高速發展帶來數據不斷累積的壓力,從而會誕生出一種設計架構:分庫分表。分庫分表對緩解單庫數據庫壓力確實是一種非常好的解決方案,但又衍生出另外一種困境,關聯查詢不友好,甚至跨庫JOIN就更加如此。

舉例說明如下: 例如一個訂單系統,通常有兩類用戶需要去查詢訂單,一類是顧客,一類是商家,在對數據庫進行分庫分表時,如果以顧客(buy_id)進行分庫的話,同一個商家的訂單數據會分佈在不同的庫中,如果以商家(shop_id)進行分庫的話,同一個用戶購買的所有訂單數據將會分佈子啊不同的庫中,這樣進行關聯查詢,就必然需要跨庫進行join,其成本都會偏高。而且上面的場景只能滿足一方的需求,那如何是好呢?

Canal 這個時候就閃亮登場了,在電商設計中,其實商家、顧客會被拆分成兩個不同的服務,我們可以爲兩個不同的服務搭建不同的數據庫集羣,我們可以用戶訂單庫、商家訂單庫進行分庫,以用戶訂單庫爲主庫,當用戶在訂單系統下單後,數據進入到用戶訂單庫中,然後可以通過 canal 監聽數據庫的binlog日誌,然後將數據再同步到商家訂單庫,而用戶訂單庫以用戶ID爲維度進行分庫,商家訂單庫以商家ID做分庫,完美解決問題。

2、架構設計原理

在瞭解到 Canal 的基本使用場景後,我們通過 canal 官方文檔,去探究一下其核心架構設計理念,以此打開進入 Canal 的神祕世界中。

首先我們簡單看一下 MySQL 的主從同步原理:
在這裏插入圖片描述
從上面的圖中可以看成主從複製主要分成三個步驟:

  • master將改變記錄到二進制日誌(binary log ) 中( 這些記錄叫做二進制日誌事件,binary log events,可以通過show binlog events進行查看)

  • slave將master的binary log events拷貝到它的中繼日誌(relay log)

  • slave重做中繼日誌中的事件,將改變反映它自己的數據。

基於 MySQL 這種數據同步機制,那 Canal 的設計目標主要就是實現數據的同步,即數據的複製,從上面的圖自然而然的想到了如下的設計:
在這裏插入圖片描述
原理相對比較簡單:

  • canal 模擬 mysql slave 的交互協議,僞裝自己爲 mysql slave,向 mysql master 發送 dump 協議

  • mysql master 收到 dump 請求,開始推送 binary log 給 slave (canal)

  • canal解析 binary log 對象(原始爲byte流)

接下來我們來看一下 Canale 的整體組成部分:
在這裏插入圖片描述
說明:

  • server代表一個canal運行實例,對應於一個jvm

  • instance對應於一個數據隊列 (1個server對應1…n個instance)

instance模塊:

  • eventParser (數據源接入,模擬slave協議和master進行交互,協議解析)

  • eventSink (Parser和Store鏈接器,進行數據過濾,加工,分發的工作)

  • eventStore (數據存儲)

  • metaManager (增量訂閱&消費信息管理器)

這些組件我暫時不打算深入去研究,因爲在目前這個階段我自己也不清楚,但這個是我後續需要學習研究的重點。

3、在 IntelliJ IDEA 中運行 Canal Demo

在 Linux 環境中安裝 canal 比較簡單,大家可以安裝官方手冊一步一步操作即可,在這裏我就不重複介紹,本節主要的目的是希望在開發工具中運行 Canal 的 Demo,以便後續在研究源碼的過程中遇到難題時可以進行 Debug。

溫馨提示:大家在學習過程中,可以根據官方文檔先安裝一遍 canal,對理解 Canal 的核心組件有着非常重要的幫助。

首先先從 canal 源碼中尋找官方提供的 Demo,其示例代碼在 example 包中,如下圖所示:
在這裏插入圖片描述
但是另外稍微遺憾的是 canal 提供提供的示例代碼中只包含了 client 端相關的代碼,並沒有包含服務端(server),故我們將目光放到其單元測試中,如下圖所示:
在這裏插入圖片描述
接下來我根據官方的一些提示,結合自己的理解,編寫出如下測試代碼,在 IDEA 開發工具中實現運行 Canal 相關的 Demo。下面的代碼已通過測試,可直接使用。

1、Canal Server Demo

package com.alibaba.otter.canal.server;
import com.alibaba.otter.canal.instance.core.CanalInstance;
import com.alibaba.otter.canal.instance.core.CanalInstanceGenerator;
import com.alibaba.otter.canal.instance.manager.CanalInstanceWithManager;
import com.alibaba.otter.canal.instance.manager.model.Canal;
import com.alibaba.otter.canal.instance.manager.model.CanalParameter;
import com.alibaba.otter.canal.server.embedded.CanalServerWithEmbedded;
import com.alibaba.otter.canal.server.netty.CanalServerWithNetty;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.Arrays;
public class CanalServerTestMain {
    protected static final String ZK_CLUSTER_ADDRESS      = "127.0.0.1:2181";
    protected static final String DESTINATION   = "example";
    protected static final String DETECTING_SQL = "select 1";
    protected static final String MYSQL_ADDRESS = "127.0.0.1";
    protected static final String USERNAME      = "canal";
    protected static final String PASSWORD      = "canal";
    protected static final String FILTER        = ".\\*\\\\\\\\..\\*";
    /** 默認 500s 後關閉 */
    protected static final long RUN_TIME = 120 * 1000;
    private final ByteBuffer header        = ByteBuffer.allocate(4);
    private CanalServerWithNetty nettyServer;
    public static void main(String[] args) {
        CanalServerTestMain test = new CanalServerTestMain();
        try {
            test.setUp();
            System.out.println("start");
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println("sleep");
            try {
                Thread.sleep(RUN_TIME);
            } catch (Throwable ee) {
            }
            test.tearDown();
            System.out.println("end");
        }
    }
    public void setUp() {
        CanalServerWithEmbedded embeddedServer = new CanalServerWithEmbedded();
        embeddedServer.setCanalInstanceGenerator(new CanalInstanceGenerator() {
            public CanalInstance generate(String destination) {
                Canal canal = buildCanal();
                return new CanalInstanceWithManager(canal, FILTER);
            }
        });
        nettyServer = CanalServerWithNetty.instance();
        nettyServer.setEmbeddedServer(embeddedServer);
        nettyServer.setPort(11111);
        nettyServer.start();
        // 啓動 instance
        embeddedServer.start("example");
    }
    public void tearDown() {
        nettyServer.stop();
    }
    private Canal buildCanal() {
        Canal canal = new Canal();
        canal.setId(1L);
        canal.setName(DESTINATION);
        canal.setDesc("test");
        CanalParameter parameter = new CanalParameter();
        //parameter.setZkClusters(Arrays.asList(ZK_CLUSTER_ADDRESS));
        parameter.setMetaMode(CanalParameter.MetaMode.MEMORY);
        parameter.setHaMode(CanalParameter.HAMode.HEARTBEAT);
        parameter.setIndexMode(CanalParameter.IndexMode.MEMORY);
        parameter.setStorageMode(CanalParameter.StorageMode.MEMORY);
        parameter.setMemoryStorageBufferSize(32 * 1024);
        parameter.setSourcingType(CanalParameter.SourcingType.MYSQL);
        parameter.setDbAddresses(Arrays.asList(new InetSocketAddress(MYSQL_ADDRESS, 3306),
                new InetSocketAddress(MYSQL_ADDRESS, 3306)));
        parameter.setDbUsername(USERNAME);
        parameter.setDbPassword(PASSWORD);
        parameter.setSlaveId(1234L);
        parameter.setDefaultConnectionTimeoutInSeconds(30);
        parameter.setConnectionCharset("UTF-8");
        parameter.setConnectionCharsetNumber((byte) 33);
        parameter.setReceiveBufferSize(8 * 1024);
        parameter.setSendBufferSize(8 * 1024);
        parameter.setDetectingEnable(false);
        parameter.setDetectingIntervalInSeconds(10);
        parameter.setDetectingRetryTimes(3);
        parameter.setDetectingSQL(DETECTING_SQL);
        canal.setCanalParameter(parameter);
        return canal;
    }
}

2、Canal Client Demo

package com.alibaba.otter.canal.example;
import java.net.InetSocketAddress;
import java.util.List;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.common.utils.AddressUtils;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.alibaba.otter.canal.protocol.Message;
import com.alibaba.otter.canal.protocol.CanalEntry.Column;
import com.alibaba.otter.canal.protocol.CanalEntry.EventType;
public class SimpleCanalClientExample {
    public static void main(String[] args) {
        // 創建鏈接
        CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(AddressUtils.getHostIp(),
                11111), "example", "", "");
        int batchSize = 1000;
        int emptyCount = 0;
        try {
            connector.connect();
            connector.subscribe(".*\\..*");
            connector.rollback();
            int totalEmptyCount = 3000;
            while (emptyCount < totalEmptyCount) {
                Message message = connector.getWithoutAck(batchSize); // 獲取指定數量的數據
                long batchId = message.getId();
                int size = message.getEntries().size();
                if (batchId == -1 || size == 0) {
                    emptyCount++;
                    System.out.println("empty count : " + emptyCount);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                    }
                } else {
                    emptyCount = 0;
                    // System.out.printf("message[batchId=%s,size=%s] \n", batchId, size);
                    printEntry(message.getEntries());
                }
                connector.ack(batchId); // 提交確認
                // connector.rollback(batchId); // 處理失敗, 回滾數據
            }
            System.out.println("empty too many times, exit");
        } finally {
            connector.disconnect();
        }
    }
    private static void printEntry(List<CanalEntry.Entry> entrys) {
        for (CanalEntry.Entry entry : entrys) {
            if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN || entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {
                continue;
            }
            CanalEntry.RowChange rowChage = null;
            try {
                rowChage = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
            } catch (Exception e) {
                throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(),
                        e);
            }
            CanalEntry.EventType eventType = rowChage.getEventType();
            System.out.println(String.format("================> binlog[%s:%s] , name[%s,%s] , eventType : %s",
                    entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),
                    entry.getHeader().getSchemaName(), entry.getHeader().getTableName(),
                    eventType));
            for (CanalEntry.RowData rowData : rowChage.getRowDatasList()) {
                if (eventType == CanalEntry.EventType.DELETE) {
                    printColumn(rowData.getBeforeColumnsList());
                } else if (eventType == EventType.INSERT) {
                    printColumn(rowData.getAfterColumnsList());
                } else {
                    System.out.println("-------> before");
                    printColumn(rowData.getBeforeColumnsList());
                    System.out.println("-------> after");
                    printColumn(rowData.getAfterColumnsList());
                }
            }
        }
    }
    private static void printColumn(List<Column> columns) {
        for (Column column : columns) {
            System.out.println(column.getName() + " : " + column.getValue() + "    update=" + column.getUpdated());
        }
    }
}

運行 client 的效果如下圖所示:
在這裏插入圖片描述
在數據庫中變更一條數據,以便產生新的binlog日誌,其輸出結果如下:
在這裏插入圖片描述
能在 IDEA 中搭建並運行 Demo,是我們踏入 canal 的第一步,後續將根據官方文檔中的內容爲提綱,嘗試逐步解開 canal 的實現原理,以便更好的指導實踐。

Canal 系列即將連載中,敬請關注。


好了,本文就介紹到這裏了,您的點贊與轉發是對我持續輸出高質量文章最大的鼓勵。

歡迎加筆者微信號(dingwpmz),加羣探討,筆者優質專欄目錄:
1、源碼分析RocketMQ專欄(40篇+)
2、源碼分析Sentinel專欄(12篇+)
3、源碼分析Dubbo專欄(28篇+)
4、源碼分析Mybatis專欄
5、源碼分析Netty專欄(18篇+)
6、源碼分析JUC專欄
7、源碼分析Elasticjob專欄
8、Elasticsearch專欄(20篇+)
9、源碼分析MyCat專欄

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