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的鎖。