天天看点

InnoDB foreign key 实现

本文是对innodb外键实现代码路径的简单记录,对外键实现逻辑熟悉的同学直接忽略吧。。。。。

外键代表两张表之间的引用约束关系:在子表上出现的列记录,必须在父表上已经存在。通过外键,我们可以确保业务上的逻辑一致性,同时还能实现一些级联操作;mysql目前只有innodb引擎支持外键,类似myisam、tokudb等引擎都不支持外键。

innodb支持建立多个列的外键,但被外键约束的父表上必须对这些列建立索引,并且子表上的外键列 和父表上索引上的顺序是一致的。默认情况下,当删除父表中被外键约束的记录时,会产生报错,但我们也可以通过在建外键索引时加上on delete cascade 来级联的更新子表,更新同理。其他行为包括restrict(限制父表的外键改动,默认值)、cascade(跟随父表的改动)、set null(子表对应列设置为null)、set default(设置为默认值,innodb不支持,但server层支持)、no action(无动作)。

由于innodb支持父表上被外键约束的索引可以不是唯一索引,因此会出现子表上一条记录 对应父表上多条记录;这是innodb的扩展特性,并不符合标准sql。

你可以从两张information_ schema表来查询实例上的外键信息:innodb_sys_foreign_cols 及 innodb_sys_foreign

在innodb层单独存储了外键约束信息,sys_foreign系统表记录表和被引用表的相关信息;sys_foreign_cols记录具体的外键引用列信息。

InnoDB foreign key 实现

插入子表的逻辑比较简单,只需要检查外键是否存在即可,堆栈如下:

row_ins_clust_index_entry/row_ins_sec_index_entry 

|–> row_ins_check_foreign_constraints 

#针对外键索引,调用函数row_ins_check_foreign_constraint,检查对应的父表中是否存在索引记录。

更新父表触发检查

row_update_for_mysql

|–>row_update_for_mysql_using_upd_graph

       |–> row_upd_sec_step –>row_upd_step –>row_upd 

              |–> row_upd_sec_index_entry 

              |–> row_upd_check_references_constraints 

               #检查表的table->referenced_set是否为空,为空表示没有子表

               #轮询每个被当前表约束的子表外键索引(table->referenced_set),检查是否修改了外键约束列 or 删除记录

               # 检查子表记录row_ins_check_foreign_constraint

               |–> row_ins_check_foreign_constraint

               #根据子表上的外键列索引,查找是否有匹配的记录(子表的外键列被隐含的创建了索引)

               # 当设置了on update/on delete属性时,需要构建联动更新的记录,调用下述函数

                      |–> row_ins_foreign_check_on_constraint

                      #根据dict_foreign_t::type类型,查询子表的聚集索引,构建更新vector和cascade node

        #当前表更新完成后 

        |–> #如果需要联动更新子表,则使用之前构建的cascade node,并loop 调用row_upd_sec_step更新子表

测试表: mysql> show create table t3\g *************************** 1. row *************************** table: t3 create table: create table `t3` ( `a` int(11) not null, `b` int(11) default null, `c` int(11) default null, `d` int(11) default null, primary key (`a`) ) engine=innodb default charset=latin1 1 row in set (0.00 sec)

mysql_alter_table 

–> ha_create_table –> ha_create –> ha_innobase::create –> create_table_info_t::create_table  // 使用copy的方式构建外键索引

|–> row_table_add_foreign_constraints // 创建表上的外键约束

     |–> dict_create_foreign_constraints

          |–> dict_create_foreign_constraints_low

          # 在该函数中,会去解析sql语句,判断sql里的各个关键字,例如alter、table、foreign之类来检查合法性,例如不允许分区表上的外键….居然是字符串匹配!!!!

          # 分配内存对象dict_mem_foreign_create

          #  如果用户没有指定,自动生产foreign key的命名:dict_create_add_foreign_id

          |–>dict_scan_table_name

          #  解出父表名,并获取对应的父表对象

               |–>dict_get_referenced_table

          # 不允许父表为分区表的外键约束

          #父表上被外键约束的字段上需要存在索引:dict_foreign_find_index

临时表在完成ddl后,需要rename成原表,这时候需要reload外键约束信息到内存数据词典cache中

–> mysql_rename_table –> ha_innobase::rename_table –>  innobase_rename_table –> row_rename_table_for_mysql

|–> dict_load_foreigns

删除外键是in-place操作,堆栈为:

mysql_alter_table –> mysql_inplace_alter_table –> ha_innobase::commit_inplace_alter_table 

|–> commit_try_norebuild

     |–> innobase_update_foreign_try

          |–> innobase_drop_foreign_try  //删除系统表sys_foreign和sys_foreign_cols上的记录 

|–> innobase_update_foreign_cache  // 更新cache信息

truncate 父表之前,会检查其是否为父表,如果是的话,直接拒绝操作,即使表上没有任何数据(sql_cmd_truncate_table::handler_truncate  –> fk_truncate_illegal_if_parent)

当父表上被外键约束的列名修改时,需要修改对应的外键信息,堆栈:

mysql_alter_table –> mysql_inplace_alter_table 

–> ha_innobase::commit_inplace_alter_table 

|–> commit_try_norebuild –>innobase_rename_columns_try –> innobase_rename_column_try 

     #修改系统表中信息,包括sys_foreign_cols

     #将修改过的外键信息从cache中移除:dict_foreign_remove_from_cache

drop 父表时,在函数row_drop_table_for_mysql中,检查其是否被其他表引用,如果是的话,则drop失败