整理一下最近读的MDL源码
Fri, Apr 4, 2014以下都是个人理解, 如有疏漏请斧正 另, 因为理解不深, 将忽略锁级别以及锁共享的细节
MDL
MDL (Metadata lock), 除了正常的Condition var提供的功能外, 还额外提供了 1. 不同的锁级别. 在不冲突的情况下, 允许共享资源 2. 死锁检查和处理 3. 记录等待状态, 是死锁检查的基础
模型
MDL_lock
表示Mysqld中的一个资源(库/表/…) 存储在全局结构 mdl_locks (MDL_map)
中, mdl_locks
内有m_partitions
(锁的分区), 用来分散查找lock时的竞争
MDL_context
为MDL上下文接口, 表示一个资源竞争者, THD实现了这个接口, 即一个Mysqld的线程可以是MDL_lock
的资源竞争者
MDL_ticket
表示MDL_lock
的许可或请求, 会同时挂在两处:
- 挂在所属
MDL_Context
中, 通过MDL_ticket.next_in_context/prev_in_context
组织链表 - 挂在
MDL_lock
的队列中, 通过MDL_ticket.next_in_lock/prev_in_lock
组织链表.MDL_lock
的队列分为两种, 一个MDL_ticket
可能会挂在其中之一- 挂在
MDL_lock
的等待队列(MDL_lock.m_waiting
)中, 表示MDL_ticket
的owner (MDL_context
)正在等待该资源(MDL_lock
) - 挂在
MDL_lock
的已许可队列(MDL_lock.m_granted
)中, 表示MDL_ticket
的owner (MDL_context
)已经获得该资源(MDL_lock
)
- 挂在
总结一下, MDL_context
和MDL_ticket
的关系是一对多, 一个竞争者可以同时申请/获得多个资源的许可; MDL_ticket
和MDL_lock
的关系是多对一, 可以同时有多个资源许可在竞争一个资源, 或者多个资源许可可以*有条件*地共享一个资源
如何获得锁
简单分析MDL_context::acquire_lock
方法, 其主要流程是
bool MDL_context::acquire_lock(MDL_request *mdl_request, ulong lock_wait_timeout) {
...
try_acquire_lock_impl(...)
//尝试不等待立刻获得资源, 如果成功直接返回
//以下是等待资源的处理
...
lock->m_waiting.add_ticket(ticket)
//将一个资源申请`ticket`挂入资源`lock`的等待队列`m_waiting`
if (lock->needs_notification(ticket)) {
//如果等待资源时需要通知状态, 则不断轮询并通知
//将忽略此处的细节
...
} else {
//等待资源
//结果可能是获得资源, 或者超时, 或者异常 (比如被死锁检测机制判定死亡)
//`timed_wait`中的实现是等待COND(条件变量)`m_wait.m_COND_wait_status`
wait_status= m_wait.timed_wait(...);
}
//收尾处理
m_tickets[mdl_request->duration].push_front(ticket)
//将资源申请`ticket`挂入`MDL_Context.m_tickets`
...
}
记录等待状态
之前提到了记录等待状态, 在MDL_context::acquire_lock
方法中可以看到如下代码 (上一节未列出)
bool MDL_context::acquire_lock(MDL_request *mdl_request, ulong lock_wait_timeout) {
m_wait.reset_status();
...
will_wait_for(ticket); //其中设置了`m_waiting_for`
if (lock->needs_notification(ticket)) {
...
//等待资源
wait_status= m_wait.timed_wait(m_owner, &abs_timeout, TRUE,
mdl_request->key.get_wait_state_name());
} else {
//等待资源
wait_status= m_wait.timed_wait(m_owner, &abs_timeout, TRUE,
mdl_request->key.get_wait_state_name());
}
done_waiting_for(); //其中清空了`m_waiting_for`
...
}
可以看到MDL_context.m_wait
是用来等待资源的工具类, 其中进行等待处理, 并记录等待资源的状态/结果.
还有一个MDL_context.m_waiting_for
也在记录MDL_context
正在进行的资源申请(MDL_ticket
), 其正在等待某个资源. 实际上m_waiting_for
是冗余的信息, 至于原因源代码中有解释, 此处不冗余说明…
如何释放锁
释放锁, 需要完成下面几个动作:
- 将
ticket
从MDL_lock
的数据结构上卸下来 - 调度选择新的锁占有者
- 将
ticket
从MDL_context
的数据结构上卸下并回收
入口为MDL_context::release_lock
void MDL_context::release_lock(enum_mdl_duration duration, MDL_ticket *ticket)
{
...
lock->remove_ticket(&MDL_lock::m_granted, ticket) {
//将`ticket`从`MDL_lock`的数据结构上卸下来
(this->*list).remove_ticket(ticket);
...
//调度选择新的锁占有者
reschedule_waiters();
}()
//将`ticket`从`MDL_context`的数据结构上卸下并回收
m_tickets[duration].remove(ticket);
MDL_ticket::destroy(ticket);
...
}
下面说明调度的细节
释放锁时的调度
调度函数的入口是MDL_lock::reschedule_waiters
最简单的调度就是从MDL_lock.m_waiting
队列中取出头元素, 直接将资源调度给头元素即可
Mysqld在此基础上添加了一个退让条件:
如果资源连续被高优先级(比如SNW
/SNRW
/X
锁类型)的ticket
获得, 那么退让一步, 允许资源间隔被调度给_低优先级_的ticket
防止其饿死.
用MDL_lock::reschedule_waiters
的代码说就是, 如果MDL_lock
被连续分配给hog_lock_types_bitmap()
中定义的_高优先级_类型的ticket
,连续的次数m_hog_lock_count
超过max_write_lock_count
, 那么开启退让条件, 批准第一个非_高优先级_的ticket
获得资源
死锁检测
死锁检测的入口是MDL_context::find_deadlock
, 本身原理很简单, 但源码写的很复杂= =. 先说明原理, 再对应源码
设当前MDL_context
为图的一个节点A
, 从节点A
出发, 找到A
的正在等待的资源L
(A.m_waiting_for.m_lock
)中的m_granted
里的每一个MDL_ticket
对应的MDL_context
B
, 表示A
正在等待B
释放资源L
. 在图中A
-> B
添加一条有向边
死锁检查的工作就是遍历这张有向图, 检查其是否存在环路
以MDL_context::find_deadlock
入口, 展开一些调用来说明代码
(MDL_context::find_deadlock)
while(1) {
visit_subgraph(visitor) {
m_waiting_for->accept_visitor(visitor) {
m_lock->visit_subgraph(this, visitor) {
...
}()
}()
}()
break if no deadlock
set deadlock victim
break if deadlock victim is current context
}
可以看到find_deadlock
以MDL_context.m_waiting_for.m_lock
为起始点, 不断遍历其有向图, 选出victim. 直到
* 没有发现死锁
* 或自己被选为victim
其使用一个visitor (MDL_wait_for_graph_visitor
) 贯穿遍历过程, 其记录了遍历的过程
再来看MDL_lock::visit_subgraph
, 此函数是以一个MDL_lock
为起点, 来遍历依赖图
MDL_lock::visit_subgraph(MDL_ticket *waiting_ticket, MDL_wait_for_graph_visitor *gvisitor) {
//此处是因为MDL_context.m_waiting_for是冗余信息, 但无法保证更新同步, 带来的额外操作. 忽略此处细节
if (src_ctx->m_wait.get_status() != MDL_wait::EMPTY) {...}
//visitor用来记录遍历层次
//当遍历层次大于MAX_SEARCH_DEPTH(32), 也认为发现死锁
if (gvisitor->enter_node(src_ctx)) {...}
//由于现在是以一个资源(`MDL_lock`)为视角, 之后的检查为了效率, 遍历会从两个方向同时进行, 即检查节点的出度方向(`MDL_lock.m_granted`)和节点的入度方向(`MDL_lock.m_waiting`).
//为了效率, 死锁检测会先检测距离为1的临近节点, 而先不深度遍历图
while ((ticket= granted_it++))
{
if (ticket->get_ctx() != src_ctx &&
ticket->is_incompatible_when_granted(waiting_ticket->get_type()) &&
gvisitor->inspect_edge(ticket->get_ctx()))
{
goto end_leave_node;
}
}
while ((ticket= waiting_it++))
{
/* Filter out edges that point to the same node. */
if (ticket->get_ctx() != src_ctx &&
ticket->is_incompatible_when_waiting(waiting_ticket->get_type()) &&
gvisitor->inspect_edge(ticket->get_ctx()))
{
goto end_leave_node;
}
}
//此处开始, 深度遍历图
granted_it.rewind();
while ((ticket= granted_it++))
{
if (ticket->get_ctx() != src_ctx &&
ticket->is_incompatible_when_granted(waiting_ticket->get_type()) &&
ticket->get_ctx()->visit_subgraph(gvisitor))
{
goto end_leave_node;
}
}
waiting_it.rewind();
while ((ticket= waiting_it++))
{
if (ticket->get_ctx() != src_ctx &&
ticket->is_incompatible_when_waiting(waiting_ticket->get_type()) &&
ticket->get_ctx()->visit_subgraph(gvisitor))
{
goto end_leave_node;
}
}
...
//visitor退栈
gvisitor->leave_node(src_ctx);
...
}
发现死锁后, 会调用Deadlock_detection_visitor::opt_change_victim_to
, 其中进行MDL_context
权重比较, 来选取一个作为victim, 此处忽略细节