美文网首页
MySQL MDL实现浅析

MySQL MDL实现浅析

作者: 真之棒2016 | 来源:发表于2020-04-30 23:11 被阅读0次

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加锁过程

加锁过程.png

1.1 MDL子系统组成

这里先说明整个MDL子系统组成,由MDL_map和MDL_lock构成,两个类实现如下。

MDL_map管理整个已经存在在服务器中的MDL锁资源,其中成员变量
m_locks 就是整个服务器MDL锁资源列表,并且每个锁资源lock维护着两个列表,已获锁列表和等待锁列表;
m_global_lock 是全局对象锁;
m_commit_lock 是全局提交锁;

MDL_map.png

MDL_lock则是代表服务器中的每一种MDL锁资源,其中成员变量

key 由<mdl_namespace>+<database name>+<table name>组成,构成区分区分不同锁的标识(MDL_key类),只要是key一样,则说明是同一个锁资源;
m_granted 该锁对象已经持有的锁tickets队列;
m_waiting 等待该lock的tickets队列

MDL_lock.png

1.2 MDL接口

外部需要申请或者是否MDL锁,都是通过相应MDL接口来实现,该类就是MDL_context。MDL_context是MDL子系统和线程交互的接口,一个context对象对应一个线程。

该类主要保存了两个重要成员变量:
m_tickets 保存该线程所有已获得的MDL锁;
m_wait 锁等待状态类

MDL_context.png

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;
}

二、解锁过程

解锁过程.png

MDL锁的释放相对更加简单,首先是在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;
}

相关文章

网友评论

      本文标题:MySQL MDL实现浅析

      本文链接:https://www.haomeiwen.com/subject/cxyxghtx.html