MySQL鎖系列3 MDL鎖


MySQL為了保護數據字典元數據,使用了metadata lock,即MDL鎖,保證在並發的情況下,結構變更的一致性。

MDL鎖的加鎖模式和源碼上的組織上和上一篇blog中MySQL表鎖的實現方式一致,都采用了【mutex+condition+queue】來實現並發,阻塞,喚醒的控制。

下面就來看看MDL鎖:

 

1. 重要的數據結構:

     

1. MDL_map

  mdl_map使用hash表,保存了MySQL所有的mdl_lock,全局共享,使用MDL_KEY作為key來表,key=【db_name+table_name】唯一定位一個表。

2. mdl_context

  mdl_context在MySQL為每一個connection創建thd時,初始化一個mdl上下文,保存了當前session請求的mdl信息。

3. MDL_lock

  mdl_lock表示系統的一個mdl鎖,所有的mdl request都請求對應的mdl_lock,這個mdl_lock結構保存了兩個queue,一個是grant_queue表示拿到lock的請求隊列。

  一個是wait_queue表示請求這個mdl_lock的阻塞隊列。

4. MDL_wait

  mdl_wait包裝了一個mutex和一個condition,提供了所有的加鎖,wait,notify操作。

5. MDL_request

  在open table的時候,會init一個request,包含了請求的enum_mdl_type,enum_mdl_duration,MDL_ticket,MDL_key。

 

下面再看看三個重要的枚舉類型:

  enum enum_mdl_namespace { GLOBAL=0,
                            SCHEMA,
                            TABLE,
                            FUNCTION,
                            PROCEDURE,
                            TRIGGER,
                            EVENT,
                            COMMIT,
                            /* This should be the last ! */
                            NAMESPACE_END };
enum enum_mdl_duration {
  /**
    Locks with statement duration are automatically released at the end
    of statement or transaction.
  */
  MDL_STATEMENT= 0,
  /**
    Locks with transaction duration are automatically released at the end
    of transaction.
  */
  MDL_TRANSACTION,
  /**
    Locks with explicit duration survive the end of statement and transaction.
    They have to be released explicitly by calling MDL_context::release_lock().
  */
  MDL_EXPLICIT,
  /* This should be the last ! */
  MDL_DURATION_END };
enum enum_mdl_type {
  MDL_INTENTION_EXCLUSIVE= 0,
  MDL_SHARED,
  MDL_SHARED_HIGH_PRIO,
  MDL_SHARED_READ,
  MDL_SHARED_WRITE,
  MDL_SHARED_NO_WRITE,
  MDL_SHARED_NO_READ_WRITE,
  MDL_EXCLUSIVE,
  MDL_TYPE_END};

 

首先:enum_mdl_namespace 表示mdl_request的作用域,比如alter table操作,需要獲取TABLE作用域。

然后:enum_mdl_duration 表示mdl_request的持久類型,比如alter table操作,類型是MDL_STATEMENT,即語句結束,就釋放mdl鎖。又比如autocommit=0;select 操作,類型是MDL_TRANSACTION,必須在顯示的commit,才釋放mdl鎖。

最后:enum_mdl_type 表示mdl_request的lock類型,根據這個枚舉類型,來判斷是否兼容和互斥。

 

2. 測試

    下面根據一個測試,看一下加鎖,釋放,阻塞的過程,已經主要的函數調用棧:

  session1:            session2:

    set autocommit=0;        alter table pp add name varchar(100):

    select * from pp;

 

2.1 創建connection過程中,初始化mdl_context.

  函數調用:

    handle_connections_sockets

      MDL_context::init: 每一個connection對應一個mdl_context

2.2 初始化mdl_request

  函數調用:

    parse_sql

      st_select_lex::add_table_to_list
        MDL_request::init

  說明: 在session1的過程中,創建的mdl_request: 

      mdl_namespace=MDL_key::TABLE,

      db_arg=0x8c7047c8 "xpchild",

      name_arg=0x8c7047d0 "pp",
      mdl_type_arg=MDL_SHARED_READ,

      mdl_duration_arg=MDL_TRANSACTION

