主从复制SQL线程中断——字符集砸出的坑

一、问题现场:SQL 线程中断,但表结构一致

告警响起:异步复制的 slave 端 SQL 线程中断了。查询表 `replication_applier_status_by_worker`检查报错信息:

```SQL

GreatSQL > SELECT * FROM performance_schema.replication_applier_status_by_worker\G;

*************************** 1. row ***************************

SERVICE_STATE: OFF

LAST_ERROR_NUMBER: 13146

LAST_ERROR_MESSAGE: Worker 1 failed executing transaction

'f08e63fb-3b91-11f0-af59-5254d9b382e7:66192867' at master log binlog.000423, end_log_pos 115725003;

Column 1 of table 'gsqldb_xxx.dynamic_yyy_log' cannot be converted

from type 'varchar(128(bytes))' to type 'varchar(96(bytes) utf8mb3)'

...

```

报错很直白:事务66192867在回放时,列类型"转换失败"。

核对主从表结构:

```SQL

GreatSQL > SHOW CREATE TABLE gsqldb_xxx.dynamic_yyy_log\G;

CREATE TABLE `dynamic_yyy_log` (

`id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',

`business_id` varchar(32) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '业务主键',

`serial` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '设备唯一编号',

`push_time` datetime DEFAULT NULL COMMENT '上传时间',

`remark` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '备注',

`sort` int DEFAULT '0' COMMENT '排序',

`create_by` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '创建人',

`edit_by` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '最后编辑人',

`time_created` datetime NOT NULL COMMENT '记录创建时间',

`time_modified` datetime NOT NULL COMMENT '记录修改时间',

`status` varchar(32) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '状态',

`operation` varchar(32) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '操作',

`version` int DEFAULT '0' COMMENT '乐观锁',

`deleted` int DEFAULT '0' COMMENT '逻辑删除',

PRIMARY KEY (`id`) USING BTREE,

KEY `business_id` (`business_id`) USING BTREE,

KEY `comp_business_id` (`serial`) USING BTREE,

KEY `sort` (`sort`) USING BTREE

) ENGINE=InnoDB AUTO_INCREMENT=1083580 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC;

```

让人困惑的是:主从两端的表结构完全一致,`business_id `都是 `varchar(32) utf8mb3`。那么,SQL 线程到底在抗议什么?


二、细看报错信息:LAST\_ERROR\_MESSAGE 透露了什么

把报错拆开看,能看出两层关键信息:

  1. **问题出在哪一列**:报错说 `Column 1`。注意这里的列号从 0 开始编号,所以 Column 1 对应的是表里的第二列------`business_id`。

  2. **直接原因是什么**:`cannot be converted from type 'varchar(128(bytes))' to type 'varchar(96(bytes) utf8mb3)'`。也就是 slave 在回放时,发现 binlog 里记录的列长度(128 字节)和本地表定义(96 字节)对不上,拒绝转换。

到这里,矛盾点已经浮现:本地表定义里 `business_id` 是 `varchar(32) utf8mb3`,怎么会转换 `varchar(128(bytes))`呢?


三、大胆假设:做了什么操作

  • slave 端是 `varchar(32) utf8mb3`。utf8mb3 每个字符最多占 3 字节,32 个字符对应 96 字节------正好对应报错里的 `varchar(96(bytes) utf8mb3)`。

  • 但 master 端在 binlog (slave 的 relaylog)中记录的是 `varchar(128(bytes))`。128 不能被 3 整除,却能被 4 整除,商是32,正好和varchar\(32\)对应。如果字符集是utf8mb4,一个字符最多占 4 字节,`varchar(32) utf8mb4`就是`varchar(128(bytes))`。

由此可以合理推测 master 上发生过的操作:

master 端在 `sql_log_bin=off` 的情况下,把 `business_id` 改成了 `utf8mb4` ,执行了insert操作,binlog中记录了`varchar(128(bytes))`;随后\(或许是发现了 slave 的SQL线程中断后\),又在 `sql_log_bin=off` 下把 `business_id` 改回了 `utf8mb3`,使表的结构在主从两端重新"看起来"一致了。


四、实验验证

大胆的假设有了,现在需要小心求证。

4\.1 实验设计

  1. 搭建异步复制环境,在 master 建表 `dynamic_yyy_log` 并插入数据,确认复制正常。

  2. master 在 `sql_log_bin=off` 下把 `business_id` 改成 `utf8mb4`,再插入一条数据,观察 slave 是否中断及报错。

  3. 观察 slave 的 `SHOW SLAVE STATUS` 与 `replication_applier_status_by_worker`。

4\.2 实验步骤

**步骤 1:建表并验证复制**

```SQL

