原文:http://mysql.taobao.org/monthly/2015/04/09/
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。
详见官方修复
附加说明
如果删除引用表对应的外键时,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;
其实,相似索引的存在是完全没有必要的。如果禁止创建相似的索引,那么引用约束这块的处理也不会这么复杂了。