基于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格式
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章