引言
本文主要介绍Mysql主要的调用流程,将从代码的角度来看一个从用户发出的"select * from test"
SQL命令在服务器内部是如何被执行的。从我个人的经验来看,阅读理解大规模项目的代码最重要的两个方面,一是了解主要的数据结构,二是了解数据流,在这
里主要是调用流程。把这两个主线把握住以后,大部分代码都是比较容易阅读的,Mysql的源代码属于比较好读的类型,因为函数的调用关系比较明确。难读的
代码一般都充斥着大量的回调、异步调用,很可能你极难找到某个函数在哪里或什么时候被调用了。当然,算法的实现代码也很难读。幸好Mysql不是那种难读
的类型,所以我们也不要害怕,大步向前吧!
主要执行过程
从架构上来看,Mysql服务器对于一条SQL语句的执行过程可以分成如下几部分:
接受命令 包括用户验证,资源申请等
命令解析 解析SQL语句,生成语法树
寻找执行计划 根据解析出来的语法树,找到可能的执行计划。对于一条SQL语句,很可能会有多种执行方案,特别是在SQL语句比较复杂的时候。这里需要对于各种可能的方案进行代价评估,最快的找到最有的执行方案。
优化执行计划 优化执行计划。这是SQL执行中最复杂的部分之一,据说全都是由数学博士们写出来的,而且比较难懂。我目前还处于不懂的状态。
执行 没啥可说的,只剩执行及返回结果了
系统启动
所有的程序都从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中被打开
socket_flags=fcntl(unix_sock, F_GETFL, 0);
while (!abort_loop) { // abort_loop是全局变量,在某些情况下被置为1表示要退出。
readFDs=clientFDs; // 需要监听的socket
select((int) max_used_connection,&readFDs,0,0,0); // select异步监听,当接收到时间以后返回。
sock = unix_sock;
flags= socket_flags;
fcntl(sock, F_SETFL, flags | O_NONBLOCK);
new_sock = accept(sock, my_reinterpret_cast(struct sockaddr *) (&cAddr), &length); // 接受请求
getsockname(new_sock,&dummy, &dummyLen);
thd= new THD; // 创建mysqld任务线程描述符,它封装了一个客户端连接请求的所有信息
vio_tmp=vio_new(new_sock, VIO_TYPE_SOCKET, VIO_LOCALHOST); // 网络操作抽象层
my_net_init(&thd->net,vio_tmp)); // 初始化任务线程描述符的网络操作
create_new_thread(thd); // 创建任务线程
}
}
看到这里,大家应该已经基本清楚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(thd); // 处理命令
end_connection(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);
mysql_insert(thd, all_tables, lex->field_list,
lex->many_values, lex->update_list, lex->value_list,
lex->duplicates, lex->ignore);
break;
...
case SQLCOM_SELECT:
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;
}
}
Mysql源代码分析系列(4): 主要调用流程(续)
在上一篇文章中我们讲到了的mysql_execute_command,这个函数根据解析出来的SQL命令分别调用不同的函数做进一步处理。我们这里先
看"INSERT"命令的处理流程。其对应的处理函数是mysql_insert,在sql/sql_insert.cc中,还是很长,大概300多
行。
bool mysql_insert(THD *thd,
TABLE_LIST *table_list, // 该命令要用到的表
List<Item> &fields, // 使用的域
List<List_item> &values_list,
List<Item> &update_fields,
List<Item> &update_values,
enum_duplicates duplic,
bool ignored) {
open_and_lock_tables(thd, table_list);
mysql_prepare_insert(...);
foreach value in values_list {
write_record(...);
}
}
其实里面还有很多处理trigger,错误,view之类的,我们暂时都忽略。
// 写数据记录
int write_record(THD *thd, TABLE *table,COPY_INFO *info) {
if (info->handle_duplicates == DUP_REPLACE || info->handle_duplicates == DUP_UPDATE) {
table->file->ha_write_row(table->record[0]);
table->file->ha_update_row(table->record[1], table->record[0]));
} else {
table->file->ha_write_row(table->record[0]);
}
}
不用说,这里我们还是省略了好多东西,要注意的是这里调用的table->file->ha_write_row和
table->file->ha_update_row。在sql/table.h可以看到table的定义,其中file被定义成
handler *file;
那handler是什么?对了,这就是我们前面提到的数据存储抽象层,所有的存储引擎都必须事先这里定义的接口,才能被mysql使用。在这里使用的具体
的接口操作是ha_write_row和ha_update_row。这两个函数可以在sql/handler.cc中看到。比如
ha_write_row:
int handler::ha_write_row(uchar *buf) {
write_row(buf); // 调用具体的实现
binlog_log_row(table, 0, buf, log_func)); // 写binlog
}
下面我们看看在myisam中是怎么实现的文件操作,代码在storage/myisam/ha_myisam.cc中。先看write_row:
723 int ha_myisam::write_row(uchar *buf)
724 {
725 ha_statistic_increment(&SSV::ha_write_count);
726
727 /* If we have a timestamp column, update it to the current time */
728 if (table->timestamp_field_type & TIMESTAMP_AUTO_SET_ON_INSERT) // 如果有timestamp域,写入当前时间。
729 table->timestamp_field->set_time();
730
731 /*
732 If we have an auto_increment column and we are writing a changed row
733 or a new row, then update the auto_increment value in the record.
734 */
735 if (table->next_number_field && buf == table->record[0]) // 更新auto_increment列
736 {
737 int error;
738 if ((error= update_auto_increment()))
739 return error;
740 }
741 return mi_write(file,buf); // 真正写文件
742 }
再看mi_write函数,很好找,就在storage/myisam/mi_write.c,我们就不再分析下去了,具体实现和myisam使用的文件格式相关,有兴趣的可以参考myisam的相关文档。
结语
写到这里,我们对于mysql执行sql语句的主要调用流程就有了大概的了解,但是真正的难点往往在细节中,我也刻意逃避了这些细节,但是还是应该列一下相关的内容:
+ Sql语句的解析和相关数据结构
+ Sql语句的描述数据结构
+ 执行优化相关算法
+ 数据存储殷勤抽象层的定义和交互
+ 存储引擎的具体操作和文件格式
必须要提到的是,这些地方的代码都比较难懂,而且核心函数都很长,非常不便与理解,有需要的人可以选一些方面具体深入,但要面面俱到就需要很多时间了。
Mysql源代码分析(5): Plugin架构介绍
Mysql现在很多模块都是通过plugin的方式连接到Mysql核心中的,除了大家熟悉的存储引擎都是Plugin之外,Mysql还支持其他类型的
plugin。本文将对相关内容做一些简单介绍。主要还是以架构性的介绍为主,具体细节会提到一点,但是肯定不会包括所有的细节。
主要数据结构和定义
大部分的数据接口,宏和常量都定义在include/mysql/plugin.h中,我们来慢慢看。
先看plugin的类型:
#define MYSQL_UDF_PLUGIN 0 /* User-defined function */
#define MYSQL_STORAGE_ENGINE_PLUGIN 1 /* Storage Engine */
#define MYSQL_FTPARSER_PLUGIN 2 /* Full-text parser plugin */
#define MYSQL_DAEMON_PLUGIN 3 /* The daemon/raw plugin type */
#define MYSQL_INFORMATION_SCHEMA_PLUGIN 4 /* The I_S plugin type */
开发者开发的plugin必须指定上述类型之一。类型包括用户自定义函数,存储引擎,全文解析,原声plugin和information schema
plugin。最常见的是前三个,daemon plugin一般用来在mysqld中启动一个线程,在某些时候干活儿。
一
个plugin的描述数据接口是:
struct st_mysql_plugin
{
int type; /* the plugin type (a MYSQL_XXX_PLUGIN value) */
void *info; /* pointer to type-specific plugin descriptor */
const char *name; /* plugin name */
const char *author; /* plugin author (for SHOW PLUGINS) */
const char *descr; /* general descriptive text (for SHOW PLUGINS ) */
int license; /* the plugin license (PLUGIN_LICENSE_XXX) */
int (*init)(void *); /* the function to invoke when plugin is loaded */
int (*deinit)(void *);/* the function to invoke when plugin is unloaded */
unsigned int version; /* plugin version (for SHOW PLUGINS) */
struct st_mysql_show_var *status_vars;
struct st_mysql_sys_var **system_vars;
void * __reserved1; /* reserved for dependency checking */
};
主要内容包括类型,名字,初始化/清理函数,状态变量和系统变量的定义等等。但是在使用的时候一般不是直接使用这个数据结构,而是使用大量的宏来辅助。
一个plugin的开始:
#define mysql_declare_plugin(NAME) \
__MYSQL_DECLARE_PLUGIN(NAME, \
builtin_ ## NAME ## _plugin_interface_version, \
builtin_ ## NAME ## _sizeof_struct_st_plugin, \
builtin_ ## NAME ## _plugin)
plugin定义结束:
#define mysql_declare_plugin_end ,{0,0,0,0,0,0,0,0,0,0,0,0}}
__MYSQL_DECLARE_PLUGIN根据plugin是动态链接plugin还是静态链接plugin有不同的定义:
#ifndef MYSQL_DYNAMIC_PLUGIN
#define __MYSQL_DECLARE_PLUGIN(NAME, VERSION, PSIZE, DECLS) \
int VERSION= MYSQL_PLUGIN_INTERFACE_VERSION; \
int PSIZE= sizeof(struct st_mysql_plugin); \
struct st_mysql_plugin DECLS[]= {
#else
#define __MYSQL_DECLARE_PLUGIN(NAME, VERSION, PSIZE, DECLS) \
int _mysql_plugin_interface_version_= MYSQL_PLUGIN_INTERFACE_VERSION; \
int _mysql_sizeof_struct_st_plugin_= sizeof(struct st_mysql_plugin); \
struct st_mysql_plugin _mysql_plugin_declarations_[]= {
#endif
特别要注意的是“#ifndef MYSQL_DYNAMIC_PLUGIN”,如果你要写的plugin是动态加载的话,需要在编译的时候定义这个宏。
总体而言,mysql_declare_plugin申明了一个struct
st_mysql_plugin数组,开发者需要在该宏之后填写plugin自定义的st_mysql_plugin各个成员,并通过
mysql_declare_plugin_end结束这个数组。
看个例子plugin/daemon_example/daemon_example.cc,这是个动态MYSQL_DAEMON_PLUGIN类型的
plugin,注意到plugin/daemon_example/Makefile.am里面有-DMYSQL_DYNAMIC_PLUGIN。具体定
义如下:
mysql_declare_plugin(daemon_example)
{
MYSQL_DAEMON_PLUGIN,
&daemon_example_plugin,
"daemon_example",
"Brian Aker",
"Daemon example, creates a heartbeat beat file in mysql-heartbeat.log",
PLUGIN_LICENSE_GPL,
daemon_example_plugin_init, /* Plugin Init */ // plugin初始化入口
daemon_example_plugin_deinit, /* Plugin Deinit */ // plugin清理函数
0x0100 /* 1.0 */,
NULL, /* status variables */
NULL, /* system variables */
NULL /* config options */
}
mysql_declare_plugin_end;
这个定义经过preprocess被展开后定义为:
int _mysql_plugin_interface_version_= MYSQL_PLUGIN_INTERFACE_VERSION; \
int _mysql_sizeof_struct_st_plugin_= sizeof(struct st_mysql_plugin); \
struct st_mysql_plugin _mysql_plugin_declarations_[]= {
{ MYSQL_DAEMON_PLUGIN,
&daemon_example_plugin,
"daemon_example",
"Brian Aker",
"Daemon example, creates a heartbeat beat file in mysql-heartbeat.log",
PLUGIN_LICENSE_GPL,
daemon_example_plugin_init, /* Plugin Init */ // plugin初始化入口
daemon_example_plugin_deinit, /* Plugin Deinit */ // plugin清理函数
0x0100 /* 1.0 */,
NULL, /* status variables */
NULL, /* system variables */
NULL /* config options */
} , {0,0,0,0,0,0,0,0,0,0,0,0}};
静态链接plugin也类似,只不过plugin宏展开出来的变量都有自己的名字,对于myisam,生成了一个叫builtin_myisam_plugin的plugin数组。
plugin可以定义自己的变量,包括系统变量和状态变量。具体的例子可以看看storage/innobase/handler/ha_innodb.cc里面对于innodb插件的申明,结合plugin.h,还是比较容易看懂的。
在mysql的源代码里面grep一把mysql_declare_plugin,看看都有哪些plugin:
$grep "mysql_declare_plugin(" --include=*.cc -rni *
plugin/daemon_example/daemon_example.cc:187:mysql_declare_plugin(daemon_example)
sql/ha_partition.cc:6269:mysql_declare_plugin(partition)
sql/log.cc:5528:mysql_declare_plugin(binlog)
sql/ha_ndbcluster.cc:10533:mysql_declare_plugin(ndbcluster)
storage/csv/ha_tina.cc:1603:mysql_declare_plugin(csv)
storage/example/ha_example.cc:893:mysql_declare_plugin(example)
storage/myisam/ha_myisam.cc:2057:mysql_declare_plugin(myisam)
storage/heap/ha_heap.cc:746:mysql_declare_plugin(heap)
storage/innobase/handler/ha_innodb.cc:8231:mysql_declare_plugin(innobase)
storage/myisammrg/ha_myisammrg.cc:1186:mysql_declare_plugin(myisammrg)
storage/blackhole/ha_blackhole.cc:356:mysql_declare_plugin(blackhole)
storage/federated/ha_federated.cc:3368:mysql_declare_plugin(federated)
storage/archive/ha_archive.cc:1627:mysql_declare_plugin(archive)
呵呵,连binlog都是plugin哦,不过还是storage plugin占大多数。
Plugin初始化
在见面的介绍main函数的文章中我也提到了其中有个函数plugin_init()是初始化的一部分,这个东东就是所有静态链接初始化plugin的初始化入口。该函数定义在"sql/sql_plugin.cc"中。
int plugin_init(int *argc, char **argv, int flags) {
// 初始化内存分配pool。
init_alloc_root(&plugin_mem_root, 4096, 4096);
init_alloc_root(&tmp_root, 4096, 4096);
// hash结构初始化
...
// 初始化运行时plugin数组,plugin_dl_array用来保存动态加载plugin,plugin_array保存静态链接plugin。而且最多各自能有16个plugin。
my_init_dynamic_array(&plugin_dl_array, sizeof(struct st_plugin_dl *),16,16);
my_init_dynamic_array(&plugin_array, sizeof(struct st_plugin_int *),16,16);
// 初始化静态链接plugin
for (builtins= mysqld_builtins; *builtins; builtins++) {
// 每一个plugin还可以有多个子plugin,参见见面的plugin申明。
for (plugin= *builtins; plugin->info; plugin++) {
register_builtin(plugin, &tmp, &plugin_ptr); // 将plugin放到plugin_array和plugin_hash中。
// 这个时候只初始化csv或者myisam plugin。
plugin_initialize(plugin_ptr); // 初始化plugin,调用plugin的初始化函数,将plugin的状态变量加入到状态变量列表中,将系统变量的plugin成员指向当前的活动plugin。
}
}
// 根据用户选项初始化动态加载plugin
if (!(flags & PLUGIN_INIT_SKIP_DYNAMIC_LOADING))
{
if (opt_plugin_load)
plugin_load_list(&tmp_root, argc, argv, opt_plugin_load); // 根据配置加载制定的plugin。包括找到dll,加载,寻找符号并设置plugin结构。
if (!(flags & PLUGIN_INIT_SKIP_PLUGIN_TABLE))
plugin_load(&tmp_root, argc, argv); // 加载系统plugin table中的plugin。
}
// 初始化剩下的plugin。
for (i= 0; i < plugin_array.elements; i++) {
plugin_ptr= *dynamic_element(&plugin_array, i, struct st_plugin_int **);
if (plugin_ptr->state == PLUGIN_IS_UNINITIALIZED)
{
if (plugin_initialize(plugin_ptr))
{
plugin_ptr->state= PLUGIN_IS_DYING;
*(reap++)= plugin_ptr;
}
}
}
...
}
这个函数执行结束以后,在plugin_array,plugin_dl_array,plugin_hash中保存了当前加载了的所有的plugin。到此plugin初始化结束。
在plugin_initialize函数里面,调用了每个plugin自己的init函数(参见前面的内容)。特别要提到的是对于各种不同类型的
plugin,初始化函数的参数也不一样,这是通过一个全局的plugin_type_initialize间接层来实现的。这个数组对于每种类型的
plugin定义了一个函数,比如对于storage
plugin对应的是ha_initialize_handlerton,对于information
scheme对应的是initialize_schema_table,然后在这些函数中再调用plugin的初始化函数。暂时对于其他类型的
plugin没有定义这个中间层初始化函数,所以就直接调用了plugin的初始化函数。
Mysql源代码分析(6): Plugin架构介绍-续
上篇文章我们分析了Mysql的Plugin接口以及plugin的初始化过程,这里我们继续看plugin怎么被使用的。基本还是通过例子看问题,主要分析myisam如何通过plugin接口被调用的。
myisam是mysql最早的和默认的storage engine,前面我们也看到在plugin初始化的时候是优先初始化myisam,然后才初始化其他的存储引擎。这里我们假定用户要对一个myisam的表做操作,具体看看其中涉及的调用过程。
myisam的初始化
myisam plugin的定义可以在storage/myisam/ha_isam.cc中找到:
mysql_declare_plugin(myisam) {
MYSQL_STORAGE_ENGINE_PLUGIN,
&myisam_storage_engine,
"MyISAM",
"MySQL AB",
"Default engine as of MySQL 3.23 with great performance",
PLUGIN_LICENSE_GPL,
myisam_init, /* Plugin Init */
NULL, /* Plugin Deinit */
0x0100, /* 1.0 */
NULL, /* status variables */
NULL, /* system variables */
NULL /* config options */
}
mysql_declare_plugin_end;
初始化函数是myisam_init。在前面文章中提到,storage
engine类型的plugin均是通过ha_initialize_handlerton初始化。myisam_init的输入参数是void
*p,实际上是handlerton*。handlerton在mysql中封装了访问一个存储引擎需要的接口,每个存储引擎在全局空间有一个
handlerton对象,保存在对应的内存中plugin结构的data域中。该结构具体定义可以在sql/handler.h中找到。
myisam_init做的事情很简单,设置handlerton中的各个域,其中最重要的域是create,被指向了一个函数
myisam_create_handler,这个函数用来创建handler,用来对于数据库文件进行操作。
打开一个表
数据库表是数据库中所有操作的基础,我们看看打开一个表需要做些什么。当一个select命令进来的时候,sql_parse.cc中的
execute_sqlcom_select被执行,并被传入parse出来的所有该命令要用的到表。它会调用open_and_lock_tables
来打开指定的表,然后调用open_and_lock_tables_derived,再调用open_tables,再调用
open_table(sql_base.cc)。一大堆调用之后真正开始干实事儿的是open_unireg_entry,名字很奇怪,但是确实就是它
开始打开表了,我们仔细将仔细看这个函数,以及它调用的函数。这个函数很长,其实大部分都是在做错误处理,最重要的就以下几行:
static int open_unireg_entry(THD *thd, TABLE *entry, TABLE_LIST
*table_list, const char *alias, char *cache_key, uint cache_key_length,
MEM_ROOT *mem_root, uint flags) {
...
share= get_table_share_with_create(thd, table_list, cache_key,
cache_key_length, OPEN_VIEW |table_list->i_s_requested_object,
&error);
open_table_from_share(thd, share, alias, ...);
...
}
get_table_share_with_create是创建一个table_share结构,包括了同一个类型的表公用的数据结构,open_table_from_share则通过这个公用结构打开对应的要操作的表。
TABLE_SHARE *get_table_share(THD *thd, TABLE_LIST *table_list, ...) {
share= alloc_table_share(table_list, key, key_length)); //分配内存
my_hash_insert(&table_def_cache, (uchar*) share); // 加入cache,以后可以直接用
open_table_def(thd, share, db_flags); // 代开表的定义,需要读frm文件
}
open_table_def是用来打开存储表定义的文件。mysql中,每个表都有一个.frm文件,存储了表的定义,这个函数就是要打开表对应的frm文件,读入定义信息,填入TABLE_SHARE结构。
int open_table_def(THD *thd, TABLE_SHARE *share, uint db_flags) {
file= my_open(path, O_RDONLY | O_SHARE, MYF(0))
open_binary_frm(thd, share, head, file);
}
open_binary_frm读入二进制的frm文件信息。这个函数超长,但是我们暂时只是对与plugin相关的部分感兴趣。因为每个表的storage engine信息就是从frm文件中读出来的,我们看相关的代码片段:
open_binary_frm(...) {
...
plugin_ref tmp_plugin= ha_resolve_by_name(thd, &name); // name就是storage engine的名字,比如"myisam"。这里根据名字找到对应的plugin。
share->db_plugin= my_plugin_lock(NULL, &tmp_plugin); //
保存plugin的引用,供以后使用。plugin中的"data"域就是handlerton*,这将是主要的使用plugin的入口。
...
}
好了,TABLE_SHARE设置好了,我们回到open_unireg_entry中,继续看open_table_from_share。这才是真正
打开表的地方。这个函数还是在sql/table.cc中。这个函数还是超长...,万幸的是我们还是只想关注plugin相关的内容。TABLE中有一
个file结构,类型是handler*,我们以前提到过,handler就是一个打开的表的引用,显然open_table_from_share的责
任之一就是要设置这个域。
int open_table_from_share(THD *thd, TABLE_SHARE *share, ... TABLE *outparam, ...) { // outparam是打开后的表信息。
...
outparam->file= get_new_handler(share, &outparam->mem_root,
share->db_type())); // 直奔主题,获取一个handler。
share->db_type()返回plugin对应的handlerton,其实就是将plugin->data强制转换成
handlerton.
...
outparam->file->ha_open(outparam, ...); // 调用plugin的handler定义的open函数,做自定义的open操作。
...
}
get_new_handler负责根据TABLE_SHARE的内容构造一个handler对象。这个函数在sql/handler.cc中。
handler *get_new_handler(TABLE_SHARE *share, MEM_ROOT *alloc, handlerton *db_type) {
file= db_type->create(db_type, share, alloc); // 调用plugin的create函数,来创建handler成员。
file->init();
}
前面我们提到过对于myisam对应的create函数是myisam_create_handler,这个函数就是new了一个ha_myisam对象,而ha_myisam又是从handler继承下来的,重载了handler对应的函数。
这样一个对于应数据库表文件的handler就可以使用啦,它的第一个使用就是在open_table_from_share中被调用ha_open。
ha_open在handler.cc中定义,其实就是调用了重载后了open函数。在ha_myisam中,我们可以看到open函数的定义,这里我们
就不仔细看了,实现细节和myisam的文件结构相关。
看到这里一个"SELECT * from test"语句如何打开表的部分就基本清楚了,主要过程包括:
从frm文件寻找storage engine的名字,并获取对应的storage engine plugin的handlerton
调用handlerton的create函数生成handler
通过handler重载的open函数打开表文件 挺清楚的。
到了这里,我们就有了表的handler,以后凡是涉及到存储引擎的操作,都通过这个接口调用来做,这样,storage engine plugin就和mysql核心紧密结合到了一起,各司其事,共同完成复杂的sql操作。
Mysql源代码分析(7): MYISAM的数据文件处理
好久没写分析文章了,一个是比较忙,另一个是因为余下的内容都是硬骨头,需要花时间慢慢理解。剩下的比较有意思的内容有:
select语句的执行和优化过程。大家关心数据库的查询性能,主要是对着部分比较感兴趣,特别是其中的查询优化部分。
Mysql的replication。Mysql的master/slave架构是大部分使用mysql的高性能网站架构的不二选择,replication则是这个架构的基础。
具体数据库引擎的实现。这部分也是很多关心mysql性能的人会比较感兴趣的部分,不过这个工作比较复杂,特别是流行的innodb,这个工作量尤其浩大,而且难度颇高。其中涉及到transaction的部分,也是特别复杂。
另外,我发现我写的文章被一些地方转摘了,感谢大家的阅读,但是我也希望转摘要注明出处,至少给个原文链接吧,也不枉我幸苦一场。
今天主要写写Myisam的数据文件的处理。
Myisam是最早实现的Mysql数据库引擎,也是人们心中的性能最好的引擎(虽然不是功能最强的,没办法,现实往往要求性能和功能做权衡)。这里选择
分析它,主要原因是其实现还算比较简单明了,而且最近我对数据文件的格式比较感兴趣,特别是变长数据的处理。要注意的是本文不会介绍myisam的索引文
件格式。
基本知识
对于每一个以Myisam做数据引擎的表,在<%data_dir%>/<database>目录下会有如下几个文件来保存其相关信息:
.frm文件。 这个文件是跨引擎的,描述了该表的元信息,其中最重要的是表定义和表的数据库引擎。
.MYD文件。这是我们要看的重点文件,包含了数据库record信息,就是数据库中的每个行。
.MYI文件。索引文件,用来加速查找。
而对于MYD中的每个record,可以是fixed,dynamic以及packed三种类型之一。fixed表示record的大小是固定的,没有
VARCHAR,
blob之类的东东。dynamic则刚好相反,有变长数据类型。packed类型是通过myisampack处理过的record。参
见:http://dev.mysql.com/doc/refman/5.1/en/myisam-table-formats.html。
需要注意的是record类型是针对表的设置,而不是对每个column的设置。
record处理接口
record的类型是表级别的设置,所以在一个表被打开的时候,myisam会检查元数据的选项,看该表的record是什么类型,然后设置对应的处理函
数,具体处理在storage/myisam/mi_open.c的mi_setup_functions中,我们看其中的一个片段:
746 void mi_setup_functions(register MYISAM_SHARE *share)
747 {
....
759 else if (share->options & HA_OPTION_PACK_RECORD)
760 {
761 share->read_record=_mi_read_dynamic_record;
762 share->read_rnd=_mi_read_rnd_dynamic_record;
763 share->delete_record=_mi_delete_dynamic_record;
764 share->compare_record=_mi_cmp_dynamic_record;
765 share->compare_unique=_mi_cmp_dynamic_unique;
766 share->calc_checksum= mi_checksum;
767
768 /* add bits used to pack data to pack_reclength for faster allocation */
769 share->base.pack_reclength+= share->base.pack_bits;
770 if (share->base.blobs)
771 {
772 share->update_record=_mi_update_blob_record;
773 share->write_record=_mi_write_blob_record;
774 }
775 else
776 {
777 share->write_record=_mi_write_dynamic_record;
778 share->update_record=_mi_update_dynamic_record;
779 }
780 }
...
这是针对pack类型的处理函数设置。设置了share结构中的一堆函数接口。顺便说一句,这种方式是C语言编程中常用的实现”多态“的办法:申明函数接
口,动态设置接口实现,思想上和C++的动态绑定是一致的。这段代码对于dynamic类型的表的record处理函数做了设置。比较有趣的是
HA_OPTION_PACK_RECORD用来指定dynamic类型。
看到这些函数名大家可以猜想出他们都是干嘛的,下面主要看看fixed类型和dynamic类型的具体处理。
Fixed类型
顾名思义,fixed类型的表中的所有字段都是定长的,不能出现TEXT, VARCHAR之类的东东。这种严格限制带来的好处就是更快更直接的数据record操作,想想也知道,每个数据都是定长的,在文件操作的时候多方便啊。
看看一个数据的函数_mi_write_static_record,它在mi_statrec.c中,所有对于fixed record的操作的实现都定义在这个文件中。
21 int _mi_write_static_record(MI_INFO *info, const uchar *record)
22 {
...
24 if (info->s->state.dellink != HA_OFFSET_ERROR &&
25 !info->append_insert_at_end)
26 {
检查dellink中是否有record。dellink是所有被删除的数据构成的链表。当一个record被删除的时候,它
所占的文件大小不是被马上释放,而是被放入dellink中,等候下次使用。
27 my_off_t filepos=info->s->state.dellink;
读入dellink所指向的数据空间的信息。
33 更新dellink,将使用了的数据空间移除。
将record写入找到的已删除的数据的空间中。
40 }
41 else
42 {
43 检查数据文件是否过大。
49 如果使用的写缓冲,则写入写缓冲。
将新数据写入文件最后。
更新元数据。
...
86 }
因为所有的数据都是一样大小,处理起来很简单。特别是当一个数据被删除的时候,它所占的空间被放入一个回收链表中,下次要写入新数据的时候,如果回收链表不为空,直接从其中找一个写入新数据即可,不用分配新的存储空间。
Fixed类型的其他处理也都很简单,这里不再多说了。需要提出的是,不管用的什么类型的数据,当数据被删除的时候,其所占的空间并不是马上被释放的,那
样操作代价太大,要把该数据后面的所有数据向前移位,肯定无法忍受。一般的做法都是将这些空间用链表穿起来,供以后使用,所以数据文件一般是不会主动缩小
的.....即使是innodb也是这样。
Dynamic类型
Dynamic类型是相对于fixed的类型而言,这种类型可以容忍变长数据类型的存在。随之而来的是更复杂的数据文件的操作。Dynamic类型中被删
除的数据块也不是马上被释放,也被链表连起来。下次要写入新数据的时候,还是优先从这个链表中找。不同于fixed类型的处理在于新来的数据和链表中的空
间的大小可能不一样。如果新数据大了,就会找好几个空余空间,将数据分散于多个数据块中,如果新数据小了,则会将空余数据块分成两个,一个写入新数据,一
个还是放在空余链表中供后来者使用。
看一下mi_dynrec.c中的write_dynamic_record函数。
320 static int write_dynamic_record(MI_INFO *info, const uchar *record,
321 ulong reclength)
322 {
检查是否有足够的空间来存放新数据,空间满了返回错误。
351
352 do
353 {
// 找一个可以写入数据的地方。注意这里是在一个循环里面,也就是说每次找到的
// 空间不一定能够写入整个数据,只能写入部分的话,剩下的还要继续找地方写。
354 if (_mi_find_writepos(info,reclength,&filepos,&length))
355 goto err;
// 写入能够放入找到的空间的数据。
356 if (_mi_write_part_record(info,filepos,length,
357 (info->append_insert_at_end ?
358 HA_OFFSET_ERROR : info->s->state.dellink),
359 (uchar**) &record,&reclength,&flag))
360 goto err;
361 } while (reclength);
...
}
其中的循环说明了一切,很有可能一个数据会被分成几块儿,写到不同的地方,但是他们合起来才构成了整个数据。
再看_mi_find_writepos。
371 static int _mi_find_writepos(MI_INFO *info,
372 ulong reclength, /* record length */
373 my_off_t *filepos, /* Return file pos */
374 ulong *length) /* length of block at filepos */
375 {
376 MI_BLOCK_INFO block_info;
...
// 先检查dellink中是否有空余的空间。
380 if (info->s->state.dellink != HA_OFFSET_ERROR &&
381 !info->append_insert_at_end)
382 {
383 /* Deleted blocks exists; Get last used block */
存在空余空间,那就把链表中的头找出来,把其中的空间用来写入新数据。
将这块空间的描述返回给调用者。
....
398 }
399 else
400 {
401 /* No deleted blocks; Allocate a new block */
没有已删除的空间,那就在数据文件的最后分配空间,并返回给调用者。
421 }
...
}
如果有已删除的空间的话,那就直接把链表头描述的空间返回。这个算法很简单,但是我觉得这样简单的算法可能会赵成一些问题,比如存储的碎片化,一块儿大空
间被切的越来越小,到后来写入一个数据要使用好几个空间。这些问题在操作系统的内存管理中也同样存在,所以产生了大量的内存管理算法,这里也应该可以借用
吧。
具体的写入是在_mi_write_part_record中完成的。这个函数比较长,我就直接简写如下了。
int _mi_write_part_record(MI_INFO *info,
my_off_t filepos, /* points at empty block */
ulong length, /* length of block */
my_off_t next_filepos,/* Next empty block */
uchar **record, /* pointer to record ptr */
ulong *reclength, /* length of *record */
int *flag) /* *flag == 0 if header */
{
如果给出的空间空间大于数据长度的话,计算填完数据后剩余的空间。
如果空间刚好,准备一些元数据。
如果空间太小,则找到下一个写入空间的位置(要么是下一个dellink,要么是文件末尾),并准备这些元数据。如果是第一部分的数据的话,要写入更多的信息。
如果空间太大,有剩余空间的话,先看这个空间能否与和下一个空闲空间连接起来形成一个大空间,如果能的话就合并。将其相关的元数据,比如空间的位置,大小之类的,准备好。
开始写数据罗,如果启用了写缓冲,则写入缓冲,否则写入找出来的空间。
更新dellink的相关信息。
}
逻辑很清楚,主要是要处理空间过大或者过小带来的复杂性。
好了,到了这里大部分的处理都很清楚了,还是很直接的。剩下的就是在删除一个数据的时候,将其所占的空间放到dellink中,要注意的是,如果其数据块 可以和dellink中的其他数据块合并,合并操作也是在删除数据的操作中调用的,而且合并出来的数据块还可能和其他数据块继续合并