mysql insert 源码_MySQL insert源码走读

由于网络原因,图片都贴不上来

MySQL的main函数在哪?执行一个sql语句到底经历了哪些流程和函数?MySQL的事务一致性到底是如何保证的?本文通过分析一个insert语句在MySQL代码中的关键流程,来为大家解答这些问题,同时这个insert,也是”insert”进入MySQL源码学习的第一步。

注:下面MySQL的源代码,若无特别说明,将会以5.7.18版本的代码作为依据

从哪里开始看MySQL代码?

MySQL源码下载下来后,大家会看到一堆文件夹,不知从何入手。不用着急,一开始只需要关注两个文件夹:sql和storage。其他文件夹暂不进行说明,后面需要时再进行介绍。

众所周知,MySQL通过把存储引擎做成插件一样,可以随意替换,其中sql文件夹中的,就是存储层之上的核心代码,storage文件夹中的,就是存储引擎的代码。

进入storage文件夹,可以看到一些熟悉的名字,ndb, myisam, innobase等等。这些就是每个存储引擎的代码。我们就以最流行的innodb存储引擎为例,来进行说明。(注意innobase文件夹中,就是innodb存储引擎的代码)。

Main函数在哪里?

进入sql文件夹,可以看到一个http://main.ccMySQL的无限循环在哪里?

写代码的人都知道,我要让程序什么都不敢,还不结束,肯定得有个无限循环,俗称:主循环。MySQL的主循环在哪呢?继续看mysqld_main函数,在经历了几百行代码后,终于看到了: mysqld_socket_acceptor->connection_event_loop(); 这里就是主循环。文件,没错,这里就是main函数,也就是整个mysql程序的入口。但是打开一看,只有只有寥寥20几行,还基本都是注释,然后发现mysqld_main才是真正的main函数。mysqld_main就在sql/http://mysqld.cc文件中。可以看到,mysqld_main函数在4364行定义的,藏得够深。

如果有IDE的朋友,可以直接点进去看,如果是用notepad++不带插件,甚至使用文本编辑器打开的高手,可以移步sql\connection_acceptor.h,就可以看到connection_event_loop方法:

看到这个方法,应该就很快能理解这里的逻辑了。

一次Insert的调用栈

看到这里,大家一定不耐烦了,啥时候开始正儿八经的插入逻辑呢。那么现在展示一下插入一行数据调用了多少函数:

大家可以看到从180964行开始,到181593行,光堆栈信息就打印了600多行,就可以知道一次insert,究竟经历了多少东西。如果每一个函数都进行说明的话,可以出一本书了。所以,本文只讲几个关键的地方,尤其是说明事务的一致性是如何保证的。

SQL命令的分发

繁多,复杂的SQL命令一定有解析和分发的地方,就算是写代码的新手也一定会想到,MySQL中一定有个庞大的switch case语句,根据不同的命令,调用不同方法。没错,这个switch就在sql/http://sql_parse.cc文件中,顾名思义,这个文件中包含了sql的解析和命令分发。找到mysql_execute_command方法,有可能你会见到一个比之前都长的swith语句:

图:(左)从2740行开始 (右)到4998行

2258行的switch语句,涵盖了mysql几乎所有命令的分发。字符串形式的命令,在进入到mysql_execute_command之前,字符串形式的命令,会被解析为mysql支持的命令,并放到thd->lex->sql_commond中,这个命令的全集,可以在include/my_sqlcommand.h的enum enum_sql_command中找Insert的函数

Insert对应的sql command枚举是:SQLCOM_INSERT,在mysql_execute_command方法中,可以轻易的搜到这个关键字:

图:insert语句的入口

但是当使用一般的IDE点击进入函数是,会发现这个是virtual的函数,它的实现在别的地方。关于sql解析的部分这里不展开说明,后面的文章会对此进行深入说明。有兴趣的读者可以先通过parse_sql,以及http://sql_yacc.cc文件了解sql的解析原理。

这里直接写出mysql_insert的实现方法:

bool Sql_cmd_insert::mysql_insert(THD *thd,TABLE_LIST *table_list)

其中,几个关键的步骤:

open_tables_for_query(thd, table_list, 0)

打开所需的tables,用于后面prepare_insert做准备。为什么是table_list,因为这里要考虑insert select,以及触发器等复杂的语句,因此有时插入数据到一张表,并不意味这就只访问一张表。

mysql_prepare_insert_check_table(thd, table_list, insert_field_list,

select_insert)

做插入之前的检查工作,包括:

区分insert和insert select,因为insert select会与select共享select_lex

检查表是否可以被修改,包括了:表是否是insertable的,table_list中的第一张表是否有insert权限,第二张表是否有select的权限(table_list中的第一个表是被插入的表,其他表一定是SELECT使用的)等。

预先计算和保存NATURAL/USING join类型行类型

……

add_function_default_columns

这里是标记表中的哪些列是需要使用默认值插入的。在使用insert语句时,我们并不一定会每个列都需要给值,那么这里就会把这些列给进行标记。

lock_tables

获得所有表的锁,里面包含了对已有锁,以及与其他类型的锁(如事务锁)的判断,以避免死锁的产生。

ha_start_bulk_insert

这里是当我们insert中使用了多行插入时,允许存储引擎进行插入优化。当只有一个值是,我们从trace日志中也可以看到并没有执行其他的内容。这里可以看到这个函数是ha开头的,在我们sql层的代码中,绝大部分ha_开头的函数都是指要调用存储引擎的接口,ha代表handler。关于handler

write_record

把要插入的数据写到table的buffer中,同时调用对应的触发器;这里是在内存中真正写数据的地方。如果使用的是innodb,从trace日志中可以看到,write_record里面,会调用innobase的handler,最终写入到b+ tree中

图 write record的调用栈

事务提交

执行完mysql_insert方法,程序又回到了mysql_execute_command方法中,并结束巨大的switch语句,在继续向下,就到了关键步骤:事务的提交。

MySQL为了提高写性能,遵循WAL(Write ahead Log)机制,事务日志落盘即可完成事务,这个过程就在trans_commit_stmt(thd)调用中完成

图 trans_commit_stmt的调用栈

如果使用的是innodb,事务提交主要流程,就是从innobase_commit开始的。从上面的调用栈中可以看到,在提交事务的过程中,执行了日志的写入,其中ib_log就是我们熟知的redo log,而后面的write 45060255 to 45060385就是lsn的变化。

详细的来看整个事务的提交过程:

>Innobase_commit

| >innobase_commit_low(trx)

| | >trx_commit_for_mysql(trx)

| | | >trx_commit(trx)

| | | | >trx_commit_low(trx, mtr)

| | | | | >trx_commit_in_memory(rx, mtr, serialised)

| | | | | | >trx_flush_log_if_needed(lsn, trx)

| | | | | | | >trx_flush_log_if_needed_low(lsn)

| | | | | | | | >log_write_up_to

|

其中关注这三个函数trx_commit_in_memory ,log_write_up_to,trx_commit_complete_for_mysql。

trx_commit_in_memory中,生成了lsn,同时,通过innodb_flush_log_at_trx_commit参数来控制mysql是否严格遵循WAL机制。

图 redo log的刷盘机制

从这段代码可以看出,若innodb_flush_log_at_trx_commit=0时,并没有立刻执行刷盘,而是调用thd_requested_durability暂时把日志写到缓冲区中,通过其他线程定时的去把事务日志写入存储。若trx_commit_complete_for_mysql调用完成后,日志还没有写入存储,而mysql又发生了故障,则就会出现事务丢失。因此在mysql的使用过程中,要尽量保证trx_commit_complete_for_mysql=1

再来看log_write_up_to,这里把事务写入了日志缓存中

图 log_write_up_to调用了log_group_write_buf写入日志

注意,从函数名和传入参数可以看出,带有group字样,这就是大名鼎鼎的group commit,把mysql的性能提高了数倍。

最后来看trx_commit_complete_for_mysql,发现几乎和trx_commit_in_memory是同样的流程,单是唯一不同的是,传入参数flush_to_disk不同了,上一次是false,这一次是true,因此,执行了这个函数:log_write_flush_to_disk_low(),其中又调用了file_flush()

图 file flush

再看file flush的定义,传入的是一组日志文件的id,或者tablespace的id

图 file_flush的定义

Innodb就通过这个id,找到对应的datafile文件,最终调用os_file_flush(file) 中的fsync真正把事务写到了存储上。

图 使用fsync写入文件

看到这里,事务终于提交了,但是也有个疑问,为什么有两个几乎相同流程的trx_commit_in_memory和trx_commit_complete_for_mysql呢?

原来,这就是大名鼎鼎的两阶段事务,mysql先在内存中提交事务,然后再把日志真正刷到存储上。两阶段事务,给mysql带来很多的好处,包括:group commit,分布式事务,解决redo和binlog一致性问题等。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值