解讀MySQL 8.0資料字典快取管理機制

华为云开发者联盟發表於2024-07-16

背景介紹

MySQL的資料字典(Data Dictionary,簡稱DD),用於儲存資料庫的後設資料資訊,它在8.0版本中被重新設計和實現,透過將所有DD資料唯一地持久化到InnoDB儲存引擎的DD tables,實現了DD的統一管理。為了避免每次訪問DD都去儲存中讀取資料,使DD記憶體物件能夠複用,DD實現了兩級快取的架構,這樣在每個執行緒使用DD client訪問DD時可以透過兩級快取來加速對DD的記憶體訪問。

整體架構

解讀MySQL 8.0資料字典快取管理機制

圖1 資料字典快取架構圖

需要訪問DD的資料庫工作執行緒透過建立一個DD client(DD系統提供的一套DD訪問框架)來訪問DD,具體流程為透過與執行緒THD繫結的類Dictionary_client,來依次訪問一級快取和二級快取,如果兩級快取中都沒有要訪問的DD物件,則會直接去儲存在InnoDB的DD tables中去讀取。後文會詳細介紹這個過程。

DD的兩級快取底層都是基於std::map,即鍵值對來實現的。

  • 第一級快取是本地快取,由每個DD client執行緒獨享,核心資料結構為Local_multi_map,用於加速當前執行緒對於同一物件的重複訪問,以及在當前執行緒執行DDL語句修改DD物件時管理已提交、未提交、刪除狀態的物件。
  • 第二級快取是共享快取,為所有執行緒共享的全域性快取,核心資料結構為Shared_multi_map,儲存著所有執行緒都可以訪問到的物件,因此其中包含一些併發控制的處理。

整個DD cache的相關類圖結構如下:

解讀MySQL 8.0資料字典快取管理機制

圖2 資料字典快取類圖

Element_map是對std::map的一個封裝,鍵是id、name等,值是Cache_element,它包含了DD cache object,以及對該物件的引用計數。DD cache object就是我們要獲取的DD資訊。

Multi_map_base中包含了多個Element_map,可以讓使用者根據不同型別的key來獲取快取物件。Local_multi_map和Shared_multi_map都是繼承於Multi_map_base。

兩級快取

第一級快取,即本地快取,位於每個Dictionary_client內部,由不同狀態(committed、uncommitted、dropped)的Object_registry組成。

class Dictionary_client {
 private:
  std::vector<Entity_object *> m_uncached_objects;  // Objects to be deleted.
  Object_registry m_registry_committed;    // Registry of committed objects.
  Object_registry m_registry_uncommitted;  // Registry of uncommitted objects.
  Object_registry m_registry_dropped;      // Registry of dropped objects.
  THD *m_thd;                        // Thread context, needed for cache misses.
  ...
};

程式碼段1

其中m_registry_committed,存放的是DD client訪問DD時已經提交且可見的DD cache object。如果DD client所在的當前執行緒執行的是一條DDL語句,則會在執行過程中將要drop的舊錶對應的DD cache object存放在m_registry_dropped中,將還未提交的新表定義對應的DD cache object存放在m_registry_uncommitted中。在事務commit/rollback後,會把m_registry_uncommitted中的DD cache object更新到m_registry_committed中去,並把m_registry_uncommitted和m_registry_dropped清空。

每個Object_registry由不同後設資料型別的Local_multi_map組成,透過模板的方式,實現對不同型別的物件(比如表、schema、tablespace、Event 等)快取的管理。

第二級快取,即共享快取,是全域性唯一的,使用單例Shared_dictionary_cache來實現。

Shared_dictionary_cache *Shared_dictionary_cache::instance() {
  static Shared_dictionary_cache s_cache;
  return &s_cache;
}

程式碼段2

