author : sufei
源码版本: 5.7.26
字典锁是为了保护元数据,如果简单保护数据库中的元数据,可以直接使用linux系统原生的读写锁,但MySQL并没有这样做,主要是使用这种粗略的RW锁严重影响并发性,对性能有较大的影响。
比如在alter table语句,因为该语句需要修改表的元数据,直接加上x锁,那么整个alter table 语句执行过程中,会阻塞该表上的所有读写操作。并且alter table相对执行时间较长(包含copy数据到新表的时间),极大影响了数据库性能。
所以,在MySQL中将alter table语句分为2个阶段,第一阶段打开相应表,进行copy数据到tmp表(新表结构)时,首先加可升级共享锁,然后升级为SNW,允许其他线程读取数据,但不能更新数据;第二阶段将SNW锁升级为X,然后删除原表,重命名tmp表;最后在提交阶段,释放所有的MDL锁。这一过程中,只有在重命名tmp表阶段才会加上x,其他阶段允许其他线程读取表数据。
所以MySQL中对MDL锁进行了更加细致的划分,并且对语句的执行过程也进行划分,不同阶段加的锁范围不同(通过锁升级机制),以便提高并发性。
本文主要讲解MySQL服务层元数据锁(MDL,也叫字典锁)的实现,通过分析MDL的实现,对以后工作中遇到的有关MDL锁问题有更加准确的认识。
一、MDL加锁过程
加锁过程.png1.1 MDL子系统组成
这里先说明整个MDL子系统组成,由MDL_map和MDL_lock构成,两个类实现如下。
MDL_map.pngMDL_map管理整个已经存在在服务器中的MDL锁资源,其中成员变量
m_locks 就是整个服务器MDL锁资源列表,并且每个锁资源lock维护着两个列表,已获锁列表和等待锁列表;
m_global_lock 是全局对象锁;
m_commit_lock 是全局提交锁;
MDL_lock则是代表服务器中的每一种MDL锁资源,其中成员变量
MDL_lock.pngkey 由<mdl_namespace>+<database name>+<table name>组成,构成区分区分不同锁的标识(MDL_key类),只要是key一样,则说明是同一个锁资源;
m_granted 该锁对象已经持有的锁tickets队列;
m_waiting 等待该lock的tickets队列
1.2 MDL接口
外部需要申请或者是否MDL锁,都是通过相应MDL接口来实现,该类就是MDL_context。MDL_context是MDL子系统和线程交互的接口,一个context对象对应一个线程。
MDL_context.png该类主要保存了两个重要成员变量:
m_tickets 保存该线程所有已获得的MDL锁;
m_wait 锁等待状态类
1.3 加锁过程浅析
整个过程大致分为三部分:
- 检测自身是否已经持有相关mdl锁,如果已经获得了相关锁,或者更高级别锁,则返回成功
- 如果没有,在MDL子系统找到和新加相应锁资源lock
- 检测lock锁资源中,请求时锁类型是否可以granted,
如果可以,更新m_granted,直接加锁成功,返回
如果冲突,则将该ticket加入到锁资源的m_waiting列表中,等待超时或者锁释放被唤醒。
1.4 简化代码
bool MDL_context::acquire_lock(mdl_request, lock_wait_timeout)
{
MDL_ticket *ticket;
//获取mdl锁,如果失败,直接返回true,获锁失败
if (try_acquire_lock_impl(mdl_request, &ticket))
return TRUE;
if (mdl_request->ticket) //获锁成功
{
return FALSE;
}
lock= ticket->m_lock;
//将锁请求ticket加入相应锁资源的等待队列中,以便锁释放时,被其他线程唤醒
lock->m_waiting.add_ticket(ticket);
will_wait_for(ticket);
//超时机制的条件等待,根据返回值不同,相应的超时返回和被唤醒返回
wait_status= m_wait.timed_wait(m_owner, &abs_timeout, TRUE,
mdl_request->key.get_wait_state_name());
done_waiting_for();
if (wait_status != MDL_wait::GRANTED)//如果不是已获得锁返回
{
//从所资源等待队列移除
lock->remove_ticket(this, m_pins, &MDL_lock::m_waiting, ticket);
MDL_ticket::destroy(ticket);
//返回值不同,打印不同错误信息
switch (wait_status)
{
case MDL_wait::VICTIM:
my_error(ER_LOCK_DEADLOCK, MYF(0));
break;
case MDL_wait::TIMEOUT:
my_error(ER_LOCK_WAIT_TIMEOUT, MYF(0));
break;
case MDL_wait::KILLED:
if (get_owner()->is_killed() == ER_QUERY_TIMEOUT)
my_error(ER_QUERY_TIMEOUT, MYF(0));
else
my_error(ER_QUERY_INTERRUPTED, MYF(0));
break;
default:
DBUG_ASSERT(0);
break;
}
return TRUE;
}
DBUG_ASSERT(wait_status == MDL_wait::GRANTED);
/*
唤醒之后,获得了锁,则
1、将锁加入到已获得列表m_tickets
2、给锁请求request附上已获得锁ticket
*/
m_tickets[mdl_request->duration].push_front(ticket);
mdl_request->ticket= ticket;
mysql_mdl_set_status(ticket->m_psi, MDL_ticket::GRANTED);
return FALSE;
}
从上面代码可以看出,相应的逻辑比较清晰,尝试获锁的函数为try_acquire_lock_impl,在未获得锁需要进行锁等待,有关所等待的唤醒,在后面讲解锁释放的时候具体分析。这里具体看一下尝试获锁函数try_acquire_lock_impl。
bool MDL_context::try_acquire_lock_impl(mdl_request,out_ticket)
{
//在自由持有的锁ticket列表中查找,如果存在更强的锁,即获锁成功
if ((ticket= find_ticket(mdl_request, &found_duration)))
{
mdl_request->ticket= ticket;
return FALSE;
}
/*
从mdl子系统中m_locks获取该锁资源lock,如果不存在该锁资源直接新建一个,然后
插入mdl子系统m_locks中,并返回lock
*/
if (!(lock= mdl_locks.find_or_insert(m_pins, key, &pinned)))
{
if (ticket->m_hton_notified)
{
mysql_mdl_set_status(ticket->m_psi, MDL_ticket::POST_RELEASE_NOTIFY);
m_owner->notify_hton_post_release_exclusive(key);
}
MDL_ticket::destroy(ticket);
return TRUE;
}
ticket->m_lock= lock;
/*
检测请求锁类型是否兼容原有锁类型
首先检测等待队列是否兼容,然后检测granted队列是否兼容,这样的目的是为了
避免等待锁夯死,比如如果持锁者队列都为S,等待锁列队存在X,新的锁请求也是S,与持有
者兼容,如果只检测与granted队列是否兼容,直接加锁成功,从而造成等待锁队列中的X
锁请求一直无法获得锁资源,我们需要避免这种情况。
如果兼容,加锁成功
*/
if (lock->can_grant_lock(mdl_request->type, this))
{
//加锁成功,在锁资源granted队列中,添加相应ticket
lock->m_granted.add_ticket(ticket);
//在自身的持有锁队列中,加上该锁
m_tickets[mdl_request->duration].push_front(ticket);
mdl_request->ticket= ticket;
mysql_mdl_set_status(ticket->m_psi, MDL_ticket::GRANTED);
}
else
*out_ticket= ticket;
return FALSE;
}
二、解锁过程
解锁过程.pngMDL锁的释放相对更加简单,首先是在MDL子系统的lock锁资源的granted列表中移除;然后检查等待队列中现在是否可以获得该锁资源,如果可以则将该锁ticket移到granted队列中,并唤醒;最后从自己已有锁队列m_tickets移除该锁。
2.1 简化代码
其实代码相对简单,这里主要分析一下,有关待定锁线程唤醒代码
void MDL_lock::reschedule_waiters()
{
MDL_lock::Ticket_iterator it(m_waiting);
MDL_ticket *ticket;
//循环检测等待队列中的锁请求
while ((ticket= it++)){
//检测该等待锁请求,释放可以现在granted
if (can_grant_lock(ticket->get_type(), ticket->get_ctx()))
{
/*
如果可以,通过ticket获得context,将该等待ticket的锁等待状态设置为GRANTED,
同时set_status函数也会进行唤醒操作
*/
if (! ticket->get_ctx()->m_wait.set_status(MDL_wait::GRANTED))
{
m_waiting.remove_ticket(ticket);
m_granted.add_ticket(ticket);
}
}
}
}
set_status函数函数如下
bool MDL_wait::set_status(enum_wait_status status_arg)
{
bool was_occupied= TRUE;
mysql_mutex_lock(&m_LOCK_wait_status);
if (m_wait_status == EMPTY)
{
was_occupied= FALSE;
m_wait_status= status_arg;
//唤醒等待线程
mysql_cond_signal(&m_COND_wait_status);
}
mysql_mutex_unlock(&m_LOCK_wait_status);
return was_occupied;
}
网友评论