SpringCloudAlibaba - 數據同步中間件阿里Canal

簡介

官方文檔:https://github.com/alibaba/canal

canal ,主要用途是基於 MySQL 數據庫增量日誌解析,提供增量數據訂閱和消費。

早期阿里巴巴因爲杭州和美國雙機房部署,存在跨機房同步的業務需求,實現方式主要是基於業務 trigger 獲取增量變更。從 2010 年開始,業務逐步嘗試數據庫日誌解析獲取增量變更進行同步,由此衍生出了大量的數據庫增量訂閱和消費業務。

基於日誌增量訂閱和消費的業務包括

  • 數據庫鏡像
  • 數據庫實時備份
  • 索引構建和實時維護(拆分異構索引、倒排索引等)
  • 業務 cache 刷新
  • 帶業務邏輯的增量數據處理

當前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x。

工作原理

MySQL主備複製原理

 

  • MySQL master 將數據變更寫入二進制日誌( binary log, 其中記錄叫做二進制日誌事件binary log events,可以通過 show binlog events 進行查看)
  • MySQL slave 將 master 的 binary log events 拷貝到它的中繼日誌(relay log)
  • MySQL slave 重放 relay log 中事件,將數據變更反映它自己的數據

canal 工作原理

  • canal 模擬 MySQL slave 的交互協議,僞裝自己爲 MySQL slave ,向 MySQL master 發送dump 協議
  • MySQL master 收到 dump 請求,開始推送 binary log 給 slave (即 canal )
  • canal 解析 binary log 對象(原始爲 byte 流)

本文講解MySQL同步Redis,分爲兩種方式:CanalClient,MQ形式。

一. CanalClient方式

1. MySQL配置

配置MySQL的  my.ini/my.cnf  開啓允許基於binlog文件主從同步

log-bin=mysql-bin
binlog-format=ROW
server_id=108

配置該文件後,重啓mysql服務器即可

手動創建cannl賬號或者直接使用root賬號

drop user 'canal'@'%';
CREATE USER 'canal'@'%' IDENTIFIED BY 'canal';
grant all privileges on *.* to 'canal'@'%' identified by 'canal'; 
flush privileges;

創建完後,在mysql庫user表裏檢查配置都爲yes即代表創建並授權成功。

2. CanalServer端配置

修改\conf\example下的instance.properties 配置文件內容

canal.instance.master.address=192.168.0.108:3306
canal.instance.dbUsername=root
canal.instance.dbPassword=root

啓動\bin\startup.bat,查看 \logs\example example.log日誌文件出現 start successful....則代表啓動成功。

3. CanalClient

  核心Jar包:

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

    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.9.0</version>
    </dependency>
</dependencies>

  RedisUtil:

import redis.clients.jedis.Jedis;

public class RedisUtil {

    private static Jedis jedis = null;

    public static synchronized Jedis getJedis() {
        if (jedis == null) {
            jedis = new Jedis("127.0.0.1", 6379);
        }
        return jedis;
    }
    public static boolean existKey(String key) {
        return getJedis().exists(key);
    }
    public static void delKey(String key) {
        getJedis().del(key);
    }
    public static String stringGet(String key) {
        return getJedis().get(key);
    }
    public static String stringSet(String key, String value) {
        return getJedis().set(key, value);
    }
    public static void hashSet(String key, String field, String value) {
        getJedis().hset(key, field, value);
    }
}

  CanalClient:

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.*;
import com.alibaba.otter.canal.protocol.Message;
import java.net.InetSocketAddress;
import java.util.List;

public class CanalClient {

