典型查询执行以一个简单查询为例:select * from some_table where field_x = 200,some_table为MyISAM存储引擎。我们可以看到MyISAM存储引擎怎么通过存储引擎抽象层完成实际的执行。
我们从MySQL最开始的周期开始,从 / sql / mysqld.cc的main开始
int main(int argc, char * * argv) {
init_common_variables(MYSQL_CONFIG_NAME, argc, argv, load_default_groups);
init_ssl();
server_init();
init_server_components();
start_signal_handler(); // Creates pidfile
acl_init((THD * ) 0, opt_noacl);
init_slave();
create_shutdown_thread();
create_maintenance_thread();
handle_connections_sockets(0);
DBUG_PRINT("quit", ("Exiting main thread"));
exit(0);
}
这个是服务器进程执行的开始。高亮的部分是大家应该更感兴趣的部分。
init_common_variables()处理mysqld或者mysqld_safe传进来的命令行参数以及MySQL的配置文件。
快速浏览一下init_server_components()和acl_init():init_server_components()确保MYSQL_LOG对象在线并且工作,
acl_init()让访问权限控制系统启动并运行,包括把权限缓存到内存中。MySQL会通过create_maintenance_thread()和create_shutdown_thread()分别创建一个独立的线程进行任务维护和停机时间处理。
handle_connections_sockets()函数是所有处理开始的地方,我们继续看:
handle_connections_sockets(arg attribute((unused))) {
if (ip_sock != INVALID_SOCKET) {
FD_SET(ip_sock, & clientFDs);
DBUG_PRINT("general", ("Waiting for connections."));
while (!abort_loop) {
new_sock = accept(sock, my_reinterpret_cast(struct sockaddr * )( & cAddr), & length);
thd = new THD;
if (sock == unix_sock) thd - > host = (char * ) my_localhost;
create_new_thread(thd);
}
}
}
基本的原理就是:mysql.sock被绑定用于监听用户请求。如果接收到一个连接请求,一个新的THD的结构就会被创建,然后被传递给create_new_thread()函数。我们接着看create_new_thread:
static void create_new_thread(THD * thd) {
DBUG_ENTER("create_new_thread"); /* don't allow too many connections */
if (thread_count - delayed_insert_threads >= max_connections + 1 || abort_loop) {
DBUG_PRINT("error", ("Too many connections"));
close_connection(thd, ER_CON_COUNT_ERROR, 1);
delete thd;
DBUG_VOID_RETURN;
}
pthread_mutex_lock( & LOCK_thread_count);
if (cached_thread_count > wake_thread) {
start_cached_thread(thd);
} else {
thread_count++;
thread_created++;
if (thread_count - delayed_insert_threads > max_used_connections) max_used_connections = thread_count - delayed_insert_threads;
DBUG_PRINT("info", (("creating thread %d"), thd - > thread_id));
pthread_create( & thd - > real_id, & connection_attrib, /
handle_one_connection, (void * ) thd))(void) pthread_mutex_unlock( & LOCK_thread_count);
}
DBUG_PRINT("info", ("Thread created"));
}
我们接着看看这些高亮的函数。首先是使用pthread_mutex_lock()函数对LOCK_thread_count资源进行加锁。这是至关重要的,因为thread_count和thread_created变量在函数执行期间会被修改。thread_count和thread_created是被所有服务器进程中的线程共享。在create_new_thread执行时,pthread_mutex_lock通过加锁阻止其它线程修改它们的内存。这是MySQL资源管理子系统的典型工作方式。
start_cached_thread启动连接线程池机制。最后并且是最重要的是,pthread_create()(线程库的一个函数)创建一个线程,并接收传递进来的THD -> real_id成员变量和handle_one_connection()函数指针(用来处理单个连接的创建),handle_one_connection()的实现参见 /sql/sql_parse.cc
/sql/sql_parse.cc handle_one_connection()
handle_one_connection(THD * thd)
{
while (!net-> error && net-> vio != 0 && !(thd-> killed == THD::KILL_CONNECTION)) {
if (do_command(thd)) break;
}
}
为了简单期间,我们省略了很多函数里面的代码。保留的函数聚焦在THD结构的初始化。需要net-> error被校验为真,意味着THD的net成员变量在loop循环中需要使用,换句话说do_command()一定在发送和接受数据包。
bool do_command(THD * thd)
{
char * packet;
ulong packet_length;
NET * net;
enum enum_server_command command;
packet = 0;
net_new_transaction(net);
packet_length = my_net_read(net);
packet = (char * ) net - > read_pos;
command = (enum enum_server_command)(uchar) packet[0];
DBUG_RETURN(dispatch_command(command, thd, packet + 1, (uint) packet_length));
}
现在开始我们已经开始逐渐深入了。首先需要记住数据包的发送是通过MySQL网络子系统的通讯协议完成的。net_new_transaction()开始启动时会初始化服务端和客户端的第一个数据包通信。客户端使用传递过来的netstruct,并将net buffers进行填充后回送给服务端。服务端通过调用my_net_read,获得客户端包的长度,并且将数据包的内容填充进入net - > read_posbuffer。网络子系统就完成了他的光荣使命了。
然后我们来看command变量。它和THD指针,数据包变量(查询的SQL语句就在里面),数据包长度一并被传递给了dispatch_command。
/sql/sql_parse.cc dispatch_command()
bool dispatch_command(enum enum_server_command command, THD * thd, char * packet, uint packet_length) {
switch (command) {
// ... omitted
case COM_TABLE_DUMP:
case COM_CHANGE_USER:
// ... omitted
case COM_QUERY:
{
if (alloc_query(thd, packet, packet_length))
break; // fatal error is set
mysql_log.write(thd, command, "%s", thd - > query);
mysql_parse(thd, thd - > query, thd - > query_length);
}
// ... omitted
}
就象函数名字解释的那样,它所作的就是把query分配给合适的处理句柄。我们进入COM_QUERY部分,alloc_query()简单的将数据包中的内容传递给THD-> querymember成员变量,并且为线程分配需要的内存。然后用log模块记录了这个请求。最后我们看一下mysql_parse()的调用,其实这个名称是不准确的,mysql_parse()实际上也会执行查询工作。
/sql/sql_parse.cc mysql_parse()
void mysql_parse(THD * thd, char * inBuf, uint length)
{
if (query_cache_send_result_to_client(thd, inBuf, length) <= 0)
{
LEX * lex = thd - > lex;
yyparse((void * ) thd);
mysql_execute_command(thd);
query_cache_end_of_result(thd);
}
DBUG_VOID_RETURN;
}
首先服务器检查查询缓存是否保存了一个和当前查询请求结果相同的查询请求。如果没有命中,THD就会被传递给yyparse()(Bison生成的分析器)进行解析。yyparse返回给THD-> Lexstruct经过优化的执行路径。上述执行完毕后,mysql_execute_command()就开始执行。需要注意一点,在查询执行完毕后,query_cache_end_of_result()函数紧接着被调用。此函数功能就是让查询缓存知道thd完成了处理。接下来我们看一下查询缓存如何保存返回的结果集。
/sql/sql_parse.cc mysql_execute_command()
bool mysql_execute_command(THD * thd)
{
all_tables = lex - > query_tables;
statistic_increment(thd - > status_var.com_stat[lex - > sql_command],
& LOCK_status);
switch (lex - > sql_command) {
case SQLCOM_SELECT:
{
select_result * result = lex - > result;
check_table_access(thd, lex - > exchange ? SELECT_ACL | FILE_ACL : SELECT_ACL, all_tables, 0);
open_and_lock_tables(thd, all_tables);
query_cache_store_query(thd, all_tables);
res = handle_select(thd, lex, result);
break;
}
case SQLCOM_PREPARE:
case SQLCOM_EXECUTE:
// ...
default:
/* Impossible */
send_ok(thd);
break;
}
}
进入mysql_execute_command(),我们可以看到一系列的有趣事情。首先,我们来看一下statistic_increment()是如何更新统计信息的。先统计SELECT语句的com_stat变量。然后,执行子系统通过check_table_access()和权限控制子系统进行交互。它将检查THD中执行的查询对于查询所需要的表是否具有权限。最有趣的是open_and_lock_tables这个进程。我们先不深入进去,仅仅描述一下它的功能:对用户的连接线程建立表缓存,并对需要的所有表进行加锁。接下来我们看一下query_cache_store_query()。查询缓存使用内部的HASH算法对请求的查询进行存储。最后,调用handle_select()处理查询, 它就是存储引擎抽象层的第一个主要标识。
/sql /sql_select.cc handle_select()
bool handle_select(THD * thd, LEX * lex, select_result * result) {
res = mysql_select(thd, & select_lex - > ref_pointer_array, (TABLE_LIST * ) select_lex - > table_list.first, select_lex - > with_wild, select_lex - > item_list, select_lex - > where, select_lex - > order_list.elements + select_lex - > group_list.elements, (ORDER * ) select_lex - > order_list.first, (ORDER * ) select_lex - > group_list.first,
select_lex - > having, (ORDER * ) lex - > proc_list.first, select_lex - > options | thd - > options, result, unit, select_lex);
DBUG_RETURN(res);
}
handle_select除了对mysql_select()函数进行简单包裹之外,其它什么也没有做。
/sql/sql_select.cc mysql_select()
bool mysql_select(THD * thd, Item * * * rref_pointer_array, TABLE_LIST * tables, uint wild_num, List < Item > & fields, COND * conds, uint og_num, ORDER * order, ORDER * group, Item * having, ORDER * proc_param, ulong select_options, select_result * result, SELECT_LEX_UNIT * unit, SELECT_LEX * select_lex)
{
JOIN * join;
join = new JOIN(thd, fields, select_options, result);
join - > prepare(rref_pointer_array, tables, wild_num,
conds, og_num, order, group, having, proc_param,
select_lex, unit));
join - > optimize();
join - > exec();
}
mysql_select()也简单的讲执行的功能交给JOIN对象。执行之前,join先执行optimize()。我们接下来看一下join::exec的实现
/sql/sql_select.cc JOIN: exec()
void JOIN::exec()
{
error = do_select(curr_join, curr_fields_list, NULL, procedure);
thd - > limit_found_rows = curr_join - > send_records;
thd - > examined_row_count = curr_join - > examined_rows;
}
JOIN::exec()看起来又象另外一个包裹函数。JOIN::exec()简单的调用do_select()函数来处理大部分工作。但是,我们需要知道一旦do_select()返回,我们将结果计数的信息赋值给THD的成员变量。接下来我们进入do_select():
/sql/sql_select.cc do_select()
static int do_select(JOIN * join, List < Item > * fields, TABLE/ * table, Procedure * procedure)
{
JOIN_TAB * join_tab;
sub_select(join, join_tab, 0);
join - > result - > send_eof())
}
我们貌似看到希望了。JOIN对象的结果成员变量在调用sub_select()后发送了一个EOF标识,所以我们要深入进去。通过这个行为,它看起来sub_select()函数应该使用结果对join对象的结果成员变量进行赋值。好的,我们看看这个猜想是否正确:
/ sql / sql_select.cc sub_select()
static int sub_select(JOIN * join, JOIN_TAB * join_tab, bool end_of_records)
{
join_init_read_record(join_tab);
READ_RECORD * info = & join_tab - > read_record;
join - > thd - > row_count = 0;
do {
join - > examined_rows++;
join - > thd - > row_count++;
} while (info - > read_record(info)));
}
return 0;
}
sub_select()函数的关键是do…while循环,此循环直到READ_RECORD结构体变量停止调用它的read_record()成员函数后结束。我们接着看:
READ_RECORD结构体在 /sql /structs.h中进行定义。它定义了MySQL内部格式的记录。
首先join_init_read_record()函数是连接存储引擎抽象层的纽带。它初始化JOIN_TAB结构体中的记录然后将READ_RECORD对象赋值给read_record成员变量。
/sql/sql_select.cc join_init_read_record()
static int join_init_read_record(JOIN_TAB * tab) {
init_read_record( & tab - > read_record, tab - > join - > thd, tab - > table, tab - > select, 1, 1);
return ( * tab - > read_record.read_record)( & tab - > read_record);
}
它直接调用init_read_record()函数,然后返回从表中获取的记录数目。这就是它所做的全部,那么存储引擎和记录缓存在哪里进行交互呢?看看init_read_record()就清楚了。
/sql/records.cc init_read_record()
void init_read_record(READ_RECORD * info, THD * thd, TABLE * table,
SQL_SELECT * select,
int use_record_cache, bool print_error) {
info - > read_record = rr_sequential;
table - > file - > ha_rnd_init(1);
}
这里完成了两件重要的事情。首先,READ_RECORD类型指针的read_record成员变量被赋值为rr_sequential。意味着,后面对于info - > read_record的调用将会转化成为rr_sequential(READ_RECORD * info),它会使用记录缓存来获取数据。现在需要记住,所有loop循环对read_record()的调用将会从现在开始命中记录缓存。我们现在来关注对ha_rnd_init()函数的调用,无论何时你看见ha_开头的函数,你立即应该知道你在使用表(存储引擎函数)的句柄函数。第一个猜测是这个函数用来从存储引擎的磁盘中扫描记录段。所以,让我们来分析ha_rnd_init().为什么只有头文件?因为句柄类仅仅是存储引擎子类实现的的接口。通过类的定义我们看到一个单例方法被定义了。
/sql/handler.h handler::ha_rnd_init()
int ha_rnd_init(bool scan)
{
DBUG_ENTER("ha_rnd_init");
DBUG_ASSERT(inited == NONE || (inited == RND && scan));
inited = RND;
DBUG_RETURN(rnd_init(scan));
}
既然我们在一张MyISAM表上进行查询,我们来看一下ha_myisam句柄类的rnd_init()虚函数的声明。
/sql/ha_myisam.cc ha_myisam::rnd_init()
int ha_myisam::rnd_init(bool scan) {
if (scan)
return mi_scan_init(file);
// …
}
现在我们已经可以肯定了,rnd_init方法包括了对表记录的扫描。具体的实现在mi_scan_init()函数中。
/myisam/mi_scan.c mi_scan_init()
int mi_scan_init(register MI_INFO * info) {
info - > nextpos = info - > s - > pack.header_length; /* Read first record */
// …
}
不可思议的是所有的工作仅仅是把一条记录读入READ_RECORD结构体中!幸运的是,我们已经基本上完结了。
/sql/records.cc rr_sequential()
static int rr_sequential(READ_RECORD * info)
{
while ((tmp = info - > file - > rnd_next(info - > record)))
{
if (tmp == HA_ERR_END_OF_FILE)
tmp = -1;
}
return tmp;
}
当sub_select()调用read_record()成员函数的时候,这个函数都会被调用。它依次调用另外一个MyISAM句柄方法,rnd_next(),简单的把当前记录指针赋值到需要的READ_RECORD结构体中。这是因为rnd_next已经被简单的映射到mi_scan函数实现上。
/myisam/mi_scan.c mi_scan()
int mi_scan(MI_INFO * info, byte * buf) {
// …
info - > update &= (HA_STATE_CHANGED | HA_STATE_ROW_CHANGED);
DBUG_RETURN(( * info - > s - > read_rnd)(info, buf, info - > nextpos, 1));
}
这样,记录缓存的行为更多的像是一个句柄的包裹库而不是一个缓存。其实不然我们省略掉了对共享IO_CACHE对象的介绍。
这基本上就是一条基本查询的整个执行过程。