解决Canal 连接数据库超时问题

根本原因:DNS 反向解析导致超时

javascript 复制代码
Caused by: java.net.SocketTimeoutException: Timeout occurred, failed to read total 4 bytes in 5000 milliseconds, actual read only 0 bytes
        at com.alibaba.otter.canal.parse.driver.mysql.socket.BioSocketChannel.read(BioSocketChannel.java:91)
        at com.alibaba.otter.canal.parse.driver.mysql.utils.PacketManager.readHeader(PacketManager.java:19)
        at com.alibaba.otter.canal.parse.driver.mysql.MysqlConnector.negotiate(MysqlConnector.java:162)
        at com.alibaba.otter.canal.parse.driver.mysql.MysqlConnector.connect(MysqlConnector.java:82)

解决方法:跳过域名解析

我是使用docker安装mysql的,找到配置文件对应的挂载目录 修改里面的my.cnf文件,在[mysqld]下面加配置 skip-name-resolve,然后重启主节点

实测:

javascript 复制代码
        <dependency>
            <groupId>com.alibaba.otter</groupId>
            <artifactId>canal.client</artifactId>
            <version>1.1.3</version>
        </dependency>
java 复制代码
package com.ldj.springboot.importbean.canal;

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.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;

import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * User: ldj
 * Date: 2026/4/2
 * Time: 9:06
 * Description: 测试Canal 同步监听Mysql
 */
@Slf4j
public class CanalSyncData {

    private static final String CANAL_IP = "192.168.208.131";
    private static final int CANAL_PORT = 11111;
    private static final String CANAL_NAME = "root";
    private static final String CANAL_PWD = "root";
    private static final String CANAL_FILTER = "demo\\..*";
    private static final String DESTINATION = "example";

