基於Canal實現敏感操作感知

本文主要內容:簡單介紹Canal及其工作原理,並實現了感知數據庫敏感操作。

1. 是什麼?

canal,中文:管道/運河,主要用途是用於MySQL數據庫增量日誌數據的訂閱、消費和解析。

2. 工作原理

  1. canal 模擬MySQL salve的交互協議,把自己僞裝成MySQL slave,向MySQL master發送dump協議。
  2. MySQL master收到dump請求,開始推送binary log給slave(即canal);
  3. canal解析binary log對象(初始數據爲byte流)

在這裏插入圖片描述

在這裏插入圖片描述

3. 搭建環境

3.1 Java開發環境配置

(1)下載JDK11

在這裏插入圖片描述
(2)安裝Java在這裏插入圖片描述
  運行安裝程序,點擊下一步即可,若有需求更改路徑,則在相應位置進行配置即可。

(3)Java環境配置
在這裏插入圖片描述
首先Java的Home目錄設置爲jdk的根目錄。

在這裏插入圖片描述
然後在Path中設置JDK根目錄下的bin文件夾。

(4)測試

在Windows的cmd命令窗口輸入java -version打印出Java的版本信息,即表示安裝成功。
在這裏插入圖片描述

3.2 Mysql環境配置

  爲配置Canal框架,需對Mysql進行必要的配置。查看mysql的log_bin是否開啓,可在mysql中執行:show variables like ‘%log_bin%’進行查看。因爲canal是通過logbin監聽更新的,所以必須開啓。注意:Mysql不能低於5.6。

(1)安裝並登錄Mysql

  具體安裝過程不再贅述,網上資料很多。

在這裏插入圖片描述
(2)配置Mysql

  主要包含mysql根目錄下的my.ini文件以下三個項的配置:

[mysqld]
log-bin=mysql-bin #開啓binlog
binlog-format=ROW #選擇ROW模式
server_id=1 #配置MySQL replaction需要定義,不要和canal的slaveId重複

在這裏插入圖片描述

(3)授權
  授權canal連接MySQL賬號具有作爲MySQL slave的權限,如果已有賬戶可直接grant授權:

// 創建用戶canal,並配置密碼。
create user canal identified by 'canal';

在這裏插入圖片描述

// 給用戶canal授權。
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
# 刷新並應用
FLUSH PRIVILEGES;

在這裏插入圖片描述

// 刷新,然後重啓Mysql
FLUSH PRIVILEGES;

在這裏插入圖片描述

(4)創建數據庫表,以待測試。

