## bug描述 设置 foreign_key_checks=0 删除被引用的索引后,再设置foreign_key_checks=1,对引用表进行DML操作会导致 mysqld crash,以下是重现的测例: ~~~ drop table if exists t2; drop table if exists t1; create table t1 (a int, b int, key idx1(a)) engine=innodb; insert into t1 values(1,1); insert into t1 values(2,2); create table t2 (a int, b int, foreign key (b) references t1(a)) engine=innodb; set session foreign_key_checks = 0; alter table t1 drop key idx1; set session foreign_key_checks = 1; insert into t2 values (1,1); //此语句执行时mysqld会crash ~~~ ## 分析 对于引用约束,在mysql实现中引用表和被引用表都会记录表的引用关系。 以下链表记录该表引用了哪些表,链表中每个元素为 dict_foreign_t ~~~ table->foreign_list ~~~ 以下链表记录该表被哪些表引用,链表中每个元素为 dict_foreign_t ~~~ table->referenced_list ~~~ dict_foreign_t 结构如下 ~~~ dict_foreign_t { foreign_table // t2 foreign_index referenced_table // t1 referenced_index // idx1 ...... } ~~~ 对于上面的测例,t1为被引用表,t2为引用表。 * 对于t1 table->foreign_list 为null table->referenced_list 则记录了引用关系 * 对于t2 table->foreign_list 记录了引用关系 table->referenced_list 则为null 对于删除索引操作,如果索引涉及到引用关系,那么对应引用关系中的索引也应该做相应调整。对于测例中的删索引操作 `alter table t1 drop key idx1;` 应该做如下调整 * 修改t1表的引用关系 table->referenced_list中dict_foreign_t->referenced_index 置为null * 修改t2表的引用关系 table->foreign_list中dict_foreign_t->referenced_index 置为null 而此bug修复之前, 并没有修改t2表的引用关系, 从而导致后面对t2表进行DML操作时,如果访问了无效的dict_foreign_t->referenced_index就会导致crash。 ## 修复方法 删除被引用的索引后,应修改引用表的引用关系,即应修改table->referenced_list。 详见[官方修复](https://github.com/mysql/mysql-server/commit/dde1b32d9e292255d09dbbe15145b346fbc208f6 "FOREIGN KEY REFERENCES FREED MEMORY AFTER DROP INDEX") ## 附加说明 如果删除引用表对应的外键时,mysql如何处理的呢?同删除被引用表的索引一样,都需要调整引用表和被引用表的关系。 实际上,当删除引用表对应的外键时,如果存在和此外键相似(这里的相似是指索引列数和列顺序相同)的索引时,会用相似的索引代替删除的外键,从而保持原有的引用约束关系。 例如,在下面的例子中,原来t2存在有两个索引idx1和idx2,都可以作为外键,函数`dict_foreign_find_index`选择了idx1作为外键。当删除idx1后,同样通过dict_foreign_find_index选择了idx2做为了外键。 ~~~ drop table if exists t2; drop table if exists t1; create table t1 (a int, b int, key(a)) engine=innodb; create table t2 (a int, b int, foreign key (b) references t1(a), key idx1(b), key idx2(b)) engine=innodb; alter table t2 drop key idx1; ~~~ 同样,此bug修复后,删除被引用表的索引时,如果存在相似的索引会用相似的索引代替。 例如,在下面的例子中,原来t1存在有两个索引idx1和idx2,都可以作为被引用索引,函数dict_foreign_find_index选择了idx1作为被引用索引。当删除idx1后,同样通过dict_foreign_find_index选择了idx2做为了新的被引用索引。 ~~~ drop table if exists t2; drop table if exists t1; create table t1 (a int, b int, key idx1(a), key idx2(a)) engine=innodb; create table t2 (a int, b int, foreign key (b) references t1(a)) engine=innodb; ~~~ 其实,相似索引的存在是完全没有必要的。如果禁止创建相似的索引,那么引用约束这块的处理也不会这么复杂了。