MySQL源码:MYSQL表对象的字典缓存
2012-05-15 21:16 竹 石 阅读(800) 评论(0) 编辑 收藏 举报前言:很久又没有写一些东西了,现在又有些空想把我对表对象缓存的一些理解记下来,可能其中有些不对或者不准确的地方请朋友指正,这里先谢谢大家了。
2012-5-13 by whuai QQ:329570985 欢迎指正!
在MYSQL中,同样有很多类型的系统对象,包括表、视图、存储过程、存储函数等,但由于MYSQL的插件式存储引擎及其它实现方面的特点,其每一种对象的缓存方式都不同,或者说这些对象的缓存不是通过一种统一的方式来管理的,每一种对象的缓存都是有自己的特点,并且缓存的内容也有很大的差异,下面先只叙述一下表对象的缓存方式。
表字典对象缓存,顾名思义,是将某个表对象的字典信息(定义内容)缓存到内存中,用来提高对表访问的效率。在某一个表第一次被访问过之后,在服务器没有关闭并且表定义没有被修改的条件下,访问这个表时,只需要从内存中找到这个已经缓存起来的对象并且做相应操作即可,而不需要再次从系统表中读取它的定义并且解析,然后再做相应的操作。
当某一个用户要查询某一个表的数据时,系统首先会找到这个表。上面已经提到过,因为MYSQL实现了表的缓存,所以首先会从缓存中寻找这个表,表字典对象的缓存是通过HASH表来管理的,MYSQL系统中,专门有一个HASH表(源代码中的名字是table_def_cache)用来存储组织表对象。
所以首先通过表的名字(包括了模式名)来构造一个HASH键值(KEY),用来从HASH表中搜索对象。
但是对于表对象的缓存,不只是简单的将一些表的定义通过HASH存储起来就算完了,那这样的话缓存可能没有任何意义,或者说局限性非常大,这样可能导致一个用户在表对象上做了什么标志或者修改等都会影响到其它用户,这种影响是不可预期的,更重要的原因是,MYSQL是插件式的数据库,每一个用户得到表对象之后还需要将表实例化,这个实例化的对象只有自己才能使用,所以不是简单的所有用户都使用同一个缓存对象即可完成的。它的缓存其实是用了一种可以称为“共享私有化缓存”,看上去这个说法是矛盾的,其实并不是这样的,它在缓存过程中用到一个叫TABLE_SHARE的结构体,一个这个结构体唯一对应MYSQL中的一个表对象,这里是不区分任何存储引擎的,它实际上就是对具体一个表的定义的翻译或映射,也就是说当需要打开一个表的时候,这个表首先是在MYSQL的系统表中存储的(当然系统表是分不同的存储引擎的,不同的存储引擎有自己的系统表,这里所说的MYSQL的系统表应该是一种统称,其实是具体某一个存储引擎的系统表),如果要使用了,首先需要从系统表中将这个表的所有信息都读入到内存中来,这些信息包括表名、模式名、所有的列信息、列的默认值、表的字符集、对应的frm文件的路径、所属的存储引引擎(MYSQL中的表可以单独定义自己的存储引擎)、主键等等,当然还有很多其它信息,所有这些信息读入内存中的时候首先就是通过结构体TABLE_SHARE来存储的,相当于这个结构体是一个表对象缓存的第一层,同时从名字就可以看出,这个结构体是所以用户都可以共享的一个表对象,所以它是静态的,不允许修改的(内存中),从系统表中读取进来之后直到这个表从缓存中删除,中间不会做任何的修改。
那么用户要访问一个表,只是构造了TABLE_SHARE是远远不够的,而且这个结构体对象也不是直接给用户使用的对象,在构造了这个结构体之后,首先需要将其缓存起来,因为这个结构体就是我们这里讨论的核心,它就是我们要缓存的对象,所以首先需要根据上面计算得到的KEY将这个表对象缓存到table_def_cache中,这个缓存操作到这里就结束了。
但是如果这个问之前已经被访问过了,那么就不需要再像上面一样构造这个共享结构体了,而是直接通过HASH的KEY值在table_def_cache中找到这个共享结构体即可。
从上面的叙述中知道,当系统得到一个SHARE对象之后,系统会真正的构造一个新的对象交给当前的操作,这个对象上面已经说过了,肯定不是TABLE_SHARE,因为这个是缓存对象,它是静态的,只读的,真正与用户交互的是TABLE_SHARE的一个衍生品,它对应的结构体名字为TABLE,它是真正的在操作中被使用的对象,那么是如何从TABLE_SHARE变为TABLE的呢?
其实这两个结构体的很多成员是相同的,并且可以直接复制过去,上面已经说了,TABLE_SHARE是一个静态的缓存对象,那么相对而言,TABLE就可以称作是一个相对动态的、一个正在进行一些操作的实例了,TABLE中有一个成员就是直接指向了TABLE_SHARE的;还有一些成员比如record,这个是用来构造插入操作中的一条记录的,这个系统会根据这个表定义的每一个列及其数据类型等提前构造好;field用来存储所有这个表中的列信息的,这个信息其实是完全将SHARE中的信息克隆过来的。其它的一些小的细节就不叙述了,不过还有两个很重要的东西必须要说一下。
因为上面已经提到了,TABLE这个对象是一个动态的,被实例化的对象,它相当于是一个被打开的表,它已经不是在MYSQL的上层了,而是具体到了某一个存储引擎了,所以这里还需要对这个对象构造它的有关存储引擎的信息并且打开这个表。
因为MYSQL是一个插件式的数据库管理系统,对于表对象的管理,MYSQL层与存储引擎层就是在这里分开的,TABLE算是它们之间的桥梁,下层是存储引擎,上层就是MYSQL了,对于一个MYSQL的存储引擎,都要提供一些公共的接口来驱动其存储引擎,这些接口包括:close_connection、savepoint_set、savepoint_rollback、savepoint_release、commit、rollback、create(创建句柄)、ha_create (创建一个表)、ha_open(打开表)、ha_close(关闭表)、ha_write_row(插入一条记录)、ha_delete_row(删除一条记录)、ha_reset(将实例恢复到新打开的状态)等操作,这些接口都是上层调用来操作对应的存储引擎的,也可以被称作是MYSQL与存储引擎之间交流的通道。
那么从SHARE到TABLE之间的过渡或者叫做SHARE的实例化过程,首先就需要调用函数create来创建一个对应的存储引擎句柄,创建之后就通过函数ha_open来打开这个表,打开表主要是对这个新创建的存储引擎句柄进行一些初始化操作。在打开之后,这个表的实例化也就算完成了,而这个已经被打开的实例句柄就挂在TABLE结构体中,从这里也可以看出,TABLE是与一个操作对应的实例化的对象,它只能在同一时间内被一个操作所使用。
在被实例化之后,这个就可以直接与存储引擎进行交互了,比如插入一条记录,可以直接调用TABLE下面已经被实例化的存储引擎句柄下的函数ha_write_row即可。
当一个操作完成之后,它所实例化的表就不需要了,此时系统不是将这个本地的实例直接释放掉,而是将其保存下来了,保存下来是为了下次某一个用户再次访问这个表的时候不需要再次进行实例化了,直接拿过来用即可,当然可能需要一些额外的操作,比如将实例状态恢复,调用函数ha_reset即可。
系统保存实例是直接将其放在SHARE的一个free_tables链表中,但首先要从used_tables链表上摘下来,这两个链表都是用来保存这个表的所有实例的,used_tables用来存储正在使用的实例,free_tables用来存储所有当前未使用的实例,有可能在并发比较高的情况下,可能在used_tables中有多个,但free_tables中没有,都执行完成之后则相反,那么如果此时再有用户又操作这个表,系统可以直接从free_tables找一个拿来用即可。
现在可以知道,在MYSQL中,表对象的缓存其实是用两个部分,一部分是SHARE的缓存,也就是说多个不同的表的SHARE对象的缓存;另一部分就是每一个SHARE结构被实例化之后的实例对象的缓存,MYSQL用来管理缓存空间大小的方法是通过计数来实现的,默认情况下,系统中总的SHARE个数不能超过400个,所有SHARE的所有表实例的个数也不能超过400个。
上面提到的都是关于表对象SHARE结构的缓存,既然是缓存,肯定相应的有它被删除或者淘汰的问题,当然在这里也不例外。那么在什么情况下SHARE结构会被淘汰或者删除呢?很明显,如果只是对这个表进行增删改等没有涉及到修改表定义的操作,SHARE是不会被删除的,只有可能会被淘汰,因为如果查询太多表的话,表对象缓存个数是有限制的,当到达这个数目之后,系统会自动将一些不经常使用的SHARE淘汰掉,这个很容易理解。
那么一般情况下,只有对表结构、依赖关系、表定义等方面进行修改之后,因为这个表的版本被更新了,如果继续将其缓存的话是不安全的,或者是错误的,或者导致一些不可预知的问题,所以这种情况下这个表对象的缓存SHARE对象必须要从缓存中删除,同时要删除它上面所以被实例化的表对象缓存结构,这样当其它用户在等待表对象的修改操作完成之后(因为修改过程中这个表是被上了锁的,进行操作需要等待),又一次像上面叙述的一样,首先是从缓存中找这个表的缓存对象,如果找不到的话再从数据字典(系统表)中读取进来,然后继续操作即可。
到这里关于表的缓存及一些其它的内容就叙述完了。
总结:
1)上面提到的表的缓存机制有很大的好处的,因为它不是全字典缓存(全字典缓存的意思就是在数据库启动时将所有的数据字典信息都一次性载入内存中来,这样在使用过程中就效率非常高,但在DDL操作方面有很大的不足),它是用到的时候再载入,修改之后直接删除有可能再重新载入,这样的实现方式减少了DDL操作或DDL的回滚导致的字典缓存维护工作的代价。
2)有效的利用了内存空间,因为可以通过设置表对象缓存空间的大小来控制内存的使用情况,同时只有用到的对象才会被载入到内存中,提高了内存的利用率。
3)上面所叙述的MYSQL表缓存实现方案虽然说是比较先进的,但是在效率方面还是有些优化的空间的,比如上面提到的,在用来控制缓存空间大小是根据实例的个数来计算的,在系统中默认最大值是400个,如果超过这个值系统会自动淘汰一些不常用的实例,但是如果一个表的定义非常大,那么并发情况下,有可能会建立很多个实例,假设接近400个,那么这样算下来有可能会将操作系统的内存用光,这个是不可控制的,也是不可预期的。对于SHARE的缓存也是一样,如果一个用户访问了很多不同的定义很大的表,也会有同样的问题。
4)从上面也可以看出,为了实现插件式的数据库,其实还是有一些效率的代价的,在表的缓存方面,中间加入了一层SHARE的缓存,真正用到的时候还需要实例化,因为每一个用户的操作及不同时间的状态都是不同的,所以每一个用户必须要再在SHARE的基础上实例化一个新的对象出来,这样就给内存、CPU带来了一定程序上的浪费及压力。
问题的解决:
1)SHARE缓存:我个人认为有一个更好的办法来很精确通过具体的空间大小来管理表缓存空间,因为对于SHARE而言,它是静态的,它是个结构体,通过使用计数来控制内存的使用,有可能会造成内存用光的情况,那么对于SHARE对象,完全可以把它流式化(扁平化),也就是说等把这个结构体的大小计算出来,申请相应的空间,将结构体中的所有信息都按照固定的顺序写入到这块内存中,那么这样一个SHARE所占的空间大小就固定了,这样可以完全通过设置空间大小来管理表对象缓存空间了,这样上面提到的内存用光的问题就自然解决了,当然这个大小需要根据计算机的内存大小合理的设置,至少不会出现不可预料的问题。
2)TABLE缓存:TABLE实例的缓存同样存在上面的问题,解决方案与上面的思想差不多,因为这个对象是一直被用的,它是一个实例,所以就不能直接像上面一样,将其流式化,而是可以通过申请一片连接的空间,这个实例中所有的指针或者其成员的值都指向(有可能要对齐)这个空间中的指定位置,这样这个结构体的使用没有任何改变,但其占用的空间大小是固定的,同样可以通过用户手动设置TABLE实例缓存空间的大小来管理缓存空间,这样也避免了表定义太大导致内存用光的问题。