技術乾貨 | 資料中介軟體如何與GreatSQL資料同步?

萬里資料庫發表於2022-07-08
    1.引入
    2.傳統方案介紹
    3.監控binlog實現"同步"更新
    4.總結

1. 引入

先前介紹了ElasticSearch,以及ES配合MySQL的問題,這種方案是讓ES上的資料根據MySQL的資料做對照從而形成對應的索引,再將資料通過處理和封裝存放在ES當中。( 可回顧: 技術分析 | 淺析MySQL與ElasticSearch的組合使用

回到生產環境,比如,我們如何保證MySQL系開源資料庫GreatSQL中與ES對照的資料,發生更新的時候ES也進行更新呢?下面以ES為例進行分析。


2.傳統方案介紹


2.1 直接的"同步"更新

第一種方式十分直接,當發生對GreatSQL資料的更新操作時,由伺服器對GreatSQL和ES同時進行更新操作,如圖:


這種方式實現起來十分“簡單粗暴”,容易理解,顯然可以解決問題,但絕不是最優解,原因如下:

  • 首先,這種方法使得我們進行資料庫的資料寫入、修改、刪除等操作,後面都要跟上ES的同步操作,程式碼書寫也過於冗長,且大大加大了業務的耦合度;
  • 其次,這種方法不能很好解決“同步”的問題,如果在執行對應操作的時候發生了斷電等情況,就可能導致資料不同步的問題;
  • 最後,為了保證兩者的更新要麼同時完成要麼都不完成,需要開啟事務來處理,系統的效能有所降低。並且,在高併發情況下,有可能造成服務的“雪崩”。

2.2 非同步的"同步"更新

針對前面的方案,可以考慮加入訊息佇列的中介軟體來優化,與第一種方法不同的是,當發生對GreatSQL資料的更新操作時,伺服器會完成GreatSQL資料的更新,並通過MQ的佇列設定好的交換機傳送更新ES的訊息,給對應的接收更新訊息的佇列,進而完成對應ES資料更新的實現。如圖:


這種方案將直接的更新方式轉換為非同步的更新方式,效能顯然提高了,同時降低了業務耦合度,也優化了資料“同步”的問題。但是,這種方案會出現MQ的消費者在消費時可能因為網路等原因導致使用者資料有延時。同時,從編碼角度看,每次系統要進行同步時都要編寫MQ程式碼,仍然存在業務的耦合,且系統架構的設計也因為加入新的中介軟體要重新考慮維護的問題。


3.監控binlog實現"同步"更新

上面兩種方案中都存在硬編碼問題,同時存在強的業務耦合,以至於實現GreatSQL資料更新後的資料同步問題的代價要麼是植入ES更新程式碼,要麼替換為MQ程式碼,程式碼的侵入性太強,且效能降低。 因此可以通過監控GreatSQL的binlog來實現資料的同步。


3.1 問題分析

binlog,該日誌存在於Server層次中,是使用儲存引擎都可以使用的日誌模組,binlog是邏輯日誌,記錄的是這個語句的原始邏輯,比如“給test表id=5這一行的col1欄位值加1”。binlog的日誌檔案是可以追加寫入的。“追加寫入”是指binlog日誌檔案寫到一定大小後會切換到下一個檔案進行寫入,可以設定sync_binlog為1,讓每次事務的binlog都持久化儲存到磁碟中。binlog在ROW模式下會記錄每次操作後每行記錄的變化。雖然此模式下所佔用的空間較大,但此模式可以保持資料的一致性。因此不管SQL是什麼,引用了什麼函式,他記錄的是執行後的效果。


3.2 使用Canal來監控binlog

Canal是阿里用Java開發的基於資料庫增量的日誌解析,是提供增量資料訂閱&消費的中介軟體。目前,Canal主要支援了  MySQL  的  binlog  解析,解析後可利用  Canal Client  來處理獲得的相關資料。

詳細可參考:


Canal的實現原理基於MySQL主從複製進行設計:

  • Master主庫將改變記錄到邏輯日誌(binary log)中(這些記錄叫做邏輯日誌事件,binary log events,可以通過 show binlog events 進行檢視);
  • Slave從庫將Master主庫的binary log events拷貝到它的中繼日誌(relay log);
  • Slave從庫讀取從重做中繼日誌中的事件,將改變反映它自己的資料同步到資料庫 中。

(源自canal官方文件)

而Canal就是將自身偽裝成一個Slave從庫,假裝從Master主庫複製資料:

  • Canal模擬MySQL Slave的互動協議,偽裝自己為MySQL Slave,向MySQL Master傳送dump協議;
  • MySQL Master收到dump請求,開始推送binary log給Slave(也就是Canal);
    Canal解析binary log物件(原始為byte流)。

(源自 canal官方文件

這種方案的好處是程式中沒有程式碼侵入、沒有硬編碼。同時,原有系統不需要任何變化對原方案的高耦合進行了業務解耦,不需要關注原來系統的業務邏輯。



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

    [mysqld]# 開啟 binloglog-bin=mysql-bin # 選擇 ROW 模式binlog-format=ROW # 指定開啟binlog的資料庫,不指定則全部資料庫開啟binlog-do-db=databasename# 配置 MySQL replaction 需要定義,不要和 canal 的 slaveId 重複server_id=1

    建立canal賬戶,授權 canal 連結 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;

      下載並啟動Canal

        
        
        wget 
        
        
        
        
        mkdir /tmp/canal tar zxvf canal.deployer-1.1.2.tar.gz  -C /tmp/canal

        修改Canal的配置檔案

          
          vi conf/example/instance.properties
          
          
          
          # # mysql serverId canal.instance.mysql.slaveId = 1234 #position info,需要改成自己的資料庫資訊 canal.instance.master.address = 127.0.0.1:3306 canal.instance.master.journal.name = canal.instance.master.position = canal.instance.master.timestamp = #canal.instance.standby.address = #canal.instance.standby.journal.name = #canal.instance.standby.position = #canal.instance.standby.timestamp = #username/password,需要改成自己的資料庫資訊 canal.instance.dbUsername = canal   canal.instance.dbPassword = canal canal.instance.defaultDatabaseName = canal.instance.connectionCharset = UTF-8 #table regex canal.instance.filter.regex = .\*\\\\..\*

          Canal操作

            # 啟動sh bin/startup.sh# 檢視server日誌vi logs/canal/canal.log</pre># 檢視 instance 的日誌vi logs/example/example.log# 關閉sh bin/stop.sh

            以Java為例,建立測試專案Maven工程,匯入應用開發場景:

              <dependencies>        <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>2.4.1</version>        </dependency>    </dependencies>

              編寫日誌監視類CanalClient來從日誌中抓取資訊,首先,獲取canal的連線物件並連線:

                //獲取 canal 連線物件CanalConnector canalConnector = CanalConnectors.newSingleConnector(new InetSocketAddress("127.0.0.1", 11111),"example", "canal","canal");//連線canalConnector.connect();

                指定需要監控的資料庫,並根據資料量來獲取 Message :


                  //指定要監控的資料庫canalConnector.subscribe("databasename.*");//獲取 MessageMessage message = canalConnector.get(100);

                  接著就可以通過處理 Message 來得到監控資訊內容了:

                    List<CanalEntry.Entry>entries = message.getEntries();    if (entries.size() > 0) {    for (CanalEntry.Entry entry : entries) {        //獲取表名        String tableName = entry.getHeader().getTableName();        //Entry 型別        CanalEntry.EntryType entryType = entry.getEntryType();        //判斷 entryType 是否為 ROWDATA        if (CanalEntry.EntryType.ROWDATA.equals(entryType)) {            //序列化資料            ByteString storeValue = entry.getStoreValue();            //反序列化            CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(storeValue);            //獲取事件型別            CanalEntry.EventType eventType = rowChange.getEventType();            //獲取具體的資料            List<CanalEntry.RowData> rowDatasList = rowChange.getRowDatasList();            //遍歷並列印資料            for (CanalEntry.RowData rowData : rowDatasList) {                List<CanalEntry.Column> beforeColumnsList = rowData.getBeforeColumnsList();                JSONObject beforeData = new JSONObject();                for (CanalEntry.Column column : beforeColumnsList) {                    beforeData.put(column.getName(), column.getValue());                }                JSONObject afterData = new JSONObject();                List<CanalEntry.Column> afterColumnsList = rowData.getAfterColumnsList();                for (CanalEntry.Column column : afterColumnsList) {                    afterData.put(column.getName(), column.getValue());                }                System.out.println("TableName:" + tableName                        +                        ",EventType:" + eventType +                        ",Before:" + beforeData +                        ",After:" + afterData);            }        }    }}

                    從程式碼中可以看出,當系統與Canal建立連線後可以獲取Message來監控資料庫的操作,Message是一次Canal從MySQL的 bin log 中抓取的資訊,一個Message中可以有多個SQL執行的結果,每個SQL執行結果(SQL命令)稱為Entry,如圖:



                    Entry中包含 TableName 、EntryType和StoreValue,其中StoreValue 包含了資料變化的內容。如下:

                    要想進行使用還需要進行反序列化操作才可以進行使用,如下:



                    當然,實際生產環境Canal可以配置MQ模式,配合RocketMQ或者Kafka,canal會把資料傳送到MQ的topic中,然後通過訊息佇列的消費者進行處理。首先需要修改canal.properties檔案,這個檔案是 canal 的基本通用配置,canal 埠號預設就是 11111,修改 canal 的輸出model,預設 tcp,改為輸出到 kafka,instance.properties檔案輸出得到主 為kafka,可配置叢集,再次啟動canal就可以啟動 Kafka 消費客戶端測試,檢視消費情況了。


                    4. 總結

                    本文介紹了三種方式使得中介軟體的資料與GreatSQL的資料儲存同步,前兩種方法在使用效能和設計上都存在較大漏洞,而第三種通過讀取 GreatSQL的bin log日誌,獲取指定表的日誌資訊來實現資料同步的方法,在編碼上看沒有程式碼侵入,業務耦合度低,且原有系統不需要任何變化。但構建bin log監控系統需要做好規劃,不過多贅述了。


                    EnjoyGreatSQL :)




                    來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69997641/viewspace-2904907/,如需轉載,請註明出處,否則將追究法律責任。

                    相關文章