## 前言
在MySQL中,DDL是不屬于事務范疇的,如果事務和DDL并行執行,操作相關聯的表的話,會出現各種意想不到問題,如[事務特性被破壞](http://mysql.taobao.org/monthly/2015/11/04/bug%E5%9C%B0%E5%9D%80)、[binlog順序錯亂](http://bugs.mysql.com/bug.php?id=989)等,為了解決類似這些問題,MySQL在5.5.3引入了MDL鎖(Metadata Locking),關于其設計思路可以參考這兩個worklog:[WL#3726](http://dev.mysql.com/worklog/task/?id=3726)?和?[WL#4284](http://dev.mysql.com/worklog/task/?id=4284)。本篇從代碼實現角度對MDL進行分析。
## 重要數據結構
MDL 是在 MySQL server 層實現的一個模塊,通過對外接口和server層其它模塊進行交互,在sql/mdl.h和sql/mdl.cc中實現。
1. `enum_mdl_type`,枚舉類型,表示MDL鎖的類型,目前一共9種
~~~
* MDL_INTENTION_EXCLUSIVE IX // 意向X鎖,只用于scope 鎖
* MDL_SHARED S // 只能讀metadata,當能讀寫數據,如檢查表是否存在時用這個鎖
* MDL_SHARED_HIGH_PRIO SH // 高優先級S鎖,可以搶占X鎖,只能讀metadata,不能讀寫數據,用于填充INFORMATION_SCHEMA,或者show create table時
* MDL_SHARED_READ SR // 可以讀表數據,select語句,lock table xxx read 都用這個
* MDL_SHARED_WRITE SW // 可以更新表數據,insert,update,delete,lock table xxx write, select for update,
* MDL_SHARED_UPGRADABLE SU // 可升級鎖,可以升級為SNW或者X鎖,ALTER TABLE第一階段會用到
* MDL_SHARED_NO_WRITE SNW // 可升級鎖,其它線程能讀metadata,數據可讀不能讀,持鎖者可以讀寫,可以升級成X鎖,ALTER TABLE的第一階段
* MDL_SHARED_NO_READ_WRITE SNRW // 可升級鎖,其它線程能讀metadata,數據不能讀寫,持鎖者可以讀寫,可以升級成X鎖,LOCK TABLES xxx WRITE
* MDL_EXCLUSIVE X // 排它鎖,禁止其它線程的所有請求,CREATE/DROP/RENAME TABLE
~~~
2. `enum_mdl_duration`,枚舉類型,表示持有MDL鎖的時間
~~~
* MDL_STATEMENT // 語句范圍的,語句結束自動釋放
* MDL_TRANSACTION // 事務范圍的,事務結束時自動釋放
* MDL_EXPLICIT // 顯式鎖,由lock tables xxx read 這種獲取,需要通過unlock tables釋放
~~~
3. `MDL_key`, 對MDL鎖的一個標識,是個三元組:namespace + db_name + table_name
~~~
* m_ptr // 字符串數組,三元組就存在這里
- enum_mdl_namespace // 內部定義的一個枚舉類型,表示加鎖對象的類型
* GLOBAL // 全局讀鎖,FLUSH TABLES WITH READ LOCK
* SCHEMA // 數據庫鎖
* TABLE // 表鎖
* FUNCTION // 函數鎖
* PROCEDURE // 存儲過程
* TRIGGER // 觸發器
* EVENT // event事件
* COMMIT // 全局commit鎖,FLUSH TABLES WITH READ LOCK
~~~
4. `MDL_request`, 線程的鎖請求,這個會發送給MDL子系統,包含加鎖對象(MDL_key)、加什么類型鎖(enum_mdl_type)、鎖持有時間(enum_mdl_duration)等信息
~~~
* type // 類型是enum_mdl_type,表示鎖請求的類型
* duration // 類型是enum_mdl_duration,表示鎖的持有時間
* next_in_list // 當前線程中下一個MDL_request指針,和prev_in_list一起所有MDL_request串起來,形成雙向鏈表
* prev_in_list // 見上
* ticket // 加鎖成功后,MDL模塊會返回一個ticket
* key // MDL_key
~~~
5. `MDL_ticket`, MDL子系統內部對加鎖請求或已獲得鎖的表示,對MDL來說非常重要,同時是`MDL_wait_for_subgraph`的子類,線程的鎖等待圖就通過ticket構建出來。
~~~
* next_in_context // 和prev_in_context一起構造在當前context下所有的ticket雙向鏈表
* prev_in_context // 見上
* next_in_lock // 和prev_in_lock一起構造當前MDL_lock的等待和持有ticket雙向鏈表
* prev_in_lock // 見上
- has_pending_conflicting_lock // 當前ticket的鎖類型是否和對應MDL鎖的等待隊列中的鎖沖突
- is_upgradable_or_exclusive // 是否是可以升級或者互斥鎖
- has_stronger_or_equal_type // 當前ticket對應的鎖和指定的鎖比較是否更強(如X比S強)
- is_incompatible_when_granted // 是否能加鎖
- is_incompatible_when_waiting // 是否比等待隊列中的tciket類型優先級更高
- accept_visitor // 死鎖檢測用到
- get_deadlock_weight // 拿一個死鎖權重,死鎖檢測用
* m_type // 鎖類型
* m_duration // 持有時間,debug 模式下有效
* m_ctx // 指向所屬context
* m_lock // 指向請求的鎖對象
~~~
6. `MDL_wait`,鎖等待實現,當拿不到鎖時就要進入等待,等待的結果也存在這里面
~~~
- enum_wait_status // 鎖等待退出時的狀態
* EMPTY // 初始化值
* GRANTED // 加鎖成功,拿到鎖
* VICTIM // 等待的時候,死鎖檢測發現死鎖,當前線程選為victim,加鎖失敗
* TIMEOUT // 加鎖超時,加鎖失敗
* KILLED // 連接被kill,加鎖失敗
- timed_wait // 等待的實現,條件變量+超時
~~~
7. `MDL_context`,在MDL子系統中,對應一個線程,thd和MDL系統交互就通過這個類實現
~~~
- try_acquire_lock // 嘗試加鎖,加鎖失敗就返回,沒有死鎖檢測
- acquire_lock // 加一個鎖,和上面的區別是多了死鎖檢測
- acquire_locks // 一次性加多個排它鎖,要么成功,要么全失敗
- upgrade_shared_lock // 升級共享鎖
- clone_ticket // clone 出一個 ticket
- release_all_locks_for_name // 把當前線程對某個對象加的所有MDL鎖都釋放掉
- release_lock // 釋放單個鎖
- is_lock_owner // 是否持有某個對象的鎖
- has_lock // 線程是否否在savepoint之前持有指定的鎖
- has_locks // 當前線程是否持有鎖
- set_explicit_duration_for_all_locks // 鎖的時間范圍都置為顯式
- set_transaction_duration_for_all_locks // 鎖的時間范圍都置為事務
- set_lock_duration // 設置鎖的時間范圍
- release_statement_locks // 釋放所有語句時間范圍的鎖
- release_transactional_locks // 釋放所有事務時間范圍的鎖
- rollback_to_savepoint // MDL 鎖回滾到某個savepoint
- get_deadlock_weight // 死鎖時拿一個權重值,以此來判斷對應線程是否要做為victim
* m_wait // 鎖等待
* m_tickets // 指針數組,每個元素指向一個ticket鏈表,分別對應當前線程的語句范圍鎖、事務范圍鎖和顯式鎖
* m_owner // 指向thd的指針
* m_waiting_for // 當前線程正在等待的鎖
- find_ticket // 在當前線程ticket鏈表中查找一個ticket
- release_locks_stored_before // 釋放ticket鏈表上在某個ticket之前所有ticket
- find_deadlock // 檢測是否有死鎖
- visit_subgraph // 和死鎖檢測相關
~~~
8. `MDL_map`,MDL_key 到 MDL_lock 的一個映射,MDL模塊內部用,MDL系統所有鎖都放在這個Map里
~~~
- init // 初始化
- destroy
- find_or_insert // 查找對應的MDL_lock,沒有的話新建并插入
- remove // 移除MDL_lock
* m_partitions // MDL_map 分區
* m_global_lock // 預先分配的全局讀鎖
* m_commit_lock // 預先分配的全局commit鎖
~~~
9. `MDL_map_partition`,為了提升MDL模塊的擴展性,把原本的一個MDL_map分成多個分區,每個分區就是一個?`MDL_map_partition`
~~~
- find_or_insert // 當前分區中查找對應的MDL_lock,沒有的話新建并插入
- remove // 在當前分區中移除MDL_lock
- move_from_hash_to_lock_mutex // 鎖轉換,釋放對分區的加鎖(MDL_map_partition::m_mutex),獲取lock對象的鎖(MDL_lock::m_rwlock)
* m_mutex // 對分區對象的一個保護鎖,修改當前分區要拿到這個鎖
* m_unused_locks_cache // 釋放掉的鎖對象的一個緩存,不用再新分配內存
~~~
10. `MDL_lock`,MDL鎖對象,對于一個key組合(三元組),整個系統只有一個鎖對象,不管請求的key是什么類型,什么時間范圍
~~~
- Ticket_list // 一個內部嵌套類,用于表示當前MDL鎖相關的ticket列表,是個list
- add_ticket // 增加 ticket
- remove_ticket // 移除 ticket
- is_empty // list 是不是空的
- clear_bit_if_not_in_list // 如果當前list中沒有某種類型的ticket,就把對應的位清掉
* m_list // 存放ticket的list
* m_bitmap // 標識當前list中所有ticket類型對應bit位的bitmap,實例是個short類型
* key // 當前鎖對應的MDL_key
* m_rwlock // 對MDL_lock鎖對象的保護鎖
- has_pending_conflicting_lock // 已經授權的ticket是否和等待隊列中的ticket不兼容
- can_grant_lock // 能否加鎖,先和等待隊列進行優先級比較,然后看和已授權的鎖是否兼容
- reschedule_waiters // 當持有當前鎖的ticket釋放或者降級時,會調用下,看等待隊列里是否有ticket此時可以獲取鎖
- remove_ticket // 從指定隊列中移出ticket
- visit_subgraph // 死鎖檢測相關
- needs_notification // 是否需要通知其它線程,當前ticket的鎖情況
- notify_conflicting_locks // 通知其它線程,有一個高級的鎖請求
- hog_lock_types_bitmap // 標識哪種鎖是高級鎖
* m_granted // 已經獲得當前MDL鎖的ticket隊列
* m_waiting // 等待當前MDL鎖的ticket隊列
* m_hog_lock_count // 高級鎖可以連接拿得鎖的個數,超過這個數目就要給低級鎖讓路,防止低級鎖餓死
* m_ref_usage // 和下面2個變量一起,為了提高鎖的擴展性
* m_ref_release
* m_is_destroyed
* m_version // 用于判斷鎖對象是否被放入unsed隊列
* m_map_part // 當前MDL鎖所在的MDL_map 分區
~~~
11. `MDL_scoped_lock`,MDL_lock的一個子類,主要用于對schema加MDL鎖,全局讀鎖和全局commit鎖也是這種類型。
12. `MDL_object_lock`,MDL_lock的另一個子類,除了`MDL_scoped_lock`外,其它都用這個(table、fucntion等),只有?`MDL_object_lock`?可以緩存。
總結下,上面這些類中,`MDL_key`?和?`MDL_request`?都是POD,用來保存信息的;`MDL_context`是MDL子系統和線程交互的接口,一個對象對應一個線程;`MDL_map`、`MDL_map_partition`?和?`MDL_lock`?都是MDL子系統內部實現細節,對server層其它部分不可見;`MDL_ticket`?表示線程對`MDL_lock`持有的某種鎖。
MDL鎖可以從不同角度進行分類:
1. namespace,如GLOBAL、SCHEMA、TABLE等;
2. 鎖的持續時間,如transaction、顯式等;
3. 鎖的兼容性,如S、X、SH等;
4. 鎖的實現類,如scope,object等;
可以看作是MDL鎖的不同屬性,大家不要搞亂了 :-)
## 模塊初始化
整個MDL模塊的初始化是在mysqld啟動時進行的,初始化邏輯在?`MDL_map::init()`?中,做的事情也比較簡單:
1. 初始化兩個全局MDL鎖,global lock 和 commit lock,兩者都是類型都是`MDL_scoped_lock`;
2. 分配`metadata_locks_hash_instances`個map分區,為了解決MDL模塊[全局鎖競爭問題](http://bugs.mysql.com/bug.php?id=66473),在5.6.8對MDL鎖做了分區([commit](https://github.com/mysql/mysql-server/commit/38ed575f03e3b5cf01ae7aabbe3c16355793abbd)),通過`metadata_locks_hash_instances`配置指定用多少個分區,默認是8個。
## 加鎖
加鎖就是server的線程(thd)向MDL模塊獲取對應鎖的ticket過程,加鎖成功標志是MDL模塊返回一個對應的ticket,大致邏輯如下:
1. 線程解析SQL語句,根據語義對每一個表對象設置`TABLE_LIST.mdl_request`,如對普通的select語句?`TABLE_lsit.mdl_request.type`?就是`MDL_SHARED_READ`,可以參考函數`st_select_lex::set_lock_for_tables()`;
2. 線程在打開每個表之前,會請求和這個表對應的MDL鎖,通過?`thd->mdl_context.acquire_lock()`?等接口將`mdl_request`請求發給MDL模塊;
3. MDL模塊根據請求類型和已有的鎖來判斷請求能否滿足,如果可以就返回一個ticket;如果不可以就等待,等待結果可以是成功(別的線程釋放了阻塞的MDL鎖)或者失敗(超時、連接被kill或者被死鎖檢測選為victim);
4. 線程根據MDL模塊的返回結果,決定繼續往下走還是報錯退出。
> 需要注意的是,MDL鎖并不是對表加鎖,而是在加表鎖前的一個預檢查,如果能拿到MDL鎖,下一步加相應的表鎖。
下面對MDL模塊中的主要加鎖方法進行介紹。
MDL_context::find_ticket
這是一個shortcut方法,加鎖的時候先檢查當前線程是否已持有對應key的MDL鎖,并且這個鎖的類型不比請求的低,那么就不需要經過MDL系統再分配一個ticket出來(這個比較復雜,代價較高),直接使用已有的ticket,或者clone一個。
舉個例子:
~~~
1\. begin;
2\. insert into t1 values (1);
3\. insert into t1 values (2);
...
~~~
在上面的語句序列中,執行語句3的時候就不需要再走一遍復雜的加鎖邏輯,因為語句2已經成功拿到t1表的ticket,類型都是MDL_SHARED_WRITE,并且MDL鎖時間范圍也一樣(transaction),這個時候直接用已有的ticket,甚至不用clone。
MDL_context::clone_ticket
經過檢測發現可以直接使用已有的ticket,比如上面的`MDL_context::find_ticket`發現了可以復用的ticket,但是鎖時間范圍不一致,為了確保已經有鎖釋放時,不影響現在請求的,就clone一個ticket。
~~~
1\. begin;
2\. insert into t1 values (1);
3\. handler t1 open;
...
~~~
在上面的語句序列中,執行語句3的時候,發現有可以復用的ticket(語句2的ticket),但是handler需要的MDL鎖是顯式的,而語句2取得的ticket是事務時間范圍的,事務完成后就會釋放,為了避免handler的MDL鎖被提前釋放,因此單獨clone一個出來用。
MDL_context::try_acquire_lock_impl
無等待的加鎖,如果發現有沖突導致加鎖失敗,直接退出。會先調用`MDL_context::find_ticket`看是否有可以復用的ticket,有的話就返回成功,如果沒有就看能否加鎖,能加的話也返回成功,不能加也直接返回(同時返回一個ticket給調用者)。
MDL_context::acquire_lock
主加鎖函數,調試MDL鎖相關問題時,給這個函數加斷點比較有效。先調用`MDL_context::try_acquire_lock_impl`,如果加鎖失敗就進入等待加鎖邏輯:
1. 將`MDL_context::try_acquire_lock_impl`返回的ticket放進MDL_lock的等待隊列;
2. 觸發一次死鎖檢測(后面會詳細介紹);
3. 進入等待,這個時候如果我們`show processlist`就會看到”Waiting for table metadata lock”之類state。等待又分為2種:
* 定時檢查等待: 如果當前請求的鎖是比較高級的(對于`MDL_object_lock`是比MDL_SHARED_NO_WRITE類型更高,對于`MDL_scoped_lock`是MDL_SHARED類型),就會每秒給其它持有當前鎖的線程(并且這些連接持有的鎖等級比較低)發信號,通知其釋放鎖,然后再檢查是否鎖已拿到;
* 一直等待,直到超時;
4. 檢查步驟3的等待結果,可以是GRANTED(拿到鎖)、VICTIM(被死鎖檢測算法選為受害者)、TIMEOUT(加鎖超時)、KILLED(連接被kill)。拿到鎖返回成功,其它返回失敗。
> 鎖等待是靠`MDL_wait`這個類來實現的。
MDL_context::acquire_locks
一次性加多個排它MDL鎖,如果其中一個加鎖失敗,前面已經拿到的鎖也全部釋放。主要用在DDL中,比如`drop table test.t1`這個DDL會一次加3個鎖:
* GLOBAL,MDL_INTENTION_EXCLUSIVE
* test 庫, MDL_INTENTION_EXCLUSIVE
* test.t1 表,MDL_EXCLUSIVE
MDL_context::upgrade_shared_lock
鎖升級,從共享鎖升級到互斥鎖,實現方式是重新申請一個目標鎖,拿到新的ticket后替換老的ticket,用在alter table和create table場景中。
如`create table test.t1(id int) engine = innodb`,會先拿test.t1的MDL_SHARED共享鎖,檢查表是否存在,如果不存在就把鎖升級到MDL_EXCLUSIVE鎖,然后開始建表。
對于`alter table test.t1 add column name varchar(10), algorithm=copy;`,alter用copy到臨時的方式來做。整個過程中MDL順序是這樣的:
1. 剛開始打開表的時候,用的是 MDL_SHARED_UPGRADABLE 鎖;
2. 拷貝到臨時表過程中,需要升級到 MDL_SHARED_NO_WRITE 鎖,這個時候其它連接可以讀,不能更新;
3. 拷貝完在交換表的時候,需要升級到是MDL_EXCLUSIVE,這個時候是禁止讀寫的。
所以在用copy算法alter表過程中,會有2次鎖升級。
MDL_ticket::downgrade_lock
和`MDL_context::upgrade_shared_lock`對應的鎖降級,從互斥鎖降級到共享鎖,實現比較簡單,直接把鎖類型改為目標類型(不用重新申請)。
對于`alter table test.t1 add column name varchar(10), algorithm=inplace`,如果alter使用inplace算法的話,整個過程中MDL加鎖順序是這樣的:
1. 和copy算法一樣,剛開始打開表的時候,用的是 MDL_SHARED_UPGRADABLE 鎖;
2. 在prepare前,升級到MDL_EXCLUSIVE鎖;
3. 在prepare后,降級到MDL_SHARED_UPGRADABLE(其它線程可以讀寫)或者MDL_SHARED_NO_WRITE(其它線程只能讀不能寫),降級到哪種由表的引擎決定;
4. 在alter結束后,commit前,升級到MDL_EXCLUSIVE鎖,然后commit。
可以看到inplace有2次鎖升級,1次降級,不過在alter最耗時的階段是有可能降級到MDL_SHARED_UPGRADABLE的,對其它線程的影響小。
MDL_context::release_locks_stored_before
釋放線程指定ticket鏈表上某個ticket之前的所有ticket,每個context有3個ticket鏈表(statement、transaction和explicit),分別對應當前線程持有的不同時間范圍的MDL鎖。而ticket在鏈表中的順序和時間順序是相反的,后插入的ticket放在鏈表開頭,因此本函數的作用就是把某個時間點之后的ticket都釋放掉,回滾MDL鎖。有幾個指釋放MDL鎖的函數都是基于此實現:
1. `MDL_context::rollback_to_savepoint`,把存檔點之后的所有MDL鎖都釋放掉;
2. `MDL_context::release_transactional_locks`,釋放所有transaction和statement時間范圍的MDL鎖;
3. `MDL_context::release_statement_locks()`,釋放所有statement時間范圍的MDL鎖。
## 死鎖檢測
MDL模塊作為一個集中的資源,收到不同線程發來的鎖請求,而有的鎖是互斥的,不能同時滿足,在這種情況下就會等待,如果線程在此之前已經拿到某些鎖的話,就會形成持有-等待的狀態;而又不可能要求所有線程按某一固定順序請求鎖,這樣就會形成等待循環,也就是死鎖,如下圖所示:

圖1\. 死鎖
線程T1持有M1,然后請求M2,但M2被線程T2持有,并且和T1的請求類型互斥,同時T2請求M1,和T1拿到的鎖互斥,形成死鎖。
在介紹MDL的死鎖檢測之前,先介紹下MDL鎖的兼容矩陣。每種類型的鎖各有2個兼容矩陣,granted matrix 和 waiting matrix,前者表示鎖的兼容性,后者表示鎖的優先級(優先級就是和等待隊列的鎖相比,當前鎖是否能夠進行加鎖嘗試,當前鎖優先級高則可以,低則需進等待隊列)。
矩陣中 ‘+’ 表示兼容,’-‘ 表示不兼容,’0’ 表示不可能存在的場景。
`MDL_scoped_lock`,支持IX,S和X鎖(關于鎖的縮寫可以看第一節)。
1. granted matrix
~~~
| Type of active |
Request | scoped lock |
type | IS(*) IX S X |
---------+------------------+
IS | + + + + |
IX | + + - - |
S | + - + - |
X | + - - - |
~~~
2. waiting matrix
~~~
| Pending |
Request | scoped lock |
type | IS(*) IX S X |
---------+-----------------+
IS | + + + + |
IX | + + - - |
S | + + + - |
X | + + + + |
~~~
IS鎖雖然列了出來,但是代碼里并沒有實現這個鎖,因為IS和所有的鎖類型都兼容(也可以理解為每次鎖請求都默認會額外有一個IS鎖)。
`MDL_object_lock`,支持S、SH、SR、SW、SU、SNW、SNRW 和 X鎖。
1. granted matrix
~~~
Request | Granted requests for lock |
type | S SH SR SW SU SNW SNRW X |
----------+----------------------------------+
S | + + + + + + + - |
SH | + + + + + + + - |
SR | + + + + + + - - |
SW | + + + + + - - - |
SU | + + + + - - - - |
SNW | + + + - - - - - |
SNRW | + + - - - - - - |
X | - - - - - - - - |
SU -> X | - - - - 0 0 0 0 |
SNW -> X | - - - 0 0 0 0 0 |
SNRW -> X | - - 0 0 0 0 0 0 |
~~~
關于’0’的情況說明下,比如對于SU鎖來說其和自身是不兼容的,不可能有2個線程對同一個對象都持有SU鎖,所以就不存在當一個線程進行鎖升級時,另一個線程持有SU。其它’0’的情況類似。
2. waiting matrix
~~~
Request | Pending requests for lock |
type | S SH SR SW SU SNW SNRW X |
----------+---------------------------------+
S | + + + + + + + - |
SH | + + + + + + + + |
SR | + + + + + + - - |
SW | + + + + + - - - |
SU | + + + + + + + - |
SNW | + + + + + + + - |
SNRW | + + + + + + + - |
X | + + + + + + + + |
SU -> X | + + + + + + + + |
SNW -> X | + + + + + + + + |
SNRW -> X | + + + + + + + + |
~~~
注意 SH 比 X 鎖的優先級還高,正是其高優先級(high priority)的體現。
在MDL系統中,資源關系是這樣的:
1. 線程和鎖的關系通過ticket建立;
2. 每個線程有3個ticket鏈表,分別對應當前持有的statement鎖、transaction鎖和顯式鎖,放在?`MDL_context::m_tickets`中;對于當前線程正在等待的鎖只有一個,用`MDL_context::m_waiting_for`表示;
3. 每個MDL鎖有2個ticket鏈表,分別對應已經獲得鎖的線程(`MDL_lock::m_granted`)和等待鎖的線程(`MDL_lock::m_waiting`);
4. 線程的ticket鏈表和MDL鎖的ticket鏈表一起構成了MDL系統的等待關系圖,死鎖檢測就是搜索這張圖,看是否有環路。
為了描述的簡潔,我們將線程和MDL鎖的ticket鏈表都簡化為1個,如下圖2矩陣的,橫線表示線程的鏈表,縱向表示MDL鎖的鏈表,有色彩的交點表示一個ticket,橘黃色表示連接已經拿到鎖,青色表示正在等待的鎖,圖中MDL上鎖的類型不兼容,形成持有等待回路——死鎖。

圖2\. MDL死鎖
下面介紹下死鎖檢測中的函數。
MDL_context::find_deadlock
這個是死鎖檢測的入口,線程在`MDL_context::acquire_lock`嘗試拿鎖失敗,進入等待之前,會調用這個函數進行一次死鎖檢測。
函數會行循環檢測,直到發現沒有死鎖(每輪檢測會去掉等待圖中一條邊,但不保證能解決死鎖,所以需要循環),或者當前線程被選為victim才退出。
MDL_context::visit_subgraph
看當前線程是否有鎖等待`MDL_context::m_waiting_for`,有的話就沿著ticket搜下去,沒有就退出。
MDL_ticket::accept_visitor
這個方法看起來沒有什么實際內容,只是簡單調用`MDL_lock::visit_subgraph`,其實可以看作是搜索視角的轉換,從?`MDL_context`?經過?`MDL_ticket`?進入到?`MDL_lock`,代碼邏輯顯得比較清晰。
MDL_lock::visit_subgraph
這個是死鎖檢測核心邏輯:
1. 先給搜索深度加1,然后判斷是否超過最大搜索深度(MAX_SEARCH_DEPTH= 32),超過就無條件認為有死鎖,退出;
2. 遍歷當前鎖的ticket鏈表,看ticket對應的線程是否和死鎖檢測的發起線程是同一個,如果是則說明有回路,退出(相當于做了一層的廣度搜索);
3. 從頭開始遍歷當前鎖的ticket鏈表,對每個ticket對應的線程,遞歸調用`MDL_context::visit_subgraph`(深度搜索)。
整個死鎖檢測邏輯是一個加了深度限制的深搜,中間同時多了一層廣搜。
Deadlock_detection_visitor?是死鎖檢測中重要的輔助類,主要負責:
1. 記錄死鎖檢測的起始線程;
2. 記錄被選做victim的線程;
3. 在檢測到死鎖,深搜一層層退出的時候,會依次檢查回路上各線程的死鎖權重,選擇權重最小的做為最終的victim(權重由鎖的類型決定)。
## global read lock
相信FTWRL(FLUSH TABLES WITH READ LOCK)這個命令很多人都用過,比如備份時為了獲取SQL線程執行位點或binlog位點,這個命令的目的是阻止新的更新進來和已有事務的提交。就這個命令主要靠MDL鎖來實現,這里用到了2個MDL鎖,namespace分別為`MDL_key::GLOBAL`和`MDL_key::COMMIT`,這2個鎖在整個MDL系統中都是全局唯一的,都是`MDL_scoped_lock`類型。
執行 FTWRL 的線程會請求這2個鎖的MDL_SHARED鎖,并且是顯式的。在所有更新數據的代碼路徑里,除了必須的鎖外,還會額外請求`MDL_key::GLOBAL`鎖的MDL_INTENTION_EXCLUSIVE鎖;在事務提交前,會先請求`MDL_key::COMMIT`鎖的MDL_INTENTION_EXCLUSIVE鎖。對于scope鎖來說,IX鎖和S鎖是不兼容的(參考granted matrix),所以更新和事務提交都被FTWRL擋到了。
Percona Server 實現的相對于 FTWRL 輕量級的backup鎖也是基于MDL實現的,其對MDL_key的 namespace 額外擴展了2個,`MDL_key::BACKUP`和`MDL_key::BINLOG`,對應的2個鎖也是全局唯一的,感興趣的同學可以了解下[backup locks](https://www.percona.com/doc/percona-server/5.6/management/backup_locks.html)。
- 數據庫內核月報目錄
- 數據庫內核月報 - 2016/09
- MySQL · 社區貢獻 · AliSQL那些事兒
- PetaData · 架構體系 · PetaData第二代低成本存儲體系
- MySQL · 社區動態 · MariaDB 10.2 前瞻
- MySQL · 特性分析 · 執行計劃緩存設計與實現
- PgSQL · 最佳實踐 · pg_rman源碼淺析與使用
- MySQL · 捉蟲狀態 · bug分析兩例
- PgSQL · 源碼分析 · PG優化器淺析
- MongoDB · 特性分析· Sharding原理與應用
- PgSQL · 源碼分析 · PG中的無鎖算法和原子操作應用一則
- SQLServer · 最佳實踐 · TEMPDB的設計
- 數據庫內核月報 - 2016/08
- MySQL · 特性分析 ·MySQL 5.7新特性系列四
- PgSQL · PostgreSQL 邏輯流復制技術的秘密
- MySQL · 特性分析 · MyRocks簡介
- GPDB · 特性分析· Greenplum 備份架構
- SQLServer · 最佳實踐 · RDS for SQLServer 2012權限限制提升與改善
- TokuDB · 引擎特性 · REPLACE 語句優化
- MySQL · 專家投稿 · InnoDB物理行中null值的存儲的推斷與驗證
- PgSQL · 實戰經驗 · 旋轉門壓縮算法在PostgreSQL中的實現
- MySQL · 源碼分析 · Query Cache并發處理
- PgSQL · 源碼分析· pg_dump分析
- 數據庫內核月報 - 2016/07
- MySQL · 特性分析 ·MySQL 5.7新特性系列三
- MySQL · 特性分析 · 5.7 代價模型淺析
- PgSQL · 實戰經驗 · 分組TOP性能提升44倍
- MySQL · 源碼分析 · 網絡通信模塊淺析
- MongoDB · 特性分析 · 索引原理
- SQLServer · 特性分析 · XML與JSON應用比較
- MySQL · 最佳實戰 · 審計日志實用案例分析
- MySQL · 性能優化 · 條件下推到物化表
- MySQL · 源碼分析 · Query Cache內部剖析
- MySQL · 捉蟲動態 · 備庫1206錯誤問題說明
- 數據庫內核月報 - 2016/06
- MySQL · 特性分析 · innodb 鎖分裂繼承與遷移
- MySQL · 特性分析 ·MySQL 5.7新特性系列二
- PgSQL · 實戰經驗 · 如何預測Freeze IO風暴
- GPDB · 特性分析· Filespace和Tablespace
- MariaDB · 新特性 · 窗口函數
- MySQL · TokuDB · checkpoint過程
- MySQL · 特性分析 · 內部臨時表
- MySQL · 最佳實踐 · 空間優化
- SQLServer · 最佳實踐 · 數據庫實現大容量插入的幾種方式
- 數據庫內核月報 - 2016/05
- MySQL · 引擎特性 · 基于InnoDB的物理復制實現
- MySQL · 特性分析 · MySQL 5.7新特性系列一
- PostgreSQL · 特性分析 · 邏輯結構和權限體系
- MySQL · 特性分析 · innodb buffer pool相關特性
- PG&GP · 特性分析 · 外部數據導入接口實現分析
- SQLServer · 最佳實踐 · 透明數據加密在SQLServer的應用
- MySQL · TokuDB · 日志子系統和崩潰恢復過程
- MongoDB · 特性分析 · Sharded cluster架構原理
- PostgreSQL · 特性分析 · 統計信息計算方法
- MySQL · 捉蟲動態 · left-join多表導致crash
- 數據庫內核月報 - 2016/04
- MySQL · 參數故事 · innodb_additional_mem_pool_size
- GPDB · 特性分析 · Segment事務一致性與異常處理
- GPDB · 特性分析 · Segment 修復指南
- MySQL · 捉蟲動態 · 并行復制外鍵約束問題二
- PgSQL · 性能優化 · 如何瀟灑的處理每天上百TB的數據增量
- Memcached · 最佳實踐 · 熱點 Key 問題解決方案
- MongoDB · 最佳實踐 · 短連接Auth性能優化
- MySQL · 最佳實踐 · RDS 只讀實例延遲分析
- MySQL · TokuDB · TokuDB索引結構--Fractal Tree
- MySQL · TokuDB · Savepoint漫談
- 數據庫內核月報 - 2016/03
- MySQL · TokuDB · 事務子系統和 MVCC 實現
- MongoDB · 特性分析 · MMAPv1 存儲引擎原理
- PgSQL · 源碼分析 · 優化器邏輯推理
- SQLServer · BUG分析 · Agent 鏈接泄露分析
- Redis · 特性分析 · AOF Rewrite 分析
- MySQL · BUG分析 · Rename table 死鎖分析
- MySQL · 物理備份 · Percona XtraBackup 備份原理
- GPDB · 特性分析· GreenPlum FTS 機制
- MySQL · 答疑解惑 · 備庫Seconds_Behind_Master計算
- MySQL · 答疑解惑 · MySQL 鎖問題最佳實踐
- 數據庫內核月報 - 2016/02
- MySQL · 引擎特性 · InnoDB 文件系統之文件物理結構
- MySQL · 引擎特性 · InnoDB 文件系統之IO系統和內存管理
- MySQL · 特性分析 · InnoDB transaction history
- PgSQL · 會議見聞 · PgConf.Russia 2016 大會總結
- PgSQL · 答疑解惑 · PostgreSQL 9.6 并行查詢實現分析
- MySQL · TokuDB · TokuDB之黑科技工具
- PgSQL · 性能優化 · PostgreSQL TPC-C極限優化玩法
- MariaDB · 版本特性 · MariaDB 的 GTID 介紹
- MySQL · 特性分析 · 線程池
- MySQL · 答疑解惑 · mysqldump tips 兩則
- 數據庫內核月報 - 2016/01
- MySQL · 引擎特性 · InnoDB 事務鎖系統簡介
- GPDB · 特性分析· GreenPlum Primary/Mirror 同步機制
- MySQL · 專家投稿 · MySQL5.7 的 JSON 實現
- MySQL · 特性分析 · 優化器 MRR & BKA
- MySQL · 答疑解惑 · 物理備份死鎖分析
- MySQL · TokuDB · Cachetable 的工作線程和線程池
- MySQL · 特性分析 · drop table的優化
- MySQL · 答疑解惑 · GTID不一致分析
- PgSQL · 特性分析 · Plan Hint
- MariaDB · 社區動態 · MariaDB on Power8 (下)
- 數據庫內核月報 - 2015/12
- MySQL · 引擎特性 · InnoDB 事務子系統介紹
- PgSQL · 特性介紹 · 全文搜索介紹
- MongoDB · 捉蟲動態 · Kill Hang問題排查記錄
- MySQL · 參數優化 ·RDS MySQL參數調優最佳實踐
- PgSQL · 特性分析 · 備庫激活過程分析
- MySQL · TokuDB · 讓Hot Backup更完美
- PgSQL · 答疑解惑 · 表膨脹
- MySQL · 特性分析 · Index Condition Pushdown (ICP)
- MariaDB · 社區動態 · MariaDB on Power8
- MySQL · 特性分析 · 企業版特性一覽
- 數據庫內核月報 - 2015/11
- MySQL · 社區見聞 · OOW 2015 總結 MySQL 篇
- MySQL · 特性分析 · Statement Digest
- PgSQL · 答疑解惑 · PostgreSQL 用戶組權限管理
- MySQL · 特性分析 · MDL 實現分析
- PgSQL · 特性分析 · full page write 機制
- MySQL · 捉蟲動態 · MySQL 外鍵異常分析
- MySQL · 答疑解惑 · MySQL 優化器 range 的代價計算
- MySQL · 捉蟲動態 · ORDER/GROUP BY 導致 mysqld crash
- MySQL · TokuDB · TokuDB 中的行鎖
- MySQL · 捉蟲動態 · order by limit 造成優化器選擇索引錯誤
- 數據庫內核月報 - 2015/10
- MySQL · 引擎特性 · InnoDB 全文索引簡介
- MySQL · 特性分析 · 跟蹤Metadata lock
- MySQL · 答疑解惑 · 索引過濾性太差引起CPU飆高分析
- PgSQL · 特性分析 · PG主備流復制機制
- MySQL · 捉蟲動態 · start slave crash 診斷分析
- MySQL · 捉蟲動態 · 刪除索引導致表無法打開
- PgSQL · 特性分析 · PostgreSQL Aurora方案與DEMO
- TokuDB · 捉蟲動態 · CREATE DATABASE 導致crash問題
- PgSQL · 特性分析 · pg_receivexlog工具解析
- MySQL · 特性分析 · MySQL權限存儲與管理
- 數據庫內核月報 - 2015/09
- MySQL · 引擎特性 · InnoDB Adaptive hash index介紹
- PgSQL · 特性分析 · clog異步提交一致性、原子操作與fsync
- MySQL · 捉蟲動態 · BUG 幾例
- PgSQL · 答疑解惑 · 詭異的函數返回值
- MySQL · 捉蟲動態 · 建表過程中crash造成重建表失敗
- PgSQL · 特性分析 · 談談checkpoint的調度
- MySQL · 特性分析 · 5.6 并行復制恢復實現
- MySQL · 備庫優化 · relay fetch 備庫優化
- MySQL · 特性分析 · 5.6并行復制事件分發機制
- MySQL · TokuDB · 文件目錄談
- 數據庫內核月報 - 2015/08
- MySQL · 社區動態 · InnoDB Page Compression
- PgSQL · 答疑解惑 · RDS中的PostgreSQL備庫延遲原因分析
- MySQL · 社區動態 · MySQL5.6.26 Release Note解讀
- PgSQL · 捉蟲動態 · 執行大SQL語句提示無效的內存申請大小
- MySQL · 社區動態 · MariaDB InnoDB表空間碎片整理
- PgSQL · 答疑解惑 · 歸檔進程cp命令的core文件追查
- MySQL · 答疑解惑 · open file limits
- MySQL · TokuDB · 瘋狂的 filenum++
- MySQL · 功能分析 · 5.6 并行復制實現分析
- MySQL · 功能分析 · MySQL表定義緩存
- 數據庫內核月報 - 2015/07
- MySQL · 引擎特性 · Innodb change buffer介紹
- MySQL · TokuDB · TokuDB Checkpoint機制
- PgSQL · 特性分析 · 時間線解析
- PgSQL · 功能分析 · PostGIS 在 O2O應用中的優勢
- MySQL · 引擎特性 · InnoDB index lock前世今生
- MySQL · 社區動態 · MySQL內存分配支持NUMA
- MySQL · 答疑解惑 · 外鍵刪除bug分析
- MySQL · 引擎特性 · MySQL logical read-ahead
- MySQL · 功能介紹 · binlog拉取速度的控制
- MySQL · 答疑解惑 · 浮點型的顯示問題
- 數據庫內核月報 - 2015/06
- MySQL · 引擎特性 · InnoDB 崩潰恢復過程
- MySQL · 捉蟲動態 · 唯一鍵約束失效
- MySQL · 捉蟲動態 · ALTER IGNORE TABLE導致主備不一致
- MySQL · 答疑解惑 · MySQL Sort 分頁
- MySQL · 答疑解惑 · binlog event 中的 error code
- PgSQL · 功能分析 · Listen/Notify 功能
- MySQL · 捉蟲動態 · 任性的 normal shutdown
- PgSQL · 追根究底 · WAL日志空間的意外增長
- MySQL · 社區動態 · MariaDB Role 體系
- MySQL · TokuDB · TokuDB數據文件大小計算
- 數據庫內核月報 - 2015/05
- MySQL · 引擎特性 · InnoDB redo log漫游
- MySQL · 專家投稿 · MySQL數據庫SYS CPU高的可能性分析
- MySQL · 捉蟲動態 · 5.6 與 5.5 InnoDB 不兼容導致 crash
- MySQL · 答疑解惑 · InnoDB 預讀 VS Oracle 多塊讀
- PgSQL · 社區動態 · 9.5 新功能BRIN索引
- MySQL · 捉蟲動態 · MySQL DDL BUG
- MySQL · 答疑解惑 · set names 都做了什么
- MySQL · 捉蟲動態 · 臨時表操作導致主備不一致
- TokuDB · 引擎特性 · zstd壓縮算法
- MySQL · 答疑解惑 · binlog 位點刷新策略
- 數據庫內核月報 - 2015/04
- MySQL · 引擎特性 · InnoDB undo log 漫游
- TokuDB · 產品新聞 · RDS TokuDB小手冊
- PgSQL · 社區動態 · 說一說PgSQL 9.4.1中的那些安全補丁
- MySQL · 捉蟲動態 · 連接斷開導致XA事務丟失
- MySQL · 捉蟲動態 · GTID下slave_net_timeout值太小問題
- MySQL · 捉蟲動態 · Relay log 中 GTID group 完整性檢測
- MySQL · 答疑釋惑 · UPDATE交換列單表和多表的區別
- MySQL · 捉蟲動態 · 刪被引用索引導致crash
- MySQL · 答疑釋惑 · GTID下auto_position=0時數據不一致
- 數據庫內核月報 - 2015/03
- MySQL · 答疑釋惑· 并發Replace into導致的死鎖分析
- MySQL · 性能優化· 5.7.6 InnoDB page flush 優化
- MySQL · 捉蟲動態· pid file丟失問題分析
- MySQL · 答疑釋惑· using filesort VS using temporary
- MySQL · 優化限制· MySQL index_condition_pushdown
- MySQL · 捉蟲動態·DROP DATABASE外鍵約束的GTID BUG
- MySQL · 答疑釋惑· lower_case_table_names 使用問題
- PgSQL · 特性分析· Logical Decoding探索
- PgSQL · 特性分析· jsonb類型解析
- TokuDB ·引擎機制· TokuDB線程池
- 數據庫內核月報 - 2015/02
- MySQL · 性能優化· InnoDB buffer pool flush策略漫談
- MySQL · 社區動態· 5.6.23 InnoDB相關Bugfix
- PgSQL · 特性分析· Replication Slot
- PgSQL · 特性分析· pg_prewarm
- MySQL · 答疑釋惑· InnoDB丟失自增值
- MySQL · 答疑釋惑· 5.5 和 5.6 時間類型兼容問題
- MySQL · 捉蟲動態· 變量修改導致binlog錯誤
- MariaDB · 特性分析· 表/表空間加密
- MariaDB · 特性分析· Per-query variables
- TokuDB · 特性分析· 日志詳解
- 數據庫內核月報 - 2015/01
- MySQL · 性能優化· Group Commit優化
- MySQL · 新增特性· DDL fast fail
- MySQL · 性能優化· 啟用GTID場景的性能問題及優化
- MySQL · 捉蟲動態· InnoDB自增列重復值問題
- MySQL · 優化改進· 復制性能改進過程
- MySQL · 談古論今· key分區算法演變分析
- MySQL · 捉蟲動態· mysql client crash一例
- MySQL · 捉蟲動態· 設置 gtid_purged 破壞AUTO_POSITION復制協議
- MySQL · 捉蟲動態· replicate filter 和 GTID 一起使用的問題
- TokuDB·特性分析· Optimize Table
- 數據庫內核月報 - 2014/12
- MySQL· 性能優化·5.7 Innodb事務系統
- MySQL· 踩過的坑·5.6 GTID 和存儲引擎那會事
- MySQL· 性能優化·thread pool 原理分析
- MySQL· 性能優化·并行復制外建約束問題
- MySQL· 答疑釋惑·binlog event有序性
- MySQL· 答疑釋惑·server_id為0的Rotate
- MySQL· 性能優化·Bulk Load for CREATE INDEX
- MySQL· 捉蟲動態·Opened tables block read only
- MySQL· 優化改進· GTID啟動優化
- TokuDB· Binary Log Group Commit with TokuDB
- 數據庫內核月報 - 2014/11
- MySQL· 捉蟲動態·OPTIMIZE 不存在的表
- MySQL· 捉蟲動態·SIGHUP 導致 binlog 寫錯
- MySQL· 5.7改進·Recovery改進
- MySQL· 5.7特性·高可用支持
- MySQL· 5.7優化·Metadata Lock子系統的優化
- MySQL· 5.7特性·在線Truncate undo log 表空間
- MySQL· 性能優化·hash_scan 算法的實現解析
- TokuDB· 版本優化· 7.5.0
- TokuDB· 引擎特性· FAST UPDATES
- MariaDB· 性能優化·filesort with small LIMIT optimization
- 數據庫內核月報 - 2014/10
- MySQL· 5.7重構·Optimizer Cost Model
- MySQL· 系統限制·text字段數
- MySQL· 捉蟲動態·binlog重放失敗
- MySQL· 捉蟲動態·從庫OOM
- MySQL· 捉蟲動態·崩潰恢復失敗
- MySQL· 功能改進·InnoDB Warmup特性
- MySQL· 文件結構·告別frm文件
- MariaDB· 新鮮特性·ANALYZE statement 語法
- TokuDB· 主備復制·Read Free Replication
- TokuDB· 引擎特性·壓縮
- 數據庫內核月報 - 2014/09
- MySQL· 捉蟲動態·GTID 和 DELAYED
- MySQL· 限制改進·GTID和升級
- MySQL· 捉蟲動態·GTID 和 binlog_checksum
- MySQL· 引擎差異·create_time in status
- MySQL· 參數故事·thread_concurrency
- MySQL· 捉蟲動態·auto_increment
- MariaDB· 性能優化·Extended Keys
- MariaDB·主備復制·CREATE OR REPLACE
- TokuDB· 參數故事·數據安全和性能
- TokuDB· HA方案·TokuDB熱備
- 數據庫內核月報 - 2014/08
- MySQL· 參數故事·timed_mutexes
- MySQL· 參數故事·innodb_flush_log_at_trx_commit
- MySQL· 捉蟲動態·Count(Distinct) ERROR
- MySQL· 捉蟲動態·mysqldump BUFFER OVERFLOW
- MySQL· 捉蟲動態·long semaphore waits
- MariaDB·分支特性·支持大于16K的InnoDB Page Size
- MariaDB·分支特性·FusionIO特性支持
- TokuDB· 性能優化·Bulk Fetch
- TokuDB· 數據結構·Fractal-Trees與LSM-Trees對比
- TokuDB·社區八卦·TokuDB團隊