INSERT INTO dynamic_yyy_log(id,business_id,serial,time_created,time_modified)

VALUES(1,'11111','11111',NOW(),NOW());

```

**步骤 2:关键操作------关 binlog 改字符集,插数据****,关binlog改回字符集**

```SQL

-- 将列business_id有utf8mb3改为utf8mb4

SET sql_log_bin=off;

ALTER TABLE dynamic_yyy_log MODIFY COLUMN `business_id`

varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '业务主键';

SET sql_log_bin=on;

-- 在utfmb4下执行insert

INSERT INTO dynamic_yyy_log(id,business_id,serial,time_created,time_modified)

VALUES(2,'22222','22222',NOW(),NOW());

-- 将列business_id有utf8mb4改为utf8mb3

SET sql_log_bin=off;

ALTER TABLE dynamic_yyy_log MODIFY COLUMN `business_id`

varchar(32) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '业务主键';

SET sql_log_bin=on;

```

**步骤 3:观察 slave**

```SQL

GreatSQL > SHOW SLAVE STATUS\G

...

Slave_IO_Running: Yes

Slave_SQL_Running: No

Last_Errno: 13146

Last_Error: Coordinator stopped because there were error(s) in the worker(s).

The most recent failure being: Worker 1 failed executing transaction

'0ecdbd10-21e1-11f1-ae16-00163e892deb:415' at master log binlog.000001, end_log_pos 190926334.

...

```

再看 worker 明细:

```SQL

GreatSQL > SELECT * FROM performance_schema.replication_applier_status_by_worker LIMIT 1\G;

SERVICE_STATE: OFF

LAST_ERROR_NUMBER: 13146

LAST_ERROR_MESSAGE: Worker 1 failed executing transaction

'0ecdbd10-21e1-11f1-ae16-00163e892deb:415' at master log binlog.000001, end_log_pos 190926334;

Column 1 of table 'testdb.dynamic_yyy_log' cannot be converted

from type 'varchar(128(bytes))' to type 'varchar(96(bytes) utf8mb3)'

...

```

报错与现场问题完全一致,此时查询表的结构信息,列business\_id都是utf8mb3。 问题发生的假设得到验证。

五、延伸问题

5\.1 binlog 里记录了列的类型、长度、字符集吗?

`mysqlbinlog -v`解析事务 0ecdbd10\-21e1\-11f1\-ae16\-00163e892deb:415 的 binlog,关键片段如下:

```Plaintext

INSERT INTO `testdb`.`dynamic_yyy_log`

SET

@1=2 /* LONGINT meta=0 */

@2='22222' /* VARSTRING(128) meta=128 nullable=0 */

@3='22222' /* VARSTRING(765) meta=765 nullable=0 */

@9='2026-03-18 09:43:37' /* DATETIME(0) */

...

```

结论很明确:

  • **binlog 记录了列的类型与字节长度**。`@2` 列(即 `business_id`)被记为 `VARSTRING(128)`,128 正是该列在 `utf8mb4` 下的字节数。

  • **binlog 没有记录列的字符集**。binlog中只记录了列"占多少字节",并未记录列的字符集。

5\.2 出了事,怎么救?

既然根因是 slave 本地列"长度"和 binlog 不一致,解法就是让 slave 对齐 master 写 binlog 时的状态------把 `business_id` 也改成 `utf8mb4`:

```SQL

SET sql_log_bin=off;

ALTER TABLE dynamic_yyy_log MODIFY COLUMN `business_id`

varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '业务主键';

SET sql_log_bin=on;