    public static void main(String args[]) {
        // 1. 初始连接配置
        InetSocketAddress inetSocketAddress = new InetSocketAddress(CANAL_IP, CANAL_PORT);
        CanalConnector connector = CanalConnectors.newSingleConnector(inetSocketAddress, DESTINATION, CANAL_NAME, CANAL_PWD);

        // 2. 启动阶段:死循环直到连上
        while (true) {
            try {
                getConnection(connector);
                break;
            } catch (Exception e) {
                log.error("启动连接失败,30秒后重试", e);
                try { TimeUnit.SECONDS.sleep(30); } catch (InterruptedException ignored) {}
            }
        }

        // 3.监听阶段
        int emptyCount = 0;
        while (true) {
            try {
                // 获取数据 (带超时)
                Message message = connector.getWithoutAck(100, 1L, TimeUnit.SECONDS);
                if (!CollectionUtils.isEmpty(message.getEntries())) {
                    emptyCount = 0;
                    for (CanalEntry.Entry entry : message.getEntries()) {
                        if (entry.getEntryType() == CanalEntry.EntryType.ROWDATA) {
                            CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
                            Map<String, Object> jsonRoot = buildResponse(entry, rowChange);
                            log.info(getInstance().writeValueAsString(jsonRoot));
                        }
                    }
                    // 确认
                    connector.ack(message.getId());
                } else {
                    emptyCount++;
                    if (emptyCount % 60 == 0) {
                        log.info("监听中... (无数据发生改变)");
                    }
                }

            } catch (Exception e) {
                log.error("Canal服务中断,准备重连", e);
                try {
                    // 4. 重连逻辑:先休眠,再重建对象,再连接
                    TimeUnit.SECONDS.sleep(30);
                    // 关键点:必须 new 一个新的,否则旧连接状态会导致重连失败
                    connector = CanalConnectors.newSingleConnector(inetSocketAddress, "example", "root", "root");
                    try {
                        getConnection(connector);
                    } catch (Exception reConnectEx) {
                        log.error("重连失败,将在下一次循环继续重试", reConnectEx);
                    }

                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }

    private static void getConnection(CanalConnector connector) {
        try {
            log.info("正在尝试连接Canal...");
            // 1. 尝试断开旧连接 (防止状态不一致)
            try {
                connector.disconnect();
            } catch (Exception ignored) {}

            // 2. 建立新连接
            connector.connect();

            // 3. 订阅
            connector.subscribe(CANAL_FILTER);

            // 4. 打印成功日志
            log.info("=== 连接Canal成功 ===");

        } catch (Exception e) {
            // 抛出异常,让外层循环去处理重试
            throw e;
        }
    }

    private static Map<String, Object> buildResponse(CanalEntry.Entry entry, CanalEntry.RowChange rowChange) {
        // 1. 操作基本信息
        Map<String, Object> jsonRoot = new HashMap<>();
        jsonRoot.put("database", entry.getHeader().getSchemaName());
        jsonRoot.put("table", entry.getHeader().getTableName());
        jsonRoot.put("type", rowChange.getEventType().toString());
        jsonRoot.put("time", entry.getHeader().getExecuteTime());

        // 2. 监听到变化的数据
        List<Object> rowsJson = new ArrayList<>();
        for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
            Map<String, Object> rowMap = new HashMap<>();
            // INSERT: 只有新数据
            if (rowChange.getEventType() == CanalEntry.EventType.INSERT) {
                rowMap.put("insert_data", columnToMap(rowData.getAfterColumnsList()));
            }
            // DELETE: 只有旧数据
            else if (rowChange.getEventType() == CanalEntry.EventType.DELETE) {
                rowMap.put("deleted_data", columnToMap(rowData.getBeforeColumnsList()));
            }
            // UPDATE: 包含修改前和修改后
            else if (rowChange.getEventType() == CanalEntry.EventType.UPDATE) {
                rowMap.put("before_update", columnToMap(rowData.getBeforeColumnsList()));
                rowMap.put("after_update", columnToMap(rowData.getAfterColumnsList()));
            }
            rowsJson.add(rowMap);
        }
        jsonRoot.put("data", rowsJson);
        return jsonRoot;
    }

    private static Map<String, Object> columnToMap(List<CanalEntry.Column> columns) {
        Map<String, Object> dataMap = new HashMap<>();
        for (CanalEntry.Column column : columns) {
            dataMap.put(column.getName(), column.getValue());
        }
        return dataMap;
    }

    private static ObjectMapper getInstance() {
        return JacksonHolder.INSTANCE;
    }

    private static class JacksonHolder {
        static final ObjectMapper INSTANCE = new ObjectMapper();
        static {
            // 开启格式化
            INSTANCE.enable(SerializationFeature.INDENT_OUTPUT);
        }
    }
}

在DBeaver修改一行数据

Canal会监听到数据的更新

相关推荐
2401_871492854 分钟前
Python机器学习怎么防止数据泄漏_确保Scaler在Pipeline内拟合
jvm·数据库·python
【心态好不摆烂】5 分钟前
数据库基础
数据库
Bert.Cai7 分钟前
MySQL UPPER()函数详解
数据库·mysql
2301_8180084411 分钟前
MySQL怎样在触发器中引用新旧数据行_NEW与OLD关键字详解
jvm·数据库·python
langsiming15 分钟前
【无标题】
java·开发语言·数据库
Boop_wu31 分钟前
[Java EE 进阶]Mybatis进阶(动态SQL)
java·数据库·maven·mybatis
Elastic 中国社区官方博客35 分钟前
使用 EDOT Browser 和 Kibana 进行 OpenTelemetry 浏览器端埋点
大数据·服务器·数据库·elasticsearch·搜索引擎·单元测试·可用性测试
星轨zb1 小时前
为什么Mysql需要索引以及如何应用到项目中
数据库·mysql
Old Uncle Tom1 小时前
提示词编写规范
数据库·算法
l1t1 小时前
DeepSeek总结的Postgres 扩展天花板:当一个实例试图包揽一切时
数据库·postgresql