什麼是MDL
MDL,Meta Data lock,元數據鎖,一般稱為字典鎖。字典鎖與數據鎖相對應。字典鎖是為了保護數據對象被改變,一般是一些DDL會對字典對象改變,如兩個TX,TX1先查詢表,然後TX2試圖DROP,字典鎖就會lock住TX2,知道TX1結束提交或回滾)。數據鎖是保護表中的數據,如兩個TX同時更新一行時,先得到row lock的TX會先執行,後者只能等待。
MDL的設計目標
字典鎖在設計的時候是為了數據庫對象的元數據。到達以下3個目的。
1. 提供對並發訪問內存中字典對象緩存(table definatin cache,TDC)的保護。這是系統的內部要求。
2. 確保DML的並發性。如TX1對表T1查詢,TX2同是對表T1插入。
3. 確保一些操作的互斥性,如DML與大部分DDL(ALTER TABLE除外)的互斥性。如TX1對表T1執行插入,TX2執行DROP TABLE,這兩種操作是不允許並發的,故需要將表對象保護起來,這樣可以保證binlog邏輯的正確性。貌似之前的版本存在字典鎖是語句級的,導致 binlog不合邏輯的bug。)
支持的鎖類型
數據庫理論中的基本鎖類型是S、X,意向鎖IS、IX是為了層次上鎖而引入的。比如要修改表中的數據,可能先對表上一個表級IX鎖,然後再對修改的數據上一個行級X鎖,這樣就可以保證其他試圖修改表定義的事物因為獲取不到表級的X鎖而等待。
MySQL中將字典鎖的類型根據不同語句的功能,進一步細分,細分的依據是對字典的操作和對數據的操作。細分的好處是能在一定程度上提高並發效率,因為如果只定義X和S兩種鎖,必然導致兼容性矩陣的局限性。MySQL不遺余力的定義了如下的鎖類型。
名稱
意義
MDL_INTENTION_EXCLUSIVE
意向排他鎖,只用於范圍上鎖
MDL_SHARED
共享鎖,用於訪問字典對象,而不訪問數據。
MDL_SHARED_HIGH_PRIO
只訪問字典對象如DESC TABLE)
MDL_SHARED_READ
共享讀鎖,用於讀取數據如select)
MDL_SHARED_WRITE
共享寫鎖,用於修改數據如update)
MDL_SHARED_NO_WRITE
共享非寫鎖,允許讀取數據,阻塞其他TX修改數據如alter table)
MDL_SHARED_NO_READ_WRITE
用於訪問字典,讀寫數據
不允許其他TX讀寫數據
MDL_EXCLUSIVE
排他鎖,可以修改字典和數據
可以看到MySQL在ALTER TABLE的時候還是允許其他事務進行讀表操作的。需要注意的是讀操作的事物需要在ALTER TABLE獲取MDL_SHARED_NO_WRITE鎖之後,否則無法並發。這種應用場景應該是對一個較大的表進行ALTER時,其他事物仍然可以讀,並發性得到了提高。
鎖的兼容性
鎖的兼容性就是我們經常看到的那些兼容性矩陣,X和S必然互斥,S和S兼容。MySQL根據鎖的類型我們也可以知道其兼容矩陣如下:
IX
S
SH
SR
SW
SNW
SNRW
X
IX
1
1
1
1
1
1
1
1
S
1
1
1
1
1
1
1
0
SH
1
1
1
1
1
1
1
0
SR
1
1
1
1
1
1
0
0
SW
1
1
1
1
1
0
0
0
SNW
1
1
1
1
0
0
0
0
SNRW
1
1
1
0
0
0
0
0
X
1
0
0
0
0
0
0
0
1代表兼容,0代表不兼容。你可能發現X和IX竟然兼容,沒錯,其實這裡的IX已經不是傳統意義上的IX,這個IX是用在范圍鎖上,所以和X鎖不互斥。
數據結構
涉及到的和鎖相關的數據結構主要是如下幾個:
MDL_context:字典鎖上下文。包含一個事物所有的字典鎖請求。
MDL_request:字典鎖請求。包含對某個對象的某種鎖的請求。
MDL_ticket:字典鎖排隊。MDL_request就是為了獲取一個ticket。
MDL_lock:鎖資源。一個對象全局唯一。可以允許多個可以並發的事物同時獲得。
涉及到的源碼文件主要是sql/mdl.cc
鎖資源
鎖資源在系統中是共享的,即全局的,存放在static MDL_map mdl_locks;的hash鏈表中,對於數據庫中的一個對象,其hashkey必然是唯一的,對應一個鎖資源。多個事務同時對一張表操作時,申請的 lock也是同一個內存對象。獲取mdl_locks中的lock需要通過全局互斥量保護起來 mysql_mutex_lock(&m_mutex); m_mutex是MDL_map的成員。
上鎖流程
一個會話連接在實現中對應一個THD實體,一個THD對應一個MDL_CONTEXT,表示需要的mdl鎖資源,一個MDL_CONTEXT中包含多個MDL_REQUEST,一個MDL_REQUEST即是對一個對象的某種類型的lock請求。每個mdl_request上有一個ticket對象,ticket中包含lock。
上鎖的也就是根據MDL_REQUEST進行上鎖。
- Acquire_lock:
- if (mdl_request contains the needed ticket )
- return ticket;
- End if;
- Create a ticket;
- If (!find lock in lock_sys)
- Create a lock;
- End if
- If (lock can be granted to mdl_request)
- Set lock to ticket;
- Set ticket to mdl_request;
- Else
- Wait for lock
- End if
稍微解釋下,首先是在mdl_request本身去查看有沒有相等的或者stronger的ticket,如果存在,則直接使用。否則創建一個 ticket,查找上鎖對象對應的lock,沒有則創建。檢查lock是否可以被賦給本事務,如果可以直接返回,否則等待這個lock;
鎖等待與喚醒
字典對象的鎖等待是發生在兩個事物對同一對象上不兼容的鎖導致的。當然,由於lock的唯一性,先到先得,後到的只能等待。
如何判斷一個lock是否可以grant給一個TX?這需要結合lock結構來看了,lock上有兩個成員,grant和wait,grant代表此 lock允許的事物都上了哪些鎖,wait表示等待的事務需要上哪些鎖。其判斷一個事物是否可以grant的邏輯如下:
- If(compatible(lock.grant, tx.locktype))
- If (compatible(lock.wait, tx.locktype))
- return can_grant;
- End if
- End if
即首先判斷grant中的鎖類型和當前事務是否兼容,然後判斷wait中的鎖類型和當前事務是否兼容。細心的話,會想到,wait中的鎖類型是不需要和當前事務進行兼容性比較的,這是不是說這個比較是多余的了?其實也不是,因為wait的兼容性矩陣和上面的矩陣是不一樣的,wait的兼容性矩陣感覺是在 DDL等待的情況下,防止DML繼續進來wait矩陣就不寫出來了,大家可以去代碼裡看下)。
比如:
TX1 TX2 TX3
SELECT T1
DROP T1
SELECT T1
這時候TX2會阻塞,TX3也會阻塞,被TX2阻塞,也就是說被wait的事件阻塞了,這樣可能就是為了保證在DDL等待時,禁止再做DML了,因為在DDL面前,DML顯得確實不是那麼重要了。
如何喚醒被等待的事務呢?比如喚醒TX2,當TX1結束時,會調用release_all_locks_for_name,對被鎖住的事務進行喚醒,具體操作封裝在reschedule_waiters函數中,重置等待時間的標記位進行喚醒,重點代碼如下:
- if (can_grant_lock(ticket->get_type(), ticket->get_ctx()))
- {
- if (! ticket->get_ctx()->m_wait.set_status(MDL_wait::GRANTED))
- {
- /*
- Satisfy the found request by updating lock structures.
- It is OK to do so even after waking up the waiter since any
- session which tries to get any information about the state of
- this lock has to acquire MDL_lock::m_rwlock first and thus,
- when manages to do so, already sees an updated state of the
- MDL_lock object.
- */
- m_waiting.remove_ticket(ticket);
- m_granted.add_ticket(ticket);
- }
今天把mdl系統總體上看了一下,對鎖的請求、等待以及喚醒有了初步了解。並發性的問題是最難調試的,大家如果想做鎖方面的實驗,可以利用VS調試中的凍結線程的功能,這樣就可以確保並發情況控制完全按照你設計思路去呈現。