表对象缓存是将某个表对象的字典信息缓存到内存中,用来提高访问表的效率。

表字典对象的缓存是通过HASH表来管理的,Mysql系统中,专门有一个HASH表(源码中是table_def_cache)
用来存储组织表对象,通过表的名字来构建Key值,用来搜索对象。

对于表对象的缓存,并不是所有用户共用一个缓存对象,而是通过实列化出一个对象,只能有自己使用。
它的缓存用到了“共享私有化缓存”,它在缓存中用到一个TABLE_SHARE的结构体,这个结构体唯一对应MySQL中的一个表对象,不区分搜索引擎。

在打开一个表的时候,这个表首先是在Mysql的系统表中存储的。这里的系统表,分两个层次,一个层次是mysql的.frm文件,这是共有的,与存储引擎没有关系。也属于一种系统表。另一层就要分不同的存储引擎,不同的存储引擎有自己的系统表。

用户要访问一个表,之构造了TABLE_SHARE是远远不够的,而且这个结构体也不是直接供用户使用的。构造了这个结构体之后,首先需要将其缓存起来,因为这个结构体就是要缓存的对象。

具体操作流程

先通过hash算法,在table_def_cache中找到这个TABLE_SHARE对象,如果找到了,先判断是否有用户在
使用,如果有,那就需要等待。如果没找到,说明当前没有缓存过该表对象,就需要从数据字典中找到这个表,并且转换为TABLE_SHARE,再将其加入到hash表中。

当系统得到一个SHARE对象之后,系统会重新再构造一个新的对象 交给当前的操作,而这个对象肯定不是TABLE_SHARE,因为它是缓存对象,是静态的、只 读的,真正与用户交互的是TABLE_SHARE的一个衍生品,它对应的结构体名字为TABLE, 它是真正在操作中被使用的对象。那是如何从TABLE_SHARE变为TABLE的呢?可以先把 这个构造过程称为实例化。

从代码中可以看到,其实这两个结构体的很多成员是相同的,并且可以直接复制过去,因TABLE_SHARE是一个静态的缓存对象,所以相对而言,TABLE就可以称作一个相对动 态的、正在进行一些操作的实例了。TABLE中有一个成员就是直接指向了TABLE.SHARE; 还有一些成员比如record,是用来构造插入操作中的一条记录的,在实例化的时候,会根据 这个表定义的每一个列及其数据类型等提前构造好;field用来存储这个表中所有的列信息, 这些信息其实是完全将SHARE中的信息克隆过来的。其他的一些小细节就不叙述了,不过 还有两个很重要的东西必须要说一下。
第一,因为上面已经提到了,TABLE这个对象是一个动态的、被实例化的对象,它相当于是 一个被打开的表,它已经不仅仅是MySQL Server层的对象了,而是具体到某一个存储引擎 了,所以这里还需要构造这个对象有关存储引擎的信息,并且打开这个表。

第二,因为MySQL是一个插件式的数据库管理系统,对于表对象的管理,MySQL层与存储 引擎层就是在这里分开的。TABLE算是它们之间的桥梁,下层是存储引擎,上层就是MySQL 了。对于MySQL的存储引擎,都要提供一些公共的接口来驱动其存储引擎,这些接口都是 给上层调用,来操作对应的存储引擎的,也可以被称作MySQL与存储引擎之间交流的通道。

在被实例化之后,这个表对象就可以直接与存储引擎进行交互。比如插入一条记录,直接调 用TABLE已经被实例化的存储引擎句柄的接口函数ha_write_row即可。
当一个操作完成之后,它所实例化的表就不需要了,此时系统不是将这个本地的实例直接 释放掉,而是将其保存下来。保存下来是为了下次某一个用户再次访问这个表的时不需要 再次进行实例化,直接拿过来用即可,当然,可能需要一些额外的操作,比如将实例状态恢 复,调用函数ha.reset即可。
系统在保存一个不使用的实例化对象时,是直接将其放在SHARE的一个free_tables链表 中,但首先要从used_tables链表上摘下来,这两个链表都是用来保存这个表的所有实例的, used_tables用来存储正在使用的实例,free_tables用来存储所有当前未使用的实例。在并发 比较高的情况IL可能在used_tables中有多个,在free_tables中却没有,但在全部执行完成 之后则相反,那么如果此时有用户再操作这个表,系统可以直接从free_tables中找一个来用 即可。
现在可以知道,在MySQL中,表对象的缓存其实是用两部分。一部分是SHARE的缓存,也 就是多个不同表的SHARE对象的缓存;另一部分就是每一个SHARE结构被实例化之后的实 例对象的缓存,MySQL用来管理缓存空间大小的方法是通过计数来实现的。默认情况下,系 统中总的 SHARE 个数不能超过table_definition_cache (对应参数table_definition_cache)个。
上面提到的都是关于表对象SHARE结构的缓存,既然是缓存,肯定有它相应被删除或淘汰 的问题,当然在这里也不例外。那么,在什么情况下SHARE结构会被淘汰或删除呢?很明 显,如果只是对这个表进行增删改等没有涉及修改表定义的操作,SHARE是不会被删除的, 只有可能会被淘汰,因为如果査询太多表的话,表对象缓存个数是有限制的,当到达这个数 目之后,系统会自动将一些不经常使用的SHARE淘汰掉,这很容易理解。
一般情况下,只要对表结构、依赖关系、表定义等方面进行过修改,这个表对象的缓存SHARE 对象就必须要从缓存中删除,同时要删除它上面所有被实例化的表对象缓存结构,因为这 个表的版本被更新了,如果继续将其缓存的话,是不安全的,或者是错误的,又或者会导致 一些不可预知的问题。这样,其他用户等待表对象的修改操作完成之后(因为修改过程中这 个表是被上了锁的,进行操作需要等待),会又一次像前面所讲的一样,首先是从缓存中找 这个表的缓存对象,如果找不到的话,再从数据字典(系统表)中读取进来,然后继续操作。

