本文是对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记录具体的外键引用列信息。
关键类对象
DML操作检查
插入子表的逻辑比较简单,只需要检查外键是否存在即可,堆栈如下:
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更新子表
DDL操作
测试表:
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父表
truncate 父表之前,会检查其是否为父表,如果是的话,直接拒绝操作,即使表上没有任何数据(Sql_cmd_truncate_table::handler_truncate –> fk_truncate_illegal_if_parent)
rename 父表列名
当父表上被外键约束的列名修改时,需要修改对应的外键信息,堆栈:
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
|–> innobase_update_foreign_cache // 更新cache信息
删除父表
drop 父表时,在函数row_drop_table_for_mysql中,检查其是否被其他表引用,如果是的话,则drop失败