## 背景 并行复制可以大大提高备库的 binlog 应用速度,内核月报也多次对并行复制特性进行介绍,感兴趣的朋友可以回顾下:[5.6 并行复制实现分析](http://mysql.taobao.org/monthly/2015/08/09/)、[5.6 并行复制恢复实现](http://mysql.taobao.org/monthly/2015/09/07/) 和 [5.6并行复制事件分发机制](http://mysql.taobao.org/monthly/2015/09/09/)。 在早期的内核月报,有一篇 [并行复制外建约束问题](http://mysql.taobao.org/index.php?title=MySQL%E5%86%85%E6%A0%B8%E6%9C%88%E6%8A%A5_2014.12#MySQL.C2.B7.E3.80.80.E6.80.A7.E8.83.BD.E4.BC.98.E5.8C.96.C2.B7.E5.B9.B6.E8.A1.8C.E5.A4.8D.E5.88.B6.E5.A4.96.E5.BB.BA.E7.BA.A6.E6.9D.9F.E9.97.AE.E9.A2.98),介绍阿里在 5.5 版本中自己实现并行复制时遇到的外键约束问题,本文接着前作继续介绍并行复制外键约束问题,这次场景不一样,并且目前官方 5.6 最新版本(5.6.30)中也有这个问题。 ## 问题描述 一般情况的复制是 A->B 这样一主一备,本文要描述的场景是 A->B->C 这样一主两备,并且备库级联,其中备库 C 开启了并行复制,B 可以串行也可以并行,binlog_fomat 都是 row。 在主库A上执行如下语句: ~~~ CREATE DATABASE db1; CREATE DATABASE db2; USE db1; CREATE TABLE `parent` ( `id` int(11) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB; USE db2; CREATE TABLE `child` ( `id` int(11) DEFAULT NULL, `parent_id` int(11) DEFAULT NULL, KEY `par_ind` (`parent_id`), CONSTRAINT `child_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `db1`.`parent` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB; INSERT INTO db1.parent VALUES(1); INSERT INTO db2.child VALUES(1, 1); ~~~ 备库 C 上会报错如下,非常明显的一个外键约束的错误: ~~~ Last_SQL_Errno: 1452 Last_SQL_Error: Worker 7 failed executing transaction '' at master log mysqld-bin.000001, end_log_pos 1008; Could not execute Write_rows event on table db2.child; Cannot add or update a child row: a foreign key constraint fails (`db2`.`child`, CONSTRAINT `child_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `db1`.`parent` (`id`) ON DELETE CASCADE), Error_code: 1452; handler error HA_ERR_NO_REFERENCED_ROW; the event's master log mysqld-bin.000001, end_log_pos 1008 ~~~ ## 问题分析 如前文[并行复制外建约束问题](http://mysql.taobao.org/index.php?title=MySQL%E5%86%85%E6%A0%B8%E6%9C%88%E6%8A%A5_2014.12#MySQL.C2.B7.E3.80.80.E6.80.A7.E8.83.BD.E4.BC.98.E5.8C.96.C2.B7.E5.B9.B6.E8.A1.8C.E5.A4.8D.E5.88.B6.E5.A4.96.E5.BB.BA.E7.BA.A6.E6.9D.9F.E9.97.AE.E9.A2.98) 所述,5.6 并行复制已经解了外键问题,遇到被外键约束的表,会先切为串行,当前事务执行完成后,再开始并行,为什么还会出问题呢?分析这个问题前,我们先来看下,5.6 是怎么解决外键约束问题的。 5.6 并行复制是基于db进行分发的,不同的db分发到不同的 worker 线程,对 row 格式的 binlog,分发信息是体现在 table_map event 中的。5.6 对 table_map 中加了一个专门的 flag `TM_REFERRED_FK_DB_F`,表示当前表被外键约束(具体参考commit [299ccba1e145c29ed3c242c152ced4cc345328b7](https://github.com/mysql/mysql-server/commit/299ccba1e145c29ed3c242c152ced4cc345328b7)),这样备库分发线程(Coordinator)在遇到有这种标志的 table_map,就切换为串行,具体逻辑参考`Log_event::get_slave_worker()` 和`apply_event_and_update_pos()`。 这个机制是没问题的,如果 flag 能从 A 传到 B 再传到 C,就不会出现这个问题,现在问题的出现是因为备库 B 执行完父表(parent)的更新后,写 binlog 时 flag 没写进去,导致 C 在并行模式下执行 parent 表更新时,没有切换到串行模式,和 child 表的更新同时在跑,如果执行 child 表更新的 worker 先做,那么就会出现外键约束报错。 ## 问题解决 `TM_REFERRED_FK_DB_F` 这个 flag 是在 `Table_map_log_event::Table_map_log_event()` 构造函数中设置的,逻辑如下: ~~~ /* Marking event to require sequential execution in MTS if the query might have updated FK-referenced db. Unlike Query_log_event where this fact is encoded through the accessed db list in the Table_map case m_flags is exploited. */ uchar dbs= thd->get_binlog_accessed_db_names() ? thd->get_binlog_accessed_db_names()->elements : 0; if (dbs == 1) { char *db_name= thd->get_binlog_accessed_db_names()->head(); if (!strcmp(db_name, "")) m_flags |= TM_REFERRED_FK_DB_F; } ~~~ 如果当前访问到的 db 个数为1,并且 db 是空字符串 `""` 的话,就设置这个 flag。`binlog_accessed_db_names` 中只有 `""` 这一个元素是一个特殊构造的场景,正常情况下db不会是 `""`的,构造这样 db 的逻辑在 `THD::decide_logging_format`,如下: ~~~ if (is_write && lex->sql_command != SQLCOM_END /* rows-event applying by slave */) { /* Master side of DML in the STMT format events parallelization. All involving table db:s are stored in a abc-ordered name list. In case the number of databases exceeds MAX_DBS_IN_EVENT_MTS maximum the list gathering breaks since it won't be sent to the slave. */ for (TABLE_LIST *table= tables; table; table= table->next_global) { if (table->placeholder()) continue; DBUG_ASSERT(table->table); if (table->table->file->referenced_by_foreign_key()) { /* FK-referenced dbs can't be gathered currently. The following event will be marked for sequential execution on slave. */ binlog_accessed_db_names= NULL; add_to_binlog_accessed_dbs(""); break; } if (!is_current_stmt_binlog_format_row()) add_to_binlog_accessed_dbs(table->db); } } ~~~ 可以看到,如果有当前表被外键约束的话(`table->table->file->referenced_by_foreign_key()`),会清掉`binlog_accessed_db_names`,只放一个空字符串进去。 但是 SQL 线程在应用 row_event 时,不会走到上面的逻辑,因为 `lex->sql_command` 的值为 `SQLCOM_END`,所以备库 B 生成的 parent 表的 table_map 就不包含这个 flag。 修复也比较简单,把 `lex->sql_command != SQLCOM_END` 这个条件去掉即可,或者参考官方 [bug](http://bugs.mysql.com/bug.php?id=80474) 这里提供的修复方法,也是可以的。