CREATE TABLE `users` (
  `id` int(11) NOT NULL,
  `nick` varchar(30) DEFAULT NULL,
  `phone` varchar(20) NOT NULL,
  `password` varchar(64) DEFAULT NULL,
  `email` varchar(50) DEFAULT NULL,
  `account` varchar(15) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

3.2 配置Canal服務端

(1)下載並解壓canal
在這裏插入圖片描述
(2)修改數據庫鏈接配置文件E:\program-software\canal.deployer-1.1.5-SNAPSHOT\conf\example\instance.properties

canal.instance.defaultDatabaseName = test_repo

在這裏插入圖片描述

4 開始實現感知敏感操作

(1)添加pom依賴

<dependency>
    <groupId>com.alibaba.otter</groupId>
     <artifactId>canal.client</artifactId>
     <version>1.1.4</version>
 </dependency>
 <dependency>
     <groupId>org.projectlombok</groupId>
     <artifactId>lombok</artifactId>
     <version>1.18.10</version>
 </dependency>

(2)UserDTO

import lombok.Data;

@Data
public class UserDTO {
    private Integer id;
    private String nick;
    private String phone;
    private String password;
    private String email;
    private String account;
}

(3)主實現類

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 com.google.protobuf.InvalidProtocolBufferException;
import com.sky.canal.dto.UserDTO;

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

/**
 * canal 監聽客戶端
 */
public class CanalClient {
    private static String SERVER_ADDRESS = "127.0.0.1";
    private static Integer PORT = 11111;
    private static String DESTINATION = "example";
    private static String USERNAME = "";
    private static String PASSWORD = "";

    public static void main(String[] args) {
        // newSingleConnector是簡單的ip直連模式,基於CanalServerWithNetty定義的網絡協議接口,對於canal數據進行get/rollback/ack等操作
        CanalConnector canalConnector = CanalConnectors.newSingleConnector(new InetSocketAddress(SERVER_ADDRESS, PORT), DESTINATION, USERNAME, PASSWORD);
        canalConnector.connect();
        //訂閱
        canalConnector.subscribe(".*\\..*");
        //恢復到之前同步的數據,避免誤操作
        canalConnector.rollback();

        while (true) {
            // 獲取指定數量的數據,但是不做確認
            Message message = canalConnector.getWithoutAck(100);
            // 消息ID
            long batchId = message.getId();
            if (batchId != -1) {
                // System.out.println(message.getEntries());
                System.out.println("msgId-->" + batchId);
                printEntity(message.getEntries());
                // canalConnector.ack(batchId); //提交確認
                // canalConnector.rollback(); //處理失敗,回滾數據
            }
        }
    }
    public static void printEntity(List<CanalEntry.Entry> entries){
        for (CanalEntry.Entry entry : entries) {
            if (entry.getEntryType() != CanalEntry.EntryType.ROWDATA){
                continue;
            }
            try{
                // 數據反序列化
                CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
                for (CanalEntry.RowData rowData: rowChange.getRowDatasList()) {
                    switch (rowChange.getEventType()){
                        case INSERT:
                            String tableName = entry.getHeader().getTableName();
                            //測試users表進行映射處理
                            UserDTO userDTO = CanalDataHadler.convertToBean(rowData.getAfterColumnsList(), UserDTO.class);
                            System.out.println("執行了一條插入操作,數據爲:" + userDTO);
                            break;
                        case DELETE:
                            UserDTO deleteUserDTO = CanalDataHadler.convertToBean(rowData.getAfterColumnsList(), UserDTO.class);
                            System.out.println("執行了一條刪除操作,數據爲:" + deleteUserDTO);
                            break;
                        case UPDATE:
                            UserDTO updateDTO = CanalDataHadler.convertToBean(rowData.getAfterColumnsList(), UserDTO.class);
                            System.out.println("執行了一條更新操作,數據爲:" + updateDTO);
                            break;
                        default:
                            break;
                    }
                }
            }catch (InvalidProtocolBufferException e){
                e.printStackTrace();
            }
        }
    }
}

(3)兩個工具類

import com.alibaba.otter.canal.protocol.CanalEntry;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class CanalDataHadler extends TypeConvertHandler{

    // 將binlog的記錄解析爲一個bean對象
    public static <T> T convertToBean(List<CanalEntry.Column> columnList, Class<T> clazz){
        T bean = null;
        try{
            bean = clazz.newInstance();
            Field[] fields = clazz.getDeclaredFields();
            Field.setAccessible(fields, true);
            Map<String, Field> fieldMap = new HashMap<>(fields.length);
            for (Field field : fields) {
                fieldMap.put(field.getName().toLowerCase(), field);
            }
            if (fieldMap.containsKey("serialVersionUID")) {
                fieldMap.remove("serialVersionUID".toLowerCase());
            }
            for (CanalEntry.Column column : columnList) {
                String columnName = column.getName();
                String columnValue = column.getValue();
                if (fieldMap.containsKey(columnName)) {
                    Field field = fieldMap.get(columnName);
                    Class<?> type = field.getType();
                    if (BEAN_FIELD_TYPE.containsKey(type)) {
                        switch (BEAN_FIELD_TYPE.get(type)){
                            case "Integer":
                                field.set(bean, parseToInteger(columnValue));
                                break;
                            case "Long":
                                field.set(bean, parseToLong(columnValue));
                                break;
                            case "Double":
                                field.set(bean, parseToDouble(columnValue));
                            case "String":
                                field.set(bean, columnValue);
                                break;
                            case "java.handle.Date":
                                field.set(bean, parseToDouble(columnValue));
                                break;
                            case "java.sql.Date":
                                field.set(bean, parseToSqlDate(columnValue));
                                break;
                            case "java.sql.Timestamp":
                                field.set(bean, parseToTimestamp(columnValue));
                            case "java.sql.Time":
                                field.set(bean, parseToSqlTime(columnValue));
                                break;
                        }
                    }else{
                        field.set(bean, parseObj(columnValue));
                    }
                }
            }
        }catch (InstantiationException | IllegalAccessException e){
            System.out.println("初始化對象出現異常,對象無法被實例化,異常爲:" + e);
        }
        return bean;
    }

    static Object parseObj(String str){
        if (str == null || str.equals("")) {
            return null;
        }
        return str;
    }
}
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * 類型轉化器
 */
public class TypeConvertHandler {

    static final Map<Class, String> BEAN_FIELD_TYPE;

    static {
        BEAN_FIELD_TYPE = new HashMap<>(8);
        BEAN_FIELD_TYPE.put(Integer.class, "Integer");
        BEAN_FIELD_TYPE.put(Long.class, "Long");
        BEAN_FIELD_TYPE.put(Double.class, "Double");
        BEAN_FIELD_TYPE.put(String.class, "String");
        BEAN_FIELD_TYPE.put(Date.class, "Date");
        BEAN_FIELD_TYPE.put(java.sql.Date.class, "java.sql.Date");
        BEAN_FIELD_TYPE.put(java.sql.Timestamp.class, "java.sql.Timestamp");
        BEAN_FIELD_TYPE.put(java.sql.Time.class, "java.sql.Time");
    }

    static Integer parseToInteger(String source){
        if (isSourceNull(source)) {
            return null;
        }
        return Integer.valueOf(source);
    }

    static Long parseToLong(String source){
        if (isSourceNull(source)) {
            return null;
        }
        return Long.valueOf(source);
    }

    static Double parseToDouble(String source){
        if (isSourceNull(source)) {
            return null;
        }
        return Double.valueOf(source);
    }

    protected static Date parseToDate(String source){
        if (isSourceNull(source)) {
            return null;
        }
        if (source.length() == 10) {
            source = source + "00:00:00";
        }
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date;
        try {
            date = sdf.parse(source);
        }catch (ParseException e){
            return null;
        }
        return date;
    }

    static java.sql.Date parseToSqlDate(String source){
        if (isSourceNull(source)) {
            return null;
        }
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        java.sql.Date sqlDate;
        Date utilDate;
        try {
            utilDate = sdf.parse(source);
        }catch (ParseException e){
            return null;
        }
        sqlDate = new java.sql.Date(utilDate.getTime());
        return sqlDate;
    }

    static java.sql.Timestamp parseToTimestamp(String source){
        if (isSourceNull(source)) {
            return null;
        }
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        java.sql.Timestamp timestamp;
        Date date;
        try {
            date = sdf.parse(source);
        }catch (ParseException e){
            return null;
        }
        timestamp = new java.sql.Timestamp(date.getTime());
        return timestamp;
    }

    static java.sql.Time parseToSqlTime(String source){
        if (isSourceNull(source)) {
            return null;
        }
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        java.sql.Time time;
        Date date;
        try {
            date = sdf.parse(source);
        }catch (ParseException e){
            return null;
        }
        time = new java.sql.Time(date.getTime());
        return time;
    }


    private static boolean isSourceNull(String source){
        return source.equals("");
    }
}

(4)測試
 開啓canal服務
在這裏插入圖片描述

添加一條數據

insert into users (nick, phone) values ('sky', '18800000000');

在這裏插入圖片描述
修改一條數據

update users set phone = 13701012323 where nick = 'sky';

在這裏插入圖片描述
刪除一條數據

delete from users where nick = 'sky';

在這裏插入圖片描述

5. 附錄 - 相關命令

  • 是否啓用了日誌
    • show variables like ‘log_bin’;
  • 怎麼知道當前的日誌
    • show master status;
  • 查看mysql binlog模式
    • show variables like ‘binlog_format’;
  • 獲取binlog文件列表
    • show binary logs;
  • 查看當前正在寫入的binlog文件
    • show master status \G
  • 查看指定binlog文件的內容
    • show binlog events in ‘mysql-bin.000002’;
      查看指定binlog文件的內容
    • show binlog events in ‘mysql-bin.000002’;
  • 注意binlog日誌格式要求爲row格式
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章