2.3 加鎖

     acquire_lock:

if (lock->can_grant_lock(mdl_request->type, this))
 {
    lock->m_granted.add_ticket(ticket);
    mysql_prlock_unlock(&lock->m_rwlock);
    m_tickets[mdl_request->duration].push_front(ticket);
    mdl_request->ticket= ticket;
  }

說明:首先進行兼容性判斷,如果兼容,那么就把ticket加入到隊列中,加鎖成功。

  函數調用棧

   open_and_lock_tables

    open_table

1. 排他鎖使用
  lock_table_names
  MDL_context::acquire_locks
2. 共享鎖使用
  open_table_get_mdl_lock
  MDL_context::try_acquire_lock

2.4 阻塞

  下面進入session2. 因為session1拿到了pp表的share讀鎖,但session2的alter操作的mdl_request類型是:MDL_INTENTION_EXCLUSIVE,兼容性判斷是互斥,所以ddl被阻塞。

while (!m_wait_status && !thd_killed(thd) &&
         wait_result != ETIMEDOUT && wait_result != ETIME)
  {
    wait_result= mysql_cond_timedwait(&m_COND_wait_status, &m_LOCK_wait_status,abs_timeout);
  }

說明:上面的這段代碼,session2進入阻塞狀態,等待超時或者mdl_wait中的條件變量。

 

2.5 喚醒

      session1進行提交動作,commit。 然后session1 release mdl_lock,最后wake up session2.  session 2完成alte操作。

    MDL_context::release_lock();
        lock->remove_ticket();    
            reschedule_waiters();
            while ((ticket= it++))
        {
          if (can_grant_lock(ticket->get_type(), ticket->get_ctx()))
           {
              if (! ticket->get_ctx()->m_wait.set_status(MDL_wait::GRANTED))

    MDL_wait::set_status();
        mysql_cond_signal(&m_COND_wait_status);

說明: commit操作,釋放session 1持有的mdl事務鎖,然后遍歷wait隊列,判斷兼容性測試,最后wakeup session2.

 

總結: 根據上面的測試,我們看到,mdl的機制和表鎖的機制基本一致性,但從上面的測試和源碼的設計上,也看到MySQL表鎖,mdl鎖令人蛋疼的地方。

 

3. 蛋疼的鎖

下面簡單介紹下MySQL鎖令人蛋疼的兩個地方:

 

1. 事務開始begin transaction的位置

  •   MySQL的設計:在設置的autocommit=0;read_commited的時候,無論session的第一條語句是select還是dml,都開始一個事務,然后直到commit,所持有的MDL鎖也一直維持到commit結束。
  •   Oracle的設計:在session的第一條更新語句發起時,才創建transaction,在讀多的系統上,減少了阻塞的發生可能性。特別是在開發人員發起select語句時,認為沒有更新,就不再commit。但在MySQL上,發起select語句,而忘記commit,是非常危險的。

 

2. ddl語句阻塞

  •   MySQL的設計:ddl語句發起時,如果無法獲取排他鎖,那么ddl將進入阻塞狀態,但由於是queue的設計,就阻塞了后續所有的dml和selec操作,在高並發系統上,可能會引起雪崩。
  •       Oracle的設計:在oracle 11g之前,ddl語句是fast fail的,不進入阻塞狀態,所以繁忙的表進行ddl操作時,經常遇到的錯誤:ORA-00054: resource busy。但在11g之后雖然可以進行阻塞,並提供了ddl_time_out這樣的參數進行控制,但在高並發的系統上,運維的操作依然不采用,而是fast fail。

 

后話:

  這里可以參照oracle的設計進行改良,ddl語句阻塞相對改源碼來說,比較簡單。而事務開始的位置,牽涉到mvcc和事務隔離級別,改動會比較大。

 

下一篇blog介紹下innodb的鎖。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM