## 前言
在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模块作为一个集中的资源,收到不同线程发来的锁请求,而有的锁是互斥的,不能同时满足,在这种情况下就会等待,如果线程在此之前已经拿到某些锁的话,就会形成持有-等待的状态;而又不可能要求所有线程按某一固定顺序请求锁,这样就会形成等待循环,也就是死锁,如下图所示:
![](https://box.kancloud.cn/2015-11-17_564aa53b544df.png)
图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上锁的类型不兼容,形成持有等待回路——死锁。
![](https://box.kancloud.cn/2015-11-17_564aa53b83f45.png)
图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团队