    public static void main(String args[]) {
        // 連接我們的CanalServer端
        CanalConnector connector = CanalConnectors.newSingleConnector(new
                InetSocketAddress("127.0.0.1",
                11111), "example", "", "");
        int batchSize = 100;
        try {
            connector.connect();
            connector.subscribe("cacal.user_table"); //同步cacal庫下的user_table
            connector.rollback();
            while (true) {
                // 獲取指定數量的數據
                Message message = connector.getWithoutAck(batchSize);
                long batchId = message.getId();
                int size = message.getEntries().size();

                if (batchId == -1 || size == 0) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    printEntry(message.getEntries());
                }
                // 提交確認
                connector.ack(batchId);
                // connector.rollback(batchId); // 處理失敗, 回滾數據
            }
        } finally {
            connector.disconnect();
        }
    }

    private static void printEntry(List<Entry> entrys) {
        for (Entry entry : entrys) {
            if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {
                continue;
            }
            RowChange rowChage = null;
            try {
                rowChage = RowChange.parseFrom(entry.getStoreValue());
            } catch (Exception e) {
                throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(),
                        e);
            }
            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 (RowData rowData : rowChage.getRowDatasList()) {
                if (eventType == EventType.DELETE) {
                    redisDelete(rowData.getBeforeColumnsList());
                } else if (eventType == EventType.INSERT) {
                    redisInsert(rowData.getAfterColumnsList());
                } else {
                    System.out.println("-------> before");
                    printColumn(rowData.getBeforeColumnsList());
                    System.out.println("-------> after");
                    redisUpdate(rowData.getAfterColumnsList());
                }
            }
        }
    }

    private static void printColumn(List<Column> columns) {
        for (Column column : columns) {
            System.out.println(column.getName() + " : " + column.getValue() + "    update=" + column.getUpdated());
        }
    }

    private static void redisInsert(List<Column> columns) {
        JSONObject json = new JSONObject();
        for (Column column : columns) {
            json.put(column.getName(), column.getValue());
        }
        if (columns.size() > 0) {
            RedisUtil.stringSet("canal:user:" + columns.get(0).getValue(), json.toJSONString());
        }
    }

    private static void redisUpdate(List<Column> columns) {
        JSONObject json = new JSONObject();
        for (Column column : columns) {
            json.put(column.getName(), column.getValue());
        }
        if (columns.size() > 0) {
            RedisUtil.stringSet("canal:user:" + columns.get(0).getValue(), json.toJSONString());
        }
    }

    private static void redisDelete(List<Column> columns) {
        JSONObject json = new JSONObject();
        for (Column column : columns) {
            json.put(column.getName(), column.getValue());
        }
        if (columns.size() > 0) {
            RedisUtil.delKey("canal:user:" + columns.get(0).getValue());
        }
    }
}

新建cacal庫,user_table,無論新增,更改,刪除,都會同步到Redis。

    

二. MQ方式

Canal支持兩種MQ:Kafka和RocketMQ,本文講解Kafka。

1. Kafka環境安裝

啓動zookeeper,並運行ZooInspector,具體安裝前面博客有講解:

解壓 kafka_2.13-2.4.0 改名爲 kafka

修改 server.properties中的配置   

log.dirs=D:\MyTools\Kafka\logs
Cmd  進入到該目錄:D:\MyTools\Kafka
.\bin\windows\kafka-server-start.bat .\config\server.properties

2. Canal配置更改

1.修改 example/instance.properties 
canal.mq.topic=zb-topic
2.修改 canal.properties
# tcp, kafka, RocketMQ
canal.serverMode = kafka
canal.mq.servers = 127.0.0.1:9092

3. 編寫消費者代碼

@RestController
public class KafkaController {
   
    @Autowired
    private RedisUtils redisUtils;

    // 消費者使用日誌打印消息
    @KafkaListener(topics = "zb-topic")
    public void receive(ConsumerRecord<?, ?> consumer) {
        System.out.println("topic名稱:" + consumer.topic() + ",key:" +
                consumer.key() + "," +
                "分區位置:" + consumer.partition()
                + ", 下標" + consumer.offset() + "," + consumer.value());
        String json = (String) consumer.value();
        JSONObject jsonObject = JSONObject.parseObject(json);
        String sqlType = jsonObject.getString("type");
        JSONArray data = jsonObject.getJSONArray("data");
        JSONObject userObject = data.getJSONObject(0);
        String id = userObject.getString("id");
        String database = jsonObject.getString("database");
        String table = jsonObject.getString("table");
        String key = database + "_" + table + "_" + id;
        if ("UPDATE".equals(sqlType) || "INSERT".equals(sqlType)) {
            redisUtils.setString(key, userObject.toJSONString());
            return;
        }
        if ("DELETE".equals(sqlType)) {
            redisUtils.deleteKey(key);
        }
    }
}

 

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