Toggle navigation
首页
关于TCPGAME
快乐分享
联系我们
anal Adapter 1.1.8 踩坑实录:频繁报 Duplicate key name 重复索引导致同步线程死锁的真相
Lonner
时间:2026-10-22 11:36:58
阅读:5
## 一、 问题现象 在使用 `canal.adapter-1.1.8` 进行 MySQL 跨公网数据同步时,遇到一个极其诡异的问题: Canal Adapter 突然疯狂报错,导致同步工作线程(worker thread)直接死亡(dead),停止同步。 核心报错日志如下: ```text ERROR c.a.o.canal.adapter.launcher.loader.AdapterProcessor - java.lang.RuntimeException: java.sql.SQLSyntaxErrorException: Duplicate key name 'idx_entity_id' Caused by: java.sql.SQLSyntaxErrorException: Duplicate key name 'idx_entity_id' at com.alibaba.otter.canal.client.adapter.rdb.service.RdbMirrorDbSyncService.executeDdl(RdbMirrorDbSyncService.java:167) ... 2026-10-22 10:59:53.457 [Thread-4] ERROR c.a.otter.canal.adapter.launcher.loader.AdapterProcessor - Outer adapter sync failed! Error sync and rollback, execute times: 50 INFO c.a.otter.canal.adapter.launcher.loader.AdapterProcessor - destination example adapters worker thread dead! ``` **疑点:** 1. 源库的 DDL 是通过一段带有 `IF` 判断的幂等脚本执行的,确认该索引在源库只有一个。 2. 我甚至手动去目标库删除了该索引,重启 Adapter 后,依然报这个错误,仿佛陷入了一个死循环。 --- ## 二、 排查历程与误区 ### 误区 1:以为源库 Binlog 产生了多条相同的 DDL 由于业务代码使用了 `PREPARE stmt FROM @sql; EXECUTE stmt;` 这样的动态 SQL 来执行建索引操作。 **真相:** MySQL Binlog 中记录的是最终执行的 DDL 语句(如 `ALTER TABLE ... ADD INDEX`),Canal 只能看到这条裸 DDL。如果源库只执行成功一次,Binlog 中也绝对只有一条。 ### 误区 2:以为 Canal Adapter 会无限重复创建 一开始看到日志里疯狂打印建索引,以为是 Adapter 抽风重复执行。 **真相:** 这是由于 `retries: -1` (无限重试) 配置导致的。当 Canal Adapter 处理一个 Batch 数据发生异常时,它没有向 Canal Server 确认成功,导致 Server 重新投递该 Batch 数据,Adapter 再次尝试执行,从而形成无限重试的“假象”。 ### 误区 3:以为把目标库索引删了就能解决,或者以为报错回滚(Rollback)会撤销索引 目标库删了索引后重启,依然报 `Duplicate key name`。而且明明日志里写了 `Error sync and rollback`,为什么刚才建好的索引没有回滚掉? **真相:** 这是因为在 MySQL 的机制中,**DDL(数据定义语言)是隐式提交的,根本不支持事务回滚。** 只要一个 Batch 中有一条数据失败,整个 Batch 都会重试。重试时,之前已经执行成功的 DDL(建索引)已经永久生效了,再次执行必然导致报重复索引。 --- ## 三、 真相大白:公网环境下的“完美风暴” 当我们再次仔细查阅错误日志时,发现了真正的元凶: ```text 2026-10-22 10:59:40.324 DDL: {"sql":"ALTER TABLE t_entity_relation ADD INDEX `idx_entity_id` ..."} 2026-10-22 10:59:43.393 ERROR com.alibaba.druid.filter.stat.StatFilter - slow sql 3053 millis. ALTER TABLE t_entity_relation ADD INDEX `idx_entity_id`... 2026-10-22 10:59:43.394 DDL: {"sql":"ALTER TABLE t_function_content ADD INDEX `idx_function_id` ..."} 2026-10-22 10:59:53.446 ERROR com.alibaba.druid.pool.DruidPooledStatement - CommunicationsException... java.net.SocketTimeoutException: Read timed out ``` ### 1. 案发现场复盘: 1. **源库执行了包含 4 个 `ADD INDEX` 的脚本**。Canal 将这 4 条 DDL 打包为一个 Batch 发送给 Adapter。 2. **第 1 条 DDL (`t_entity_relation`)** 在目标库开始执行,耗时 3 秒,**成功建立索引**(注意:DDL 是隐式提交的,此时索引已永久生效)。 3. **第 2 条 DDL (`t_function_content`)** 开始执行。由于目标库是**公网环境**(`xxx.xxx.xxx.xxx`),网络抖动或大表建索引耗时过长,触发了 MySQL 驱动的默认网络心跳超时时间(10 秒)。 4. 抛出 `SocketTimeoutException: Read timed out`,数据库连接断开。 5. **触发灾难级重试**:Canal 认为整个 Batch 失败,于是发起重试。 6. **死神降临**:重试重新执行第 1 条 DDL。但此时目标库的索引已经在第 2 步建好了,于是直接抛出致命的 `Duplicate key name` 报错。由于此前配置了无限重试,直接导致死循环,线程彻底死亡。 ### 2. 核心机制剖析:为什么 Rollback 救不了场? 很多人会疑惑:既然 Canal 报错了并执行了 `Rollback`,为什么不能把刚才建好的索引撤销掉? * **DML(增删改)是支持回滚的**:如果在事务中插入了 10 条数据,遇到错误 Rollback 后,这 10 条数据就像没发生过一样。 * **DDL(建表、建索引)是不支持回滚的**:MySQL 在执行 `ALTER TABLE` 等 DDL 语句时,会触发**隐式提交(Implicit Commit)**。这意味着,当 DDL 语句执行完毕的那一瞬间,结构变更就已经物理写入数据库了,它就像泼出去的水,任何 JDBC 的 `rollback()` 都无法将其撤销。 **核心坑点总结:** * **Canal 的 Batch 重试机制** 与 **MySQL DDL 的不可回滚性** 产生了致命冲突。 * **公网环境执行大表 DDL 极易超时**,超时引发断连重试,重试必然撞上已经生效且无法回滚的索引,引发重复建索引报错。 --- ## 四、 解决方案 针对此类问题,有以下几种解决和优化方案: ### 方案一:调整连接超时时间(治标) 因为跨公网执行 DDL 极易超时,需要在 Adapter 的 `application.yml` 中增加 JDBC 的连接超时参数,给执行慢 SQL 留足时间: ```yaml jdbc.url: jdbc:mysql://xxx.xxx.xxx.xxx:3306/db?rewriteBatchedStatements=true&...&socketTimeout=60000&connectTimeout=60000 ``` ### 方案二:调整重试次数 千万不要使用 `retries: -1` 进行无限重试,这会导致一旦出错就彻底卡死。建议在 `application.yml` 中改为有限次数(如 3 次): ```yaml canal.conf: retries: 3 ``` ### 方案三:彻底解耦,过滤 DDL(治本,强烈推荐) 既然在跨公网同步中,让 Canal 去自动同步 DDL 非常脆弱(无法保证幂等、极易超时且不支持回滚),**最安全的生产实践是由 DBA 或开发者手动在目标库执行 DDL**。 让 Canal 只负责纯数据(DML)的同步。在 Canal Server (Deployer) 的 `conf/example/instance.properties` 配置文件中添加: ```properties # 过滤掉所有的 DDL 语句,不向下游发送 canal.instance.filter.query.ddl = true ``` **注意**:开启此配置后,源库的表结构变更(增删改字段、索引)将不再自动同步到目标库,必须人工介入同步。 ### 方案四:清理目标库脏数据 如果死循环已经发生,必须在目标库手动清理因为重试而建好的半截索引,再配合方案二或方案三重启: ```sql DROP INDEX idx_entity_id ON t_entity_relation; -- 其他报错的表也一起清理 ``` --- ## 五、 总结反思 1. **DDL 永远是“裸奔”的**:不要指望 Canal 能理解源库脚本里的 `IF NOT EXISTS`,Binlog 里只有冰冷的 `ALTER TABLE`。 2. **警惕公网 + 大表 DDL**:在网络不稳定的公网链路上执行耗时不可控的 DDL,是极不理智的架构设计。 3. **敬畏 MySQL 的隐式提交机制**:这是本次踩坑最深切的教训。由于 DDL 不支持回滚,Canal 的 Batch 重试机制会无限放大这个副作用。**将 DDL 与 DML 同步解耦,把结构变更交由人工或独立流程把控,才是保障数据同步高可用的终极方案。**
上一章:没有了
下一章:没有了