```

改完后 `START SLAVE`这个事务可以继续回放。但主从字符集不一致(主utf8mb3,从utf8mb4),那么,什么时候还需要再改一次字符集?

5\.3 有没有"容忍"类型或长度变化的开关?

GreatSQL 提供了系统变量来控制主从之间的类型转换容忍度`replica_type_conversions`。

  • **默认值(空字符串)**:禁止任何属性提升或降级,主从对应列的长度和类型必须完全一致。本次问题中该变量正是空值,所以一遇到 128 vs 96 就直接报错。

  • **ALL\_LOSSY** 允许会造成信息丢失的转换。

  • **ALL\_NON\_LOSSY** 允许不需要截断、无需特殊处理原始值的转换;即目标类型的取值范围能覆盖源类型。

  • **ALL\_LOSSY,ALL\_NON\_LOSSY** 开启后,所有支持的类型转换都允许,无论是否有损。

  • **ALL\_SIGNED** 属性提升后的整数视为有符号类型(默认行为)。

  • **ALL\_UNSIGNED** 属性提升后的整数视为无符号类型。

  • **ALL\_SIGNED,ALL\_UNSIGNED** 优先将提升后的整数按有符号处理;无法容纳时,则按无符号处理。两个配置的书写顺序无影响。


结语

这次中断的表象是"表结构不一致",真相却是"结构一致、历史 binlog 不一致"。字符集这种看似无害的字段属性,一旦在 `sql_log_bin=off` 下被悄悄改动,DML操作就会在 binlog 里留下历史痕迹,等 slave 回放时就可能跳出来叫停SQL线程。

对于DBA的启示:

  • 但凡涉及到"主从复制类型转换失败"情况的,基本上都可以第一时间往字符集不一致方向去排查。

  • 日常巡检时,对比主从两端数据列的长度、字符集、排序规则是否一致。

  • 不要在 `sql_log_bin=off` 下改列定义,列的字符集/长度变更要走正常 DDL 流程,让变更进入 binlog,保证主从同步演进。

老叶有话说

GreatSQL 社区现已上线"**AI助手**"功能,我试着把这个问题丢给它,互动结果如下。

问:

> GreatSQL主从复制结构中,从服务器上报告错误:Column 1 of table 'zhgd_screen.dynamic_device_status_log' cannot be converted

> from type 'varchar(128(bytes))' to type 'varchar(96(bytes) utf8mb3)' 这种情况下一般是什么原因导致,如何分析排查

AI助手回答:

先说明一下:我检索了官方手册,**未查到针对这条报错文案的专项条目**,下面基于 MySQL/GreatSQL 行复制(ROW format)的通用机制给您分析,具体细节请以官方手册为准。

一、这个报错的本质

这是典型的 **RBR(行复制)列类型不匹配**错误(对应 MySQL 错误码 1677 `ER_SLAVE_CONVERSION_FAILED`):

  • 主库以 ROW 格式记录 binlog,里面带有**主库表的列元数据**;

  • 从库回放 `TABLE_MAP` / 行事件时,会把主库列定义和**本地表的列定义**逐列比对,只有"可安全转换"才放行;

  • 报错说明第 1 列两边定义不一致,无法转换。

报错里的字节数很关键:

  • 主库侧:`varchar(128(bytes))` ------ 列最大容量 128 字节;

  • 从库侧:`varchar(96(bytes) utf8mb3)` ------ 96 字节且字符集是 utf8mb3。

一个很典型的对应关系是:**主库 `VARCHAR(32) utf8mb4`(32×4=128 bytes),从库 `VARCHAR(32) utf8mb3`(32×3=96 bytes)**------字符数相同,但两边建表时的**默认字符集不同**,导致表结构"漂移"了。GreatSQL 8.0 起默认字符集为 utf8mb4,而 utf8 是 utf8mb3,若从库是老版本迁移过来或建库时未显式指定字符集,就很容易出现这种差异 GreatSQL 官方手册 - 字符集 & 校验集↗。

二、常见成因(按概率排序)

  1. **两边默认字符集不一致时各自建表**:主库 `character_set_server=utf8mb4`,从库还是旧的 utf8mb3/latin1,表不是通过复制建的,而是各自初始化或分别导入的。

  2. **单边 DDL 导致结构漂移**:

  • DBA 在从库上直接 `ALTER` 做验证,主库没执行;

  • 主库 `ALTER` 后,从库执行失败被跳过(检查是否配置了 `replica_skip_errors` / `slave_skip_errors` 跳过了 1xxx DDL 错误);

  • 复制过滤规则(`Replicate_Do_DB` / `Replicate_Ignore_DB` 等)把这条 DDL 过滤掉了。

  1. **从库表被外部任务覆盖**:备份恢复、数据导入脚本、初始化任务在从库上重建了这张表,用的是不同 `--default-character-set` 的 dump。

  2. **历史架构调整**:从库曾是别的主库的从库,或做过切换后表结构没对齐。

三、排查步骤

**1. 看复制状态,拿到出错时间线**

```sql

