bug描述

设置 foreign_key_checks=0 删除被引用的索引后,再设置foreign_key_checks=1,对引用表进行DML操作会导致 mysqld crash,以下是重现的测例:

  1. drop table if exists t2;
  2. drop table if exists t1;
  3. create table t1 (a int, b int, key idx1(a)) engine=innodb;
  4. insert into t1 values(1,1);
  5. insert into t1 values(2,2);
  6. create table t2 (a int, b int, foreign key (b) references t1(a)) engine=innodb;
  7. set session foreign_key_checks = 0;
  8. alter table t1 drop key idx1;
  9. set session foreign_key_checks = 1;
  10. insert into t2 values (1,1); //此语句执行时mysqld会crash

分析

对于引用约束,在mysql实现中引用表和被引用表都会记录表的引用关系。

以下链表记录该表引用了哪些表,链表中每个元素为 dict_foreign_t

  1. table->foreign_list

以下链表记录该表被哪些表引用,链表中每个元素为 dict_foreign_t

  1. table->referenced_list

dict_foreign_t 结构如下

  1. dict_foreign_t
  2. {
  3. foreign_table // t2
  4. foreign_index
  5. referenced_table // t1
  6. referenced_index // idx1
  7. ......
  8. }

对于上面的测例,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。 详见官方修复

附加说明

如果删除引用表对应的外键时,mysql如何处理的呢?同删除被引用表的索引一样,都需要调整引用表和被引用表的关系。

实际上,当删除引用表对应的外键时,如果存在和此外键相似(这里的相似是指索引列数和列顺序相同)的索引时,会用相似的索引代替删除的外键,从而保持原有的引用约束关系。

例如,在下面的例子中,原来t2存在有两个索引idx1和idx2,都可以作为外键,函数dict_foreign_find_index选择了idx1作为外键。当删除idx1后,同样通过dict_foreign_find_index选择了idx2做为了外键。

  1. drop table if exists t2;
  2. drop table if exists t1;
  3. create table t1 (a int, b int, key(a)) engine=innodb;
  4. create table t2 (a int, b int, foreign key (b) references t1(a),
  5. key idx1(b), key idx2(b)) engine=innodb;
  6. alter table t2 drop key idx1;

同样,此bug修复后,删除被引用表的索引时,如果存在相似的索引会用相似的索引代替。 例如,在下面的例子中,原来t1存在有两个索引idx1和idx2,都可以作为被引用索引,函数dict_foreign_find_index选择了idx1作为被引用索引。当删除idx1后,同样通过dict_foreign_find_index选择了idx2做为了新的被引用索引。

  1. drop table if exists t2;
  2. drop table if exists t1;
  3. create table t1 (a int, b int, key idx1(a), key
  4. idx2(a)) engine=innodb;
  5. create table t2 (a int, b int,
  6. foreign key (b) references t1(a)) engine=innodb;

其实,相似索引的存在是完全没有必要的。如果禁止创建相似的索引,那么引用约束这块的处理也不会这么复杂了。