本文主要介绍Mysql主要的调 用流程,将从代码的角度来看一个从用户发出的"select * from test" SQL命令在服务器内部是如何被执行的。从我个人的经验来看,阅读理解大规模项目的代码最重要的两个方面,一是了解主要的数据结构,二是了解数据流,在这 里主要是调用流程。把这两个主线把握住以后,大部分代码都是比较容易阅读的,Mysql的源代码属于比较好读的类型,因为函数的调用关系比较明确。难读的 代码一般都充斥着大量的回调、异步调用,很可能你极难找到某个函数在哪里或什么时候被调用了。当然,算法的实现代码也很难读。幸好Mysql不是那种难读 的类型,所以我们也不要害怕,大步向前吧!
从架构上来看,Mysql服务器对于一条SQL语句的执行过程可以分成如下几部分:
接受命令 包括用户验证,资源申请等
|
V
命令解析 解析SQL语句,生成语法树
|
V
寻找执行计划 根据解析出来的语法树,找到可能的执行计划。对于一条SQL语句,很可能会有多种执行方案,特别是在SQL语句比较复杂的时候。这里需要对于各种可能的方案进行代价评估,最快的找到最有的执行方案。
|
V
优化执行计划 优化执行计划。这是SQL执行中最复杂的部分之一,据说全都是由数学博士们写出来的,而且比较难懂。我目前还处于不懂的状态。
|
V
执行 没啥可说的,只剩执行及返回结果了
所有的程序都从main开始,mysqld也不例外,打开sql/mysqld.cc,稍加搜索,你就能看到熟悉的main函数,我们可以将其进行如下简写:
int main(int argc, char* argv[]) {
logger.init_base();
init_common_variables(MYSQL_CONFIG_NAME, argc, argv, load_default_groups)); // 解析配置文件和命令行参数,将配置文件中的内容转行成命令行参数
init_signals();
user_info= check_user(mysqld_user);
set_user(mysqld_user, user_info);
init_server_components(); // 初始化服务器模块
network_init(); // 初始化网络模块,根据配置,打开IP socket/unix socket/windows named pipe来进行监听。
start_signal_handler(); // 开始接收信号
acl_init(...); // 初始化ACL (Access Control List)
servers_init(0); // 服务器初始化
init_status_vars(); // 状态变量初始化
create_shutdown_thread(); // 创建关闭线程
create_maintenance_thread(); // 创建维护线程
sql_print_information(...); // 打印一些信息
handle_connections_sockets(0); // 主要的服务处理函数,循环等待并接受命令,进行查询,返回结果,也是我们要详细关注的函数
wait for exit; // 服务要退出
cleanup;
exit(0);
}
可以仔细的看看这个简写的main函数,逻辑很清楚,就算没有我的这些注释大部分人也能容易的理解整个系统的执行流程。其实完整的main函数有接近300行,但是中心思想已经被包含在这里简短的十几行代码中了。
通过看这些代码,读者会发现mysqld是通过多线程来处理任务的,这点和Apache服务器是不一样的。
mysqld等待和处理命令主要在handle_connections_sockets(0);来完成,这里我们仔细看看这个函数调用发生了什么。该函数也在mysqld.cc中,也有大概300行,我们继续简写。
为了方便分析,这里我们假定配置服务器通过unix domain socket来监听接受命令,其他方式类同。
pthread_handler_t handle_connections_sockets(void *arg __attribute__((unused)))
{
FD_ZERO(&clientFDs);
FD_SET(unix_sock,&clientFDs); // unix_socket在network_init中被打开
}
看到这里,大家应该已经基本清楚mysqld如何启动并进入监听状态,真正的命令处理就是在create_new_thread里面,看名字也知道就是创建一个新线程来处理任务。
怎么样,是不是觉得mysql的代码很好懂呢?呵呵,更坚定了要继续读下去的信心。
下面具体看看服务器如何执行语句"insert"语句的。
上一节我们提到create_new_thread是所有处理的入口,这里我们仔细看看它到底干了什么。幸运的是,它也在mysqld.cc里面,我们不费吹灰之力就找他了它:
static void create_new_thread(THD *thd) {
NET *net=&thd->net;
if (connection_count >= max_connections + 1 || abort_loop) { // 看看当前连接数是不是超过了系统配置允许的最大值,如果是就断开连接。
close_connection(thd, ER_CON_COUNT_ERROR, 1);
delete thd;
}
++connection_count;
thread_scheduler.add_connection(thd); // 将新连接加入到thread_scheduler的连接队列中。
}
现在看来关键还是在thread_scheduler干了什么,现在打开sql/scheduler.cc文件:
void one_thread_per_connection_scheduler(scheduler_functions* func) {
func->max_threads= max_connections;
func->add_connection= create_thread_to_handle_connection;
func->end_thread= one_thread_per_connection_end;
}
再看create_thread_to_handle_connection,它还是在mysqld.cc中,哈哈:
void create_thread_to_handle_connection(THD *thd) {
if (cached_thread_count > wake_thread) {
thread_cache.append(thd);
pthread_cond_signal(&COND_thread_cache);
} else {
threads.append(thd);
pthread_create(&thd->real_id,&connection_attrib, handle_one_connection, (void*) thd)));
}
}
恩,看来先是看当前工作线程缓存(thread_cache)中有否空余的线程,有的话,让他们来处理,否则创建一个新的线程,该线程执行handle_one_connection函数
很好,继续往下看,到了sql/sql_connection.cc中。
pthread_handler_t handle_one_connection(void *arg) {
thread_scheduler.init_new_connection_thread();
setup_connection_thread_globals(thd);
for (;;) {
lex_start(thd);
login_connection(thd); // 进行连接身份验证
prepare_new_connection_state(thd);
}
}
do_command在sql/sql_parse.cc中。
bool do_command(THD *thd) {
NET *net= &thd->net;
packet_length= my_net_read(net);
packet= (char*) net->read_pos;
command= (enum enum_server_command) (uchar) packet[0]; // 解析客户端穿过来的命令类型
dispatch_command(command, thd, packet+1, (uint) (packet_length-1));
}
再看dispatch_command:
bool dispatch_command(enum enum_server_command command, THD *thd, char* packet, uint packet_length) {
NET *net= &thd->net;
thd->command=command;
switch (command) {
case COM_INIT_DB: ...;
case COM_TABLE_DUMP: ...;
case COM_CHANGE_USER: ...;
...
case COM_QUERY:
alloc_query(thd, packet, packet_length);
mysql_parse(thd, thd->query, thd->query_length, &end_of_stmt);
}
}
进行sql语句解析
void mysql_parse(THD *thd, const char *inBuf, uint length, const char ** found_semicolon) {
lex_start(thd);
if (query_cache_send_result_to_client(thd, (char*) inBuf, length) <= 0) { // 看query cache中有否命中,有就直接返回结果,否则进行查找
Parser_state parser_state(thd, inBuf, length);
parse_sql(thd, & parser_state, NULL); // 解析sql语句
mysql_execute_command(thd); // 执行
}
}
总算开始执行了,mysql_execute_command函数超长,接近3k行:-(,我们还是按需分析吧。还是觉得这种代码不应该出现在这种高水平的开源软件里面,至少在linux kernel中很少看见这么长的函数,而在mysql里面确实是常常看到。
int mysql_execute_command(THD *thd) {
LEX *lex= thd->lex; // 解析过后的sql语句的语法结构
TABLE_LIST *all_tables = lex->query_tables; // 该语句要访问的表的列表
switch (lex->sql_command) {
...
case SQLCOM_INSERT:
insert_precheck(thd, all_tables);
check_table_access(thd, lex->exchange ? SELECT_ACL | FILE_ACL : SELECT_ACL, all_tables, UINT_MAX, FALSE); // 检查用户对数据表的访问权限
execute_sqlcom_select(thd, all_tables); // 执行select语句
break;
}
}