SHOW REPLICA STATUS\G

-- 关注 Replica_SQL_Running、Last_SQL_Error、Last_SQL_Error_Timestamp

-- 旧语法 SHOW SLAVE STATUS 也可用,GreatSQL 新旧两套语法并行兼容

```

GreatSQL 官方手册 - 恢复部分主从复制兼容接口↗

**2. 两边对比表结构(核心步骤)**

分别在主库和从库执行:

```sql

SHOW CREATE TABLE zhgd_screen.dynamic_device_status_log\G

SELECT COLUMN_NAME, ORDINAL_POSITION, COLUMN_TYPE,

CHARACTER_SET_NAME, COLLATION_NAME

FROM information_schema.COLUMNS

WHERE TABLE_SCHEMA='zhgd_screen'

AND TABLE_NAME='dynamic_device_status_log';

```

把两边结果逐列 diff,重点看第 1 列的 `COLUMN_TYPE` 和 `CHARACTER_SET_NAME`。

**3. 检查环境差异**

```sql

SHOW VARIABLES LIKE 'character_set_server';

SHOW VARIABLES LIKE 'collation_server';

SHOW VARIABLES LIKE 'binlog_format'; -- 主库应为 ROW

SHOW REPLICA STATUS; -- 看 Replicate_* 过滤规则

SHOW VARIABLES LIKE '%skip_errors%'; -- 是否配置了跳错

```

**4. 追溯 DDL 历史**:用 `mysqlbinlog` 在主库 binlog 中搜该表名,看这张表的 `ALTER/CREATE` 记录是否都到达了从库;结合 `Last_SQL_Error_Timestamp` 缩小时间窗。

**5. 数据层校验**:结构对齐后,建议用 `pt-table-checksum` 校验主从数据是否还有其他隐性差异。

四、修复建议

  • **以主库为准修正从库结构**(执行前先 `STOP REPLICA`):

```sql

STOP REPLICA;

ALTER TABLE zhgd_screen.dynamic_device_status_log

MODIFY 第1列列名 <与主库完全一致的定义>;

START REPLICA;

```

  • ⚠️ 如果从库该列比主库**窄**且已有存量数据,`MODIFY` 时可能因数据超出长度/字符集表示不了而失败;这种情况下更稳妥的做法是在从库重建表(`CREATE TABLE ... LIKE` 主库表结构)再回填数据。

  • ❌ **不建议**用 `replica_skip_errors=1677` 直接跳过------事件是跳过去了,但主从数据从此不一致,后患更大。只有在明确接受数据差异、紧急恢复复制时才考虑跳过单个事件。

  • **预防**:两边统一 `character_set_server`/`collation_server`;建库建表显式写 `CHARACTER SET utf8mb4`;DDL 统一走变更流程、严禁在从库单边执行;定期用 `pt-table-checksum` 巡检。

如果您把主库和从库两边 `SHOW CREATE TABLE` 的结果贴出来,我可以帮您精确定位是哪一列、哪种差异导致的。也可以到技术论坛发帖讨论:技术论坛(https://greatsql.cn/forum.php)

相关推荐
Wang's Blog1 小时前
Java 项目实战: 交友项目 - 入门案例:服务提供者搭建总结
java·交友
hudd6681 小时前
JavaEE 初阶 - 多线程(进阶)
java·开发语言·java-ee
XLYcmy1 小时前
AI 赋能后中文与英文数据库对比分析报告 上
数据库·ai·llm·prompt·检索·wos·cnki
Pniubi2 小时前
区分Java创建线程池、多线程
java·线程·线程池·多线程·并发
程序员梅雨2 小时前
ZooKeeper面经总结
java·大数据·linux·分布式·后端·zookeeper·中间件
这个DBA有点耶2 小时前
ELT 工具选型完整指南:从 ETL 差异分析到工程落地
数据库·架构·代码规范
IvorySQL2 小时前
PostgreSQL 日报|堆表选择性索引更新(10 月 13 日)
数据库·人工智能·postgresql·开源·区块链
Yyyyyy~2 小时前
【Java】多线程初阶
java
蜗牛互联网2 小时前
Java 17 调用视觉 Responses API:JSON 约束与人工复核路由
java·人工智能·后端