與本地快取中Object_registry相似,Shared_dictionary_cache也包含針對各種型別物件的快取。與本地快取的區別在於,本地快取可以無鎖訪問,而共享快取需要在獲取/釋放DD cache object時進行加鎖來完成併發控制,並會透過Shared_multi_map中的條件變數來完成併發訪問中的執行緒同步與快取未命中情況的處理。

快取讀取過程

邏輯流程

DD物件主要有兩種訪問方式,即透過後設資料的id,或者name來訪問。需要訪問DD的資料庫工作執行緒透過DD client,傳入後設資料的id,name等key去快取中讀取後設資料物件。讀取的整體過程:一級本地快取 -> 二級共享快取 -> 儲存引擎。流程圖如下:

解讀MySQL 8.0資料字典快取管理機制

圖3 資料字典快取讀取流程圖

由上圖所示,在DD cache object加入到一級快取時,已經確保其在二級快取中也備份了一份,以供其他執行緒使用。

程式碼實現如下:

// Get a dictionary object.
template <typename K, typename T>
bool Dictionary_client::acquire(const K &key, const T **object,
                                bool *local_committed,
                                bool *local_uncommitted) {
  ...

  // Lookup in registry of uncommitted objects
  T *uncommitted_object = nullptr;
  bool dropped = false;
  acquire_uncommitted(key, &uncommitted_object, &dropped);

  ...

  // Lookup in the registry of committed objects.
  Cache_element<T> *element = NULL;
  m_registry_committed.get(key, &element);

  ...

  // Get the object from the shared cache.
  if (Shared_dictionary_cache::instance()->get(m_thd, key, &element)) {
    DBUG_ASSERT(m_thd->is_system_thread() || m_thd->killed ||
                m_thd->is_error());
    return true;
  }

  ...
}

程式碼段3

在一級本地快取中讀取時,會先去m_registry_uncommitted和m_registry_dropped中讀取(均在acquire_uncommitted()函式中實現),因為這兩個是最新的修改。之後再去m_registry_committed中讀取,如果讀取到就直接返回,否則去二級共享快取中嘗試讀取。共享快取的讀取過程在Shared_multi_map::get()中實現。就是加鎖後直接到對應的Element_map中查詢,存在則把其加入到一級快取中並返回;不存在,則會進入到快取未命中的處理流程。

快取未命中

當本地快取和共享快取中都沒有讀取到後設資料物件時,就會呼叫DD cache的持久化儲存的介面Storage_adapter::get()直接從儲存在InnoDB中的DD tables中讀取,建立出DD cache object後,依次把其加入到共享快取和本地快取中。

DD client對併發訪問未命中快取的情況做了併發控制,這樣做有以下幾個考量:

1.因為記憶體物件可以共用,所以只需要維護一個DD cache object在記憶體即可。

2.訪問持久化儲存的呼叫棧較深,可能涉及IO,比較耗時。

3.不需要每個執行緒都去持久化儲存中讀取資料,避免資源的浪費。

併發控制的程式碼如下:

// Get a wrapper element from the map handling the given key type.
template <typename T>
template <typename K>
bool Shared_multi_map<T>::get(const K &key, Cache_element<T> **element) {
  Autolocker lock(this);
  *element = use_if_present(key);
  if (*element) return false;

  // Is the element already missed?
  if (m_map<K>()->is_missed(key)) {
    while (m_map<K>()->is_missed(key))
      mysql_cond_wait(&m_miss_handled, &m_lock);

    *element = use_if_present(key);

    // Here, we return only if element is non-null. An absent element
    // does not mean that the object does not exist, it might have been
    // evicted after the thread handling the first cache miss added
    // it to the cache, before this waiting thread was alerted. Thus,
    // we need to handle this situation as a cache miss if the element
    // is absent.
    if (*element) return false;
  }

  // Mark the key as being missed.
  m_map<K>()->set_missed(key);
  return true;
}

程式碼段4

