解决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会监听到数据的更新

相关推荐
于樱花森上飞舞7 小时前
【Redis】Redis的数据结构
数据结构·数据库·redis
城数派8 小时前
谷歌18亿建筑足迹数据集 Google Open Buildings V3
数据库·arcgis·信息可视化·数据分析·excel
sunwenjian8868 小时前
MySQL加减间隔时间函数DATE_ADD和DATE_SUB的详解
android·数据库·mysql
Dxy12393102168 小时前
Python如何使用正则判断是否是姓名
数据库·python·mysql
1688red8 小时前
MySQL Redo Log 和 Undo Log 迁移实践文档
数据库·mysql
天若有情6738 小时前
Python精神折磨系列(完整11集·无断层版)
数据库·python·算法
ictI CABL8 小时前
MySQL Workbench菜单汉化为中文
android·数据库·mysql
贺小涛8 小时前
VictoriaMetrics深度解析
java·网络·数据库
lingggggaaaa8 小时前
PHP原生开发篇&SQL注入&数据库监控&正则搜索&文件定位&静态分析
数据库·sql·安全·web安全·php