水平有限仅供参考,仅供参考。
一、综述
在MySQL8.0中我们没有了frm文件,取而带之的是全新的字段缓存的设计和多个持久化的字典表,这部分不仅为原子性DDL提供了基础,而且减少打开物理frm文件的开销。
但是原先的table/table_share的缓存依旧架设在前面。因此看起来在获取表的字典数据的时候就依次为:
- table_cache (大概率就命中了,参数相关,分多个实例,每个session只能用一个实例)
- table_define_cache(获取table share,命中率高,参数相关)
- Dictionary_client(字典元素)
- Shared_dictionary_cache(字典元素,命中率高,最大可缓存max connections个数的表字典信息)
- 持久化的表
而Dictionary_client和Shared_dictionary_cache和持久化的表就代替了原先的frm文件。这里先说一下flush tables语句,从debug来看并不影响 3和4的缓存。
二、Dictionary_client
Dictionary_client是一个session(THD)相关的,也就是session自己的,其中在Dictionary_client内包含一个重要的元素叫做Object_registry,其中包含了各种字典类型的map,如下:
std::unique_ptr<Local_multi_map<Abstract_table>> m_abstract_table_map;
std::unique_ptr<Local_multi_map<Charset>> m_charset_map;
std::unique_ptr<Local_multi_map<Collation>> m_collation_map;
std::unique_ptr<Local_multi_map<Column_statistics>> m_column_statistics_map;
std::unique_ptr<Local_multi_map<Event>> m_event_map;
std::unique_ptr<Local_multi_map<Resource_group>> m_resource_group_map;
std::unique_ptr<Local_multi_map<Routine>> m_routine_map;
std::unique_ptr<Local_multi_map<Schema>> m_schema_map;
std::unique_ptr<Local_multi_map<Spatial_reference_system>>
每种类型的元数据都缓存在自己的map中,比如我们常说的Schema和Abstract_table,这里需要注意的是他们都是父类,比如
image.png
当然table还会更加复杂一些。具体的可以从父类Entity_object开始进行探索。但是需要注意的是这里是一个unique_ptr类型的智能指针,也就是说不能共享。
而Local_multi_map来自Multi_map_base,而Multi_map_base又包含了如下4种map,除了m_aux_map,其他3种如下:
Element_map<const T *, Cache_element<T>> m_rev_map; // Reverse element map.
Element_map<typename T::Id_key, Cache_element<T>>
m_id_map; // Id map instance.
Element_map<typename T::Name_key, Cache_element<T>>
m_name_map; // Name map instance.
其实从multi名字也可以看出来是多个map。
Element_map实际上就是一个std::map的封装,我们可以看到3种根据是指针地址/Id_key/Name_key 进行的分别map,主要应对多种查询方式。如果以实例化的dd::Table为例子,Id_key/Name_key其定义如下:
typedef Primary_id_key Id_key; (表中的主键?)
typedef Item_name_key Name_key;(字符串名字)
而Cache_element(dd::cache::Cache_element)就是实际元素的封装拉,也是map的value值,其中包含一些元素如下:
const T *m_object; // Pointer to the actual object.
uint m_ref_counter; // Number of concurrent object usages.
Key_wrapper<typename T::Id_key> m_id_key; // The id key for the object.
Key_wrapper<typename T::Name_key> m_name_key; // The name key for the object.
Key_wrapper<typename T::Aux_key> m_aux_key; // The aux key for the object.
其中const T *m_object就是实际指向对象的指针了,比如一个dd::Table的元数据,也就是实际的一个字典元素,而m_ref_counter在后面的Shared_dictionary_cache会用到,主要是一个LRU链表的会使用到,对于超过最大容量得做淘汰,后面再说。
最后Dictionary_client实际上提供了3个Object_registry元素如下
- m_registry_uncommitted
加入时机: dd::cache::Dictionary_client::update
比如实例化的函数: dd::cache::Dictionary_client::update<dd::Table> - m_registry_committed
加入时机: dd::cache::Dictionary_client::remove_uncommitted_objects
比如实例化的函数:
dd::cache::Dictionary_client::remove_uncommitted_objects<dd::Table> - m_registry_dropped
加入时机: dd::cache::Dictionary_client::register_dropped_object
比如实例化的函数: dd::cache::Dictionary_client::register_dropped_object<dd::Table>
实际上这部分主要和DDL操作进行的状态转换有关,这里先不考虑了,因为原子化DDL没有学习过。但是从流程来看,一般select语句会加入到m_registry_committed中,并且查找也会在里面查找。
那么总结一下,这里面包含3个Object_registry元素,每个元素包含多个Local_multi_map,而每个Local_multi_map是Multi_map_base的继承,每个Multi_map_base包含了4个map,其中3个常用,分别是主键/名字/元素的指针 为key,元素就是对应的Cache_element。Cache_element原则也是一个元素的指针。
三、Shared_dictionary_cache
Shared_dictionary_cache是全局的,使用的是单例模式,这部分可以在dd::cache::Shared_dictionary_cache::instance函数中找到他的static变量,如下:
image.png
实际上Shared_dictionary_cache和Dictionary_client类似,只是没有3个Object_registry,取而代之是基于Multi_map_base实现的Shared_multi_map,并且也是包含各个字典类型的map,但是在Shared_multi_map实现中加入了LRU链表的方式,因为Shared_dictionary_cache会缓存较多的字段元素(比如一个表的字典)。
在Shared_dictionary_cache初始化(dd::cache::Shared_dictionary_cache::init)的时候会根据各自最大容量限制,如下:
void Shared_dictionary_cache::init() {
instance()->m_map<Collation>()->set_capacity(collation_capacity);
instance()->m_map<Charset>()->set_capacity(charset_capacity);
// Set capacity to have room for all connections to leave an element
// unused in the cache to avoid frequent cache misses while e.g.
// opening a table.
instance()->m_map<Abstract_table>()->set_capacity(max_connections);
instance()->m_map<Event>()->set_capacity(event_capacity);
instance()->m_map<Routine>()->set_capacity(stored_program_def_size);
instance()->m_map<Schema>()->set_capacity(schema_def_size);
instance()->m_map<Column_statistics>()->set_capacity(
column_statistics_capacity);
instance()->m_map<Spatial_reference_system>()->set_capacity(
spatial_reference_system_capacity);
instance()->m_map<Tablespace>()->set_capacity(tablespace_def_size);
instance()->m_map<Resource_group>()->set_capacity(resource_group_capacity);
}
其中大部分都在Shared_dictionary_cache的定义中,硬编码如下:
static const size_t collation_capacity = 256;
static const size_t column_statistics_capacity = 32;
static const size_t charset_capacity = 64;
static const size_t event_capacity = 256;
static const size_t spatial_reference_system_capacity = 256;
static const size_t resource_group_capacity = 32;
但是我们发现有如下不同
- Abstract_table map:最大值和参数max_connections有关这也是我们最关注的,表的字典信息的缓存。
- Schema map:最大值和参数schema_definition_cache有关,默认256
- Tablespace map:最大值和参数tablespace_definition_cache有关,默认256
- Routine map:最大值和参数stored_program_definition_cache有关,默认256
实际上当我们的table share失效过后,一般用到的都是这里的map,因此依赖LRU进行缓存是非常有必要的。
而在Dictionary_client的RAII类析构的时候会自动调用释放,释放的时候调用函数,
template <typename T>
size_t Dictionary_client::release(Object_registry *registry)
如下:
// Release the element from the shared cache.
Shared_dictionary_cache::instance()->release(element);//在share中去除
这里就是调用的
template <typename T>
void release(Cache_element<T> *e) {
m_map<T>()->release(e);
}
实际上就是,Shared_multi_map的release方法,关键如下:
Shared_multi_map<T>::release
// Release the element.
element->release(); //计数器 -1
// If the element is not used, add it to the free list.
if (element->usage() == 0) {
m_free_list.add_last(element);
rectify_free_list(&lock); //放到free list
}
实际上就是上面说的Cache_element中m_ref_counter的作用。
后面进行DEBUG发现,确实大部分访问过的表的字段都会存在于Abstract_table map中,这个比较简单,只要拿到static指针的地址去访问就可以了如下:
-
p (*((dd::cache::Shared_dictionary_cache *) 0x84be3c0)).m_abstract_table_map->m_name_map
image.png
我们访问就是 名字(key) - value(Cache_element) 这样一个map,因为是名字比较容易看。这里我们发现元素有90个,因为比较多,值显示了小部分,实际上有90个表的字典都缓存在了Shared_dictionary_cache中。
四、Dictionary_client的Auto_releaser类
dd::cache::Dictionary_client::Auto_releaser实际上是一个RAII类,目的在于在析构的时候能够自动释放Dictionary_client中本次访问的字典对象。它满足先进后出的方式,它包含一个主要的元素为m_release_registry,也是一个Object_registry类型,主要目的就是将访问到的字典对象也保存在里面,以便析构自动循环它并且在dd::cache::Dictionary_client中释放
其主要使用方式为
- 每次定义个dd::cache::Dictionary_client::Auto_releaser类,并且将其m_client指向到会话的dd::cache::Dictionary_client
- 当然访问字典对象的时候,同时加入到dd::cache::Dictionary_client的对应map中和dd::cache::Dictionary_client::Auto_releaser的m_release_registry对应的map中。
- 当析构的时候自动根据dd::cache::Dictionary_client::Auto_releaser中注册的对象,在dd::cache::Dictionary_client中删除。
下面就是这部分如下:
size_t Dictionary_client::release(Object_registry *registry) {
assert(registry);
size_t num_released = 0;
// Iterate over all elements in the registry partition.
typename Multi_map_base<T>::Const_iterator it;
for (it = registry->begin<T>(); it != registry->end<T>(); ++num_released) { //循环迭代Auto_relase中的响应的map对象,哑元函数确认
// Make sure we handle iterator invalidation: Increment
// before erasing.
Cache_element<T> *element = it->second; //获取元素
++it;
// Remove the element from the actual registry.
registry->remove(element); //哑元函数确认,本生做删除
// Remove the element from the client's object registry.
if (registry != &m_registry_committed)
m_registry_committed.remove(element); //client做删除
else
(void)m_current_releaser->remove(element);
// Release the element from the shared cache.
Shared_dictionary_cache::instance()->release(element);// share dict中进行计数器操作
}
return num_released;
正是因为这样的删除方式,实际上dd::cache::Dictionary_client在语句结束后就会自动删除本身持有的字典对象,但是这个时候已经加入到了dd::cache::Shared_dictionary_cache中,因此感觉Shared_dictionary_cache的作用更大,而dd::cache::Dictionary_client和DDL联系更紧。
五、关于字典元素
前面说错数据一旦读取出来(如何读取后面我们会看到),就放到了字段元素这些类里面,然后挂到相应的各个map中去。
这里以dd::Table_impl 为例,实际上有很多次的继承,很是麻烦,如下
image.png
每个字典元素信息都包含在这个类自身或者其父类上。
dd::Table_impl <- Abstract_table_impl <- Entity_object_impl <- Entity_object <- Weak_object
<- Weak_object_impl <- Weak_object
<- Abstract_table <- Entity_object <- Weak_object
<- dd::Table <- Abstract_table <- Entity_object <- Weak_object
friend class cache::Storage_adapter
friend class Entity_object_table_impl
这里需要注意的是Entity_object 包含一个友元性质 friend class cache::Storage_adapter,这说明存储层是可以访问各个内存字典元素的数据的,获取了就可以对底层的表进行操作了,也可以操作底层的表读取数据后给内存的字典元素。
六、字典表的属性定义
除了字典元素本生,字典表本生也有自己的属性,比如字段/表名等等,这些属性都放到了字典表定义这个类里面,注意这也是单例,下面是和它有关的继承关系,只截取一部分。
image.png
比如这里Tables类,里面就要字段的定义如下
image.png
七、information_schema视图定义相关类
除了上面提到的字典元素的类,字典表属性的类,建立视图还有一个类,这里简单看看。
这部分实际上就是各个内部视图的定义,information_schema中大部分的视图都在这里定义,也就是建视图的语句都包含在这些类里面,这个太多了(注意这些类的对象也是单例)就截取一部分:
我们还是以information_schema.tables表为例实际上他定义的类就是dd::system_views::Tables,随便翻一下就能看到视图的定义如下:
image.png下面是相关的继承关系,
dd.system_views.Tables
<- dd.system_views.Tables_base
<- dd.system_views.System_view_impl
<- dd.system_views.System_view(基类)
dd.system_views.System_view_select_definition_impl
<-dd.system_views.System_view_definition_impl
<-dd.system_views.System_view_definition
八、打开字典的流程
实际上这里谈到的知识和原子DDL有很大关系,但是我们这里只大概看看open table的时候,在获取这种字典的时候如下做的,我们主要看如果table cache失效后,我们要拿table share如何拿的。这个函数就是get_table_share_with_discover调用的get_table_share。
get_table_share流程大概为:
- 从table_def_cache中寻找是否有缓存的table share。如果有直接返回,如果没有开始做第2步。
- 从dd::cache::Dictionary_client中获取,这是线程自己的,如果找到直接返回,并且通过这个字典元素构建table share,并且加入table_def_cache,如果没有知道走第3步。
- 从dd::cache::Shared_dictionary_cache中获取,这是单例,如果找到就返回,并且加入到dd::cache::Dictionary_client中,然后构建table share(open_table_def),并且加入到table_def_cache,如果没有知道走第4步。
- 从底层字典表获取,找到后加入到dd::cache::Shared_dictionary_cache和dd::cache::Dictionary_client中,然后构建table share,并且加入到table_def_cache。
在获取底层表的时候主要是通过cache::Storage_adapter::get这个方法进行的,主要是获取对应底层表的相关的一行数据,然后解析为字典元素需要的信息(Table_impl::restore_attributes)。顺便说有一下关于sdi的维护也在这个cache::Storage_adapter下进行,比如inplace DDL的最后(commit之后)会进行sdi的维护:
mysql_alter_table
->mysql_inplace_alter_table
->dd::cache::Dictionary_client::store
->dd::cache::Storage_adapter::store
->dd::sdi::store
如果这个流程都没找到,说明你访问的表不存在。这里需要注意的是open_table_def函数,在5.7基于是frm文件构建,而到了8.0就是我们提到的这里的字典元素了。
九、相关重点技术
这部分新的设计完全是根据接口和实现进行的,并且多继承在那里面,回调函数使用也特别多,这里看看涉及到重点的一些C++语法的使用。
-
哑元函数
这个在Object_registry体现了重要的作用,因为每个Object_registry包含了好几个不同类型的字典的map,当要确认是哪个map的时候就是通过一个类Type_selector构造哑元函数进行确认的如下:
image.png -
const函数重载
同上,我们可以发现他们函数的名字都是相同的,但是统一类型的map的 m_map函数有带const的又不带,不同的含义,当需要新建的时候(分配map的内存给指针)就需要不带const的,比如map的put方法,如果只是map的get方法,那么就需要带const的函数,这样就返回其指针。
image.png -
友元和纯虚函数(继承)
虽然友元自身不能继承,但是和纯虚函数一起就发生了质变。这一点在Entity_object有体现,它包含了一个友元性质 friend class cache::Storage_adapter,并且Entity_object是父类,会继承出很多的不同类型的字典类型的class子类,那么当cache::Storage_adapter访问父类Entity_object的纯虚函数的时候,如果填入的是子类的指针,实际上跑的是子类的重写的函数。 -
RAII
这个前面已经说了,就是方便释放字典对象,或者一般的释放内存等。 -
虚继承
有多重继承的时候为了消除二义性。
十、隐藏的字典表
这部分你实际上包含好多表,需要DEBUG版本并且开启
- SET session debug='+d,skip_dd_table_access_check';
才能访问到,也就是我们上面谈的缓存的实际存储位置,如下:
mysql.resource_groups
mysql.table_stats
mysql.routines
mysql.events
mysql.column_statistics
mysql.index_stats
mysql.tablespaces
mysql.spatial_reference_systems
mysql.schemata
mysql.collations
mysql.tables
mysql.character_sets
mysql.catalogs
mysql.check_constraints
mysql.columns
mysql.column_type_elements
mysql.dd_properties
mysql.foreign_keys
mysql.foreign_key_column_usage
mysql.indexes
mysql.index_column_usage
mysql.index_partitions
他们就是上面cache的实际数据的存储。其次如果我们试图查看information_schema里面表的定义我们也能够发现,其中大部分为视图,其来源就是这些内部表,比如information_schema.tables这个视图,如下:
image.png
而在5.7中则是memory的表如下:
image.png
十一、打开table share代码流程
get_table_share_with_discover
->get_table_share(这个过程是一定要找到share的)
通过table cache def寻找是否有share
->for 循环table_def_cache,进行寻找
->如果没找到,
为shared分配内存alloc_table_share
->assign_new_table_id
为share分配一个map id,share->table_map_id
->table_def_cache->emplace
将key和share的智能指针放入buffer
->Auto_releaser releaser
RAII自动析构,其中包含一个Object_registry元素
->dd::cache::Dictionary_client.acquire
acquire(share->db.str, &sch)
寻找schema是否存在
->bool dd::cache::Dictionary_client::acquire(const String_type &object_name,const T **object)
这里的T就是进行实例化比如这里的sch
->bool Dictionary_client::acquire(const K &key, const T **object,bool *local_committed,bool *local_uncommitted)
->m_registry_committed.get(key, &element);
现在本地的Dictionary_client map 中寻找,如果找到直接返回
->如果没有找到在全局shared中查找
Shared_dictionary_cache::instance()->get(m_thd, key, &element)
具体流程见后面
->m_registry_committed.put(element);
插入到本地Dictionary_client map中
->open_table_def
根据找到的字典元素进行share构建
->Shared_dictionary_cache::instance()->get(m_thd, key, &element)
当client没有命中则调用
->Shared_dictionary_cache::get(THD *thd, const K &key,Cache_element<T> **element)
用于获取字段对象
->m_map<T>()->get(key, element)
从对应的map中获取,如果获取失败则调用引擎层进行查询
->Shared_dictionary_cache::get_uncached(thd, key, ISO_READ_COMMITTED, &new_object)
这里需要的是一个新的字典对象,传入指针的指针
->Storage_adapter::get(thd, key, isolation, false, object)
dd::cache::Storage_adapter::get
调用接口查询数据
->Entity_object_table &table = T::DD_table::instance();
通过字典对象的属性(typedef tables::Tables DD_table;)获取字典表的信息
比如tables
->Raw_table *t = trx.otx.get_table(table.name());
获取字典表访问的接口
->t->find_record(key, r)
查询数据
->table.restore_object_from_record(&trx.otx, *r.get(), &new_object)
将获取的底层字典表的信息,存储到new_object这个字典对象中,比如这是一个
表的信息。实际上调用为对应字典对象类的
->Table_impl::restore_attributes(const Raw_record &r)
->m_map<T>()->put(&key, new_object, element);
插入share cache对应的map中
以上。。
参考:
MySQL · 源码分析 · 原子DDL的实现过程
MySQL 深潜 - 一文详解 MySQL Data Dictionary
MySQL8.0数据字典实现一窥
网友评论