第一個訪問未命中快取的DD client會將key加入到Shared_multi_map的m_missed集合中,這個集合包含著現在所有正在讀取DD table中後設資料的物件key值。之後的client在訪問DD table之前會先判斷目標key值是否在m_missed集合中,如在,就會進入等待。當第一個DD client構建好DD cache object,並把其加入到共享快取之後,移除m_missed集合中對應的key,並透過條件變數通知所有等待的執行緒重新在共享快取中獲取。這樣對於同一個DD cache object,就只會對DD table訪問一次了。時序圖如下:

解讀MySQL 8.0資料字典快取管理機制

圖4 資料字典快取未命中時序圖

快取修改過程

在一個資料庫工作執行緒對DD進行修改時,DD cache也會在事務commit階段透過remove_uncommitted_objects()函式進行更新,更新的過程為先把DD舊資料從快取中刪除,再把修改後的DD cache object更新到快取中去,先更新二級快取,再更新一級快取,流程圖如下:

解讀MySQL 8.0資料字典快取管理機制

圖5 資料字典快取更新流程圖

因為這個更新DD快取的操作是在事務commit階段進行,所以在更新一級快取時,會先把更新後的DD cache object放到一級快取中的m_registry_committed裡去,再把m_registry_uncommitted和m_registry_dropped清空。

快取失效過程

當Dictionary_client的drop方法被呼叫對後設資料物件進行清理時,在後設資料物件從DD tables中刪除後,會呼叫invalidate()函式使兩級快取中的DD cache object失效。流程圖如下:

解讀MySQL 8.0資料字典快取管理機制

圖6 資料字典快取失效流程圖

這裡在判斷DD cache object在一級快取中存在,並在一級快取中刪除掉該物件後,可以直接在二級快取中完成刪除操作。快取失效的過程受到後設資料鎖(Metadata lock, MDL)的保護,因為後設資料鎖的併發控制,保證了一個執行緒在刪除共享快取時,不會有其他執行緒也來刪除它。實際上本地快取的資料有效,就是依賴於後設資料鎖的保護,否則共享快取區域的資訊,是可以被其他執行緒更改的。

快取容量管理

一級本地快取為DD client執行緒獨享,由RAII類Auto_releaser來負責管理其生命週期。其具體流程為:每次建立一個DD client時,會定義一個對應的Auto_releaser類,當訪問DD時,會把讀取到的DD cache object同時加到Auto_releaser裡面的m_release_registry中去,當Auto_releaser析構時,會呼叫Dictionary_client的release()函式把m_release_registry中的DD快取全部釋放掉。

二級共享快取會在Shared_dictionary_cache初始化時,根據不同型別的物件設定好快取的容量,程式碼如下:

void Shared_dictionary_cache::init() {
  instance()->m_map<Collation>()->set_capacity(collation_capacity);
  instance()->m_map<Charset>()->set_capacity(charset_capacity);
  ...
}

程式碼段5

在二級快取容量達到上限時,會透過LRU的快取淘汰策略來淘汰最近最少使用的DD cache物件。在一級快取中存在的快取物件不會被淘汰。

// Helper function to evict unused elements from the free list.
template <typename T>
void Shared_multi_map<T>::rectify_free_list(Autolocker *lock) {
  mysql_mutex_assert_owner(&m_lock);
  while (map_capacity_exceeded() && m_free_list.length() > 0) {
    Cache_element<T> *e = m_free_list.get_lru();
    DBUG_ASSERT(e && e->object());
    m_free_list.remove(e);
    // Mark the object as being used to allow it to be removed.
    e->use();
    remove(e, lock);
  }
}

程式碼段6

總結

MySQL 8.0中的資料字典,透過對兩級快取的逐級訪問,以及精妙的對快取未命中情況的處理方式,有效的加速了在不同場景下資料庫對DD的訪問速度,顯著的提升了資料庫訪問後設資料資訊的效率。另外本文還提到了後設資料鎖對資料字典快取的保護,關於後設資料鎖的相關機制,會在後續文章陸續介紹。

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章