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

相关推荐
zlwool6 小时前
图纸管理选型避坑指南
数据库·erp·设备erp·非标机械
小灰灰搞电子8 小时前
完全驾驭 Qt 与数据库:C++ ORM 框架 QxOrm 原理与实践指南
数据库·c++·qt
软件聚导航9 小时前
「聚小软 AI 助手」技术升级:从数据库检索到 RAG 知识库问答
前端·数据库·mysql·微信小程序·小程序·ai编程·rag
学习星球10 小时前
单调栈——从“找下一个更大的“到柱状图中的最大矩形
数据库·c++·算法·leetcode·xcode
GoppViper11 小时前
RDF资源描述框架深度解析:语义Web的数据基石与实战逻辑
前端·数据库
2601_9621741711 小时前
Redis 简介
数据库·redis·wpf
夜雪一千11 小时前
MySQL 各种数据同步方案全梳理:从简单复制到异构同步实战
数据库·mysql
Highcharts.js12 小时前
Highcharts 下钻(Drilldown)柱状图 Demo
数据库·信息可视化·hbase·highcharts·drilldown·js模块·下钻图表
翼龙云_cloud12 小时前
阿里云国际渠道代理商:如何在ECS上部署Flask应用?
运维·数据库·阿里云·flask·云计算
进阶的小木桩13 小时前
mysql 安装问题解决方案记录
数据库·mysql