本文主要是简介一下mysql中关于table cache的代码实现。
1. 背景知识
mysql大体上可以分为两层,sql层和 引擎层。mysql 中的 table cache 位于sql 层。
table cache的作用:
The table cache holds descriptors for open tables. For frequently used tables, keeping the descriptors in the cache avoids having to open the tables again and again.
2. table cache的组织结构 与 概述
如图1所示,在mysql中基于同一个物理表的所有表实例都共用一个table share对象。 这些表实例可以是正在被使用,也可以处于空闲状态。
图1
如图2所示,table share对象维护2个链表,free(图中黑色)和used(图中绿色)。分别表示空闲的实例链表和正在被使用的实例链表(都是双向环形链表)。对于处于空闲状态的表实例,它们还处在全局global的unused双向环形链表中(图中为红色)。 另外对于没有使用中的table share, 它会放到一个全局的unused_table_share中(图中没有画出,因为这是一个使用中的table share,它有)
图2
在table cache中缓存的是每个物理表的table share对象, 缓存的key是 db_name + \0 + table_name + \0 .
对于临时表key还有些特殊处理,具体代码可以参考sql/sql_base.cc的create_table_def_key函数(226行)。
流程:
1. 当mysql需要打开一个表时(如执行query中要用到的表),它会先去table cache中查找这个表对应的table share对象。若存在跳到步骤3.
2. 若不存在, 根据物理表创建一个新的table_share并分配table id之后放入table cache中。(如果空间不足,会删除最近最少使用的table_share对象)
3. 把这个table share从全局unused_table_share双向环形链表中移出(如果在的话)。查看table_share对象的free链表是否有空闲的。若有跳到步骤5。
4. 若free链表为空,那么根据table_share对象生成一个新的table实例。share的引用计数ref_count++。同时检查此时全局缓存的实例数是否超标,若超,则关闭最近最少使用的实例(见步骤8)。
5. 把找到的table实例从free链表和全局unused链表移动到used链表中。
6. 返回这个实例
7. 关闭表时把所使用的表对象放回到free和unused链表末尾。
8. 若这个时候全局缓存的表实例数量超标,根据LRU(最近最少使用),把unused链表的第一个表实例释放,同时把对应的share上的引用计数ref_count--。当所有引用都释放时把table share加回全局unused_table_share双向环形链表末尾, 如果缓存中的数量已经大于某个特定值,那么根据LRU(最近最少使用),删除unused_table_share第一个
3. 从table cache中打开表的代码
代码入口在 open_and_lock_tables (sql/sql_base.cc 5453行)。
这个函数最终会调用到函数open_table (sql/sql_base.cc 2600行).
首先先生成key :
可以看出:key是 db_name + \0 + table_name + \0 .
跳过临时表,锁表,与视图的处理。
获取share对象
从缓存找查找对应的table_share, 与这个操作关联的锁是LOCK_open.
这个函数主要调用了另一个函数get_table_share (sql/sql_base.cc 497行)来从缓存中取share,若没有,新建之。
在get_table_share函数中首先尝试从缓存中查找:
这里table_def_cache是作用缓存的hash结构,用于保存打开的物理表对应的share。 把找到的share引用计数加1,如果这个share之前处于空闲状态,那么把它从全局unused_table_share 中移出。
如果不存在那么新建一个,分配表id,并根据物理表进行初始化。最后给share的引用加1.
注: 无论那种情况,只要最后找到share,那么这个share的引用计数都加一。 但并不是每次都会怎么share的表实例数,对于最终没有产生新实例的情况,share的引用数不应该加1,所以后面有代码处理这种情况。
获取表实例:
首先检查这个share的空闲表链表是否有空闲表实例,如果有把这个实例从“空闲表“链表free和“全局未使用表”链表unused中移出,并添加到share的 ”已使用表“链表used中. 最后把share中引用计数减1,因为没有生成新的表实例。
release_table_share函数的功能是把share的引用计数减1,如果计数为0,那么把这个share放入全局的unused_table_share链表中,如果链表中share数量太多,那么把最近最少使用的删除。具体如下图:
如果free链表为空,那么就需要新建一个table实例了:
首先先看看是否缓存已超标,如果超标释放掉全局unused链表的第一个对象(最近最少使用),然后根据share对象创建新的表实例,并添加到share的used链表中,全局实例数++。
4. 关闭表的代码
关闭表的代码入口是close_thread_tables函数 (sql/sql_base.cc 1386行)。
这个函数最终会调用函数close_thread_table。
以下是该函数中关闭表的处理,忽略了关于reopen的处理,只看实际关闭表的代码:
通过调用table_def_unuse_table函数把这个表从share的used链表中删除,并添加到free链表和全局unused链表中。
如果此时实例数超标,那么调用free_cache_entry(unused_tables) 来吧最近最空闲的实例删除。
在free_cache_entry(unused_tables)中首先通过table_def_remove_table从所有链表中把这个表删除,把全局实例计数--。
然后调用intern_close_table函数释放table所用的资源。首先是io上的缓存,然后是触发器,最后调用closefrm关闭这个表实例。
在closefrm函数中,通过调用release_table_share来减少share的引用计数。如果计数最终为0的话把这个share放入unused_table_share链表中,并根据需要释放最近最少使用的shar(该函数主要代码在上一节中有介绍)。
5. table cache的更新或版本控制
table cache缓存的只是表的结构。 根据目前已读的代码,有2中可能导致cache的更新:
1. FLUSH TABLE <table_list> 命令
2. ALTER TABLE 命令
对于 FLUSH TABLE 而言,它会关闭指定物理表的所有free实例(如果所有实例都没有了share一起个干掉)。它会把share里的version字段设为0,这样share->has_old_version()会返回1,表明这个share的已经不是最新版本了。
这个功能的实现代码为函数 flush_tables_with_read_lock (sql/sql_reload.cc 369行)。
可以看出该函数调用了 tdc_remove_table (sql/sql_base.cc 8762行)函数。
函数中依次调用了:
I_P_List_iterator<TABLE, TABLE_share> it(share->free_tables); // 取所有free的表实例
share->version= 0; //把share的版本设为0
while ((table= it++))
free_cache_entry(table); //释放所有free的表实例
关于free_cache_entry函数,之前的章节有介绍。
对于那些还在used的实例, 当它们检测到share->has_old_version()为1时,会通过调用 tdc_wait_for_old_version函数(sql/sql_base.cc 2541行)来等待share的版本更新,然后重新获取share。
由上面的代码可知,该函数调用了share->wait_for_old_version,该方法会更新share。
对于 ALTER TABLE而言,入口函数为mysql_alter_table (sql/sql_table.cc 5862行),这个函数的逻辑就不介绍了(太大了!)。
它里面会调用close_all_tables_for_name函数(sql/sql_base.cc 1323行),来把旧的缓存清空。
1. 背景知识
mysql大体上可以分为两层,sql层和 引擎层。mysql 中的 table cache 位于sql 层。
table cache的作用:
The table cache holds descriptors for open tables. For frequently used tables, keeping the descriptors in the cache avoids having to open the tables again and again.
2. table cache的组织结构 与 概述
如图1所示,在mysql中基于同一个物理表的所有表实例都共用一个table share对象。 这些表实例可以是正在被使用,也可以处于空闲状态。
如图2所示,table share对象维护2个链表,free(图中黑色)和used(图中绿色)。分别表示空闲的实例链表和正在被使用的实例链表(都是双向环形链表)。对于处于空闲状态的表实例,它们还处在全局global的unused双向环形链表中(图中为红色)。 另外对于没有使用中的table share, 它会放到一个全局的unused_table_share中(图中没有画出,因为这是一个使用中的table share,它有)
在table cache中缓存的是每个物理表的table share对象, 缓存的key是 db_name + \0 + table_name + \0 .
对于临时表key还有些特殊处理,具体代码可以参考sql/sql_base.cc的create_table_def_key函数(226行)。
流程:
1. 当mysql需要打开一个表时(如执行query中要用到的表),它会先去table cache中查找这个表对应的table share对象。若存在跳到步骤3.
2. 若不存在, 根据物理表创建一个新的table_share并分配table id之后放入table cache中。(如果空间不足,会删除最近最少使用的table_share对象)
3. 把这个table share从全局unused_table_share双向环形链表中移出(如果在的话)。查看table_share对象的free链表是否有空闲的。若有跳到步骤5。
4. 若free链表为空,那么根据table_share对象生成一个新的table实例。share的引用计数ref_count++。同时检查此时全局缓存的实例数是否超标,若超,则关闭最近最少使用的实例(见步骤8)。
5. 把找到的table实例从free链表和全局unused链表移动到used链表中。
6. 返回这个实例
7. 关闭表时把所使用的表对象放回到free和unused链表末尾。
8. 若这个时候全局缓存的表实例数量超标,根据LRU(最近最少使用),把unused链表的第一个表实例释放,同时把对应的share上的引用计数ref_count--。当所有引用都释放时把table share加回全局unused_table_share双向环形链表末尾, 如果缓存中的数量已经大于某个特定值,那么根据LRU(最近最少使用),删除unused_table_share第一个
3. 从table cache中打开表的代码
代码入口在 open_and_lock_tables (sql/sql_base.cc 5453行)。
这个函数最终会调用到函数open_table (sql/sql_base.cc 2600行).
首先先生成key :
可以看出:key是 db_name + \0 + table_name + \0 .
跳过临时表,锁表,与视图的处理。
获取share对象
从缓存找查找对应的table_share, 与这个操作关联的锁是LOCK_open.
这个函数主要调用了另一个函数get_table_share (sql/sql_base.cc 497行)来从缓存中取share,若没有,新建之。
在get_table_share函数中首先尝试从缓存中查找:
这里table_def_cache是作用缓存的hash结构,用于保存打开的物理表对应的share。 把找到的share引用计数加1,如果这个share之前处于空闲状态,那么把它从全局unused_table_share 中移出。
如果不存在那么新建一个,分配表id,并根据物理表进行初始化。最后给share的引用加1.
注: 无论那种情况,只要最后找到share,那么这个share的引用计数都加一。 但并不是每次都会怎么share的表实例数,对于最终没有产生新实例的情况,share的引用数不应该加1,所以后面有代码处理这种情况。
获取表实例:
首先检查这个share的空闲表链表是否有空闲表实例,如果有把这个实例从“空闲表“链表free和“全局未使用表”链表unused中移出,并添加到share的 ”已使用表“链表used中. 最后把share中引用计数减1,因为没有生成新的表实例。
release_table_share函数的功能是把share的引用计数减1,如果计数为0,那么把这个share放入全局的unused_table_share链表中,如果链表中share数量太多,那么把最近最少使用的删除。具体如下图:
如果free链表为空,那么就需要新建一个table实例了:
首先先看看是否缓存已超标,如果超标释放掉全局unused链表的第一个对象(最近最少使用),然后根据share对象创建新的表实例,并添加到share的used链表中,全局实例数++。
4. 关闭表的代码
关闭表的代码入口是close_thread_tables函数 (sql/sql_base.cc 1386行)。
这个函数最终会调用函数close_thread_table。
以下是该函数中关闭表的处理,忽略了关于reopen的处理,只看实际关闭表的代码:
通过调用table_def_unuse_table函数把这个表从share的used链表中删除,并添加到free链表和全局unused链表中。
如果此时实例数超标,那么调用free_cache_entry(unused_tables) 来吧最近最空闲的实例删除。
在free_cache_entry(unused_tables)中首先通过table_def_remove_table从所有链表中把这个表删除,把全局实例计数--。
然后调用intern_close_table函数释放table所用的资源。首先是io上的缓存,然后是触发器,最后调用closefrm关闭这个表实例。
在closefrm函数中,通过调用release_table_share来减少share的引用计数。如果计数最终为0的话把这个share放入unused_table_share链表中,并根据需要释放最近最少使用的shar(该函数主要代码在上一节中有介绍)。
5. table cache的更新或版本控制
table cache缓存的只是表的结构。 根据目前已读的代码,有2中可能导致cache的更新:
1. FLUSH TABLE <table_list> 命令
2. ALTER TABLE 命令
对于 FLUSH TABLE 而言,它会关闭指定物理表的所有free实例(如果所有实例都没有了share一起个干掉)。它会把share里的version字段设为0,这样share->has_old_version()会返回1,表明这个share的已经不是最新版本了。
这个功能的实现代码为函数 flush_tables_with_read_lock (sql/sql_reload.cc 369行)。
可以看出该函数调用了 tdc_remove_table (sql/sql_base.cc 8762行)函数。
函数中依次调用了:
I_P_List_iterator<TABLE, TABLE_share> it(share->free_tables);
share->version= 0;
while ((table= it++))
关于free_cache_entry函数,之前的章节有介绍。
对于那些还在used的实例, 当它们检测到share->has_old_version()为1时,会通过调用 tdc_wait_for_old_version函数(sql/sql_base.cc 2541行)来等待share的版本更新,然后重新获取share。
由上面的代码可知,该函数调用了share->wait_for_old_version,该方法会更新share。
对于 ALTER TABLE而言,入口函数为mysql_alter_table (sql/sql_table.cc 5862行),这个函数的逻辑就不介绍了(太大了!)。
它里面会调用close_all_tables_for_name函数(sql/sql_base.cc 1323行),来把旧的缓存清空。
本文深入解析MySQL中的Table Cache机制,包括其背景、组织结构、工作流程及更新方式等内容,帮助理解如何高效管理和利用表资源。

820

被折叠的 条评论
为什么被折叠?