涉及的参数变量

与表对象缓存相关的参数,包括两个,分别是table_open_cache和table_dehnition_cache。这 一节详细讲述它们之间的关系及需要注意的一些细节。
前面已经讲到,表的缓存实际上是有两层的。从文件开始往上,第一层是数据字典的缓存, 是通过.frm文件内存化之后,被缓存起来的对象,也就是上面所说的SHARE对象的缓存, 其空间大小通过参数table.definition.cache来控制,以表的个数为单位。第二层就是将这些
SHARE对象实例化并打开之后表所占用的缓存空间,其大小通过参数table.open.cache来 控制,以表的实例化个数为单位。
如果一个实例中的表非常多,则可以通过设置一个比较大的表定义缓存空间来加速对表的 处理。现在可以看到,这个缓存空间所缓存的SHARE对象,实际上比已打开表的缓存对象TABLE占用空间小一些,这是因为一个SHARE对象可以被多个线程实例化。
table_definition_cache参数的最小值为400,不过这个参数的设置与table_open_cache之间有 一定的关系,默认情况下,它们之间的换算方法如下。

从代码可以看到,table.definition.cache通过这样的方法计算时,最大值为2000,也就是说, 如果 table_open_cache (对应代码中的table_cache_size)大于3200 (因为3200/2+400 的值为2000),则table_definition_cache 就不会再跟着table_open_cache 的增长而增长 了。
而参数table_cache_size,也是通过一系列复杂的计算得来的,它与参数innodb_open_hles、先来看这些参数的计算逻辑顺序关系,如下。

优缺点总结
本节所讲述的MySQL表的缓存机制有如下很多优点。
-相比全字典缓存(全字典缓存的意思是在数据库启动时将所有的数据字典信息都一次性 载入到内存中来,这样在使用过程中的效率非常高,但在DDL操作方面有很大的不足), 它的触发时机是用到的时候才载入缓存,在被修改之后,会将其从缓存中删除,以后用 到的时候,再次载入,这样的实现方式降低了 DDL操作或回滚导致的字典缓存维护工 作的代价。
•有效地利用了内存空间,因为可以通过设置表对象缓存空间的大小来控制内存的使用情 况,同时只有用到的对象才会被载入到内存中,提高了内存的利用率。
上面所述的MySQL表缓存实现方案虽说是比较先进的,但也有一些缺点,如下。
-该缓存实现方案在效率方面还是有些优化空间的。比如上面提到的,控制缓存空间大 小是根据实例化表对象的个数来计算的,在系统中默认最大值是table_open_cache,如 果超过这个值系统会自动淘汰一些不常用的实例化表对象。但是如果一个表的定义非 常大,那么在并发情况下,就有可能会建立很多个实例化表对象,假设对象个数接近 table_open_cache,那么这样算下来有可能会将操作系统的内存用光,这是不可控制的, 也是不可预期的。对于SHARE的缓存也是一样,如果一个用户访问了很多不同的定义 或很大的表,也会有同样的问题。
・从前面的讲述也可以看出,为了实现插件式的数据库,其实还是有一些效率的代价的。在
表的缓存方面,中间加入了一层SHARE的缓存,真正用到的时候还需要实例化,因为每
一个用户的操作及不同时间的状态都是不同的,所以每一个用户必须要在SHARE的基
础上实例化一个新的对象出来,这样就给内存、CPU带来了一定程度上的浪费及压力。
存在的问题
此外,该缓存方案也存在一些问题,如下。
• SHARE缓存:个人认为有一个更好的办法来很精确地通过具体的空间大小来管理表缓 存空间。因为SHARE缓存对象是静态的,是个结构体,通过使用计数来控制内存的使 用,有可能会造成内存用光的情况。那么对于SHARE对象,完全可以把它流式化(扁 平化),也就是说把这个结构体的大小计算出来以后,申请相应的空间,将结构体中的 所有信息都按照固定的顺序写入这块内存中,这样,一个SHARE所占的空间大小就固 定了,便可以完全通过设置表缓存空间大小来管理表对象缓存了,那么上面提到的内存 用光的问题就自然解决了。当然,这个缓存空间大小需要根据计算机的内存大小进行合 理的设置,避免出现不可预料的问题。
• TABLE缓存:TABLE实例的缓存同样存在上面的问题,解决方案与上面的思想差不多。
因为这个对象是一直被用的,它是一个实例,所以就不能直接像上面一样,将其流式化,
而是可以通过申请一片连续的空间,这个实例中的所有指针或其成员的值都指向(有可
能要对齐)这个空间中的指定位置,如此,这个结构体的使用没有任何改变,但其占用 的空间大小是固定的,同样可以通过用户手动设置TABLE实例缓存空间的大小来管理
缓存空间,也避免了表定义太大导致内存用光的问题。