本文介绍在MDL系统中常用的数据结构及含义,从实现角度讨论MDL的获取机制与死锁检测,分享在实践中如何监控MDL状态。

背景信息

为了满足数据库在并发请求下的事务隔离性和一致性要求,同时针对MySQL插件式多种存储引擎都能发挥作用,MySQL在Server层实现了 Metadata Locking(MDL)机制。例如,可以在事务访问数据库的某种资源时,限制其他并发事务删除该资源。这是一种逻辑意义上的锁,与操作系统内核提供的有限种类mutex不同,MDL可以灵活自定义锁的对象、锁的类型以及不同锁类型的优先级,甚至可以做到在系统不同状态时动态调整不同锁类型的兼容性,极大的方便了数据库对各种查询请求进行合理的并发控制。

基本概念

  • MDL_key
    MDL的对象是采用键值对(key-value)的方式描述的,每一个key值都唯一的代表了锁的对象(value代表数据库的某种资源)。key 是由 MDL_key表示的,用字符串的形式表示对象的名称。MDL_KEY

    完整的字符串由namespace、按层次每一级的名称组成,多种命名空间可以将不同类型的同名对象区分开。命名空间包括GLOBAL、SCHEMA、TABLE、FUNCTION、PROCEDURE等数据库中可以创建的不同对象类型组成。

    对象的名称根据类型的不同可以由多种层次组成。例如,表对象就由数据库名和表名唯一的描述组成。如果是SCHEMA对象,则只有数据库名这一个层次。名称之间用字符串结束符'\0'分隔。因此由这几部分组成的字符串整体就能作为key唯一的表示数据库的某种对象。

  • enum_mdl_type

    对于同一个数据库对象而言,不同的查询也有着不同的访问模式,例如,SELECT语句是读取对象的内容,INSERT/UPDATE语句是修改对象的内容,DDL语句是修改对象的结构和定义。这些语句对于对象的影响程度和并发隔离性的要求不同,因此MySQL定义了不同类型的MDL以及他们之间的兼容性来控制这些语句的并发访问。

    MDL的类型由enum_mdl_type表示,最常用的类型如下:
    • MDL_SHARED(S),可以共享访问对象的元数据,例如,SHOW CREATE TABLE语句。
    • MDL_SHARED_READ(SR),可以共享访问对象的数据,例如,SELECT语句。
    • MDL_SHARED_WRITE(SW),可以修改对象的数据,例如,INSERT/UPDATE语句。
    • MDL_SHARED_UPGRADABLE(SU),可升级的共享锁,后面可升级到更强的锁(例如X锁,阻塞并发访问),例如,DDL的第一阶段。
    • MDL_EXCLUSIVE(X),独占锁,阻塞其他线程对该对象的并发访问,可以修改对象的元数据,例如,DDL的第二阶段。

不同的查询语句通过请求不同类型的MDL,结合不同类型的MDL之间灵活定制的兼容性,就可以对相互冲突的语句进行并发控制。对于同一对象而言,不同类型的MDL之间的默认兼容性如下所示。

不同类型的MDL兼容性。MySQL将锁类型划分为范围锁和对象锁。
  • 范围锁

    范围锁种类较少(IX、S、X),主要用于GLOBAL、COMMIT、TABLESPACE、BACKUP_LOCK 和 SCHEMA命名空间的对象。这几种类型的兼容性简单,主要是从整体上去限制并发操作。例如,全局的读锁来阻塞事务提交、DDL更新表对象的元信息通过请求SCHEMA范围的意向独占锁(IX)来阻塞 SCHEMA 层面的修改操作。

    这几种类型的MDL兼容性关系由两个矩阵定义。对于同一个对象来说,一个是已经获取到的MDL类型对新请求类型的兼容性情况。另一个是未获取到,正在等待的MDL请求类型对新请求类型的兼容性。由于IS(INTENTION_SHARE) 在所有情况下与其他锁都兼容,在MDL系统中可忽略。

            | Type of active   |
    Request |   scoped lock    |
     type   | IS(*)  IX   S  X |
    ---------+------------------+
    IS       |  +      +   +  + |
    IX       |  +      +   -  - |
    S        |  +      -   +  - |
    X        |  +      -   -  - |
            
             |    Pending      |
     Request |  scoped lock    |
      type   | IS(*)  IX  S  X |
     ---------+-----------------+
    IS       |  +      +  +  + |
    IX       |  +      +  -  - |
    S        |  +      +  +  - |
    X        |  +      +  +  + |
            
    Here: "+" -- means that request can be satisfied
     "-" -- means that request can't be satisfied and should wait
  • 对象锁
    对象锁包含的MDL类型比较丰富,应用于数据库绝大多数的基本对象。兼容性矩阵如下所示:
      Request  |  Granted requests for lock            |
       type    | S  SH  SR  SW  SWLP  SU  SRO  SNW  SNRW  X  |
     ----------+---------------------------------------------+
     S         | +   +   +   +    +    +   +    +    +    -  |
     SH        | +   +   +   +    +    +   +    +    +    -  |
     SR        | +   +   +   +    +    +   +    +    -    -  |
     SW        | +   +   +   +    +    +   -    -    -    -  |
     SWLP      | +   +   +   +    +    +   -    -    -    -  |
     SU        | +   +   +   +    +    -   +    -    -    -  |
     SRO       | +   +   +   -    -    +   +    +    -    -  |
     SNW       | +   +   +   -    -    -   +    -    -    -  |
     SNRW      | +   +   -   -    -    -   -    -    -    -  |
     X         | -   -   -   -    -    -   -    -    -    -  |
            
      Request  |         Pending requests for lock          |
       type    | S  SH  SR  SW  SWLP  SU  SRO  SNW  SNRW  X |
     ----------+--------------------------------------------+
     S         | +   +   +   +    +    +   +    +     +   - |
     SH        | +   +   +   +    +    +   +    +     +   + |
     SR        | +   +   +   +    +    +   +    +     -   - |
     SW        | +   +   +   +    +    +   +    -     -   - |
     SWLP      | +   +   +   +    +    +   -    -     -   - |
     SU        | +   +   +   +    +    +   +    +     +   - |
     SRO       | +   +   +   -    +    +   +    +     -   - |
     SNW       | +   +   +   +    +    +   +    +     +   - |
     SNRW      | +   +   +   +    +    +   +    +     +   - |
     X         | +   +   +   +    +    +   +    +     +   + |
            
      Here: "+" -- means that request can be satisfied
            "-" -- means that request can't be satisfied and should wait
    在MDL获取过程中,通过以上两个兼容性矩阵,可以判断当前是否存在与请求的MDL不兼容的granted/pending状态的MDL,来决定该请求是否能被满足,如果不能被满足则进入pending等待状态。
    MDL系统也通过兼容性矩阵来判断锁类型的强弱,方法如下:
    /**
      Check if ticket represents metadata lock of "stronger" or equal type
      than specified one. I.e. if metadata lock represented by ticket won't
      allow any of locks which are not allowed by specified type of lock.
    
      @return true  if ticket has stronger or equal type
              false otherwise.
    */
    bool MDL_ticket::has_stronger_or_equal_type(enum_mdl_type type) const {
      const MDL_lock::bitmap_t *granted_incompat_map =
          m_lock->incompatible_granted_types_bitmap();
    
      return !(granted_incompat_map[type] & ~(granted_incompat_map[m_type]));
    }
    如果type参数类型和该ticket兼容的某种锁类型不兼容,那么type类型更强;否则当前ticket类型相同或更强。

重要数据结构

  • 关系示意图关系示意图
  • MDL_request
    MDL_request表示语句对MDL的请求,由MDL_key 、enum_mdl_type和enum_mdl_duration组成,MDL_key和enum_mdl_type确定了 MDL的对象和锁类型。
    说明 enum_mdl_duration有三种类型,表示MDL的持有周期,有单条语句级的周期、事务级别的和显式周期。

    MDL_request的生命周期是在MDL系统之外,由用户控制的,可以是一个临时变量。但是通过该请求获取到的MDL生命周期是持久的,由 MDL系统控制,并不会随着MDL_request的销毁而释放。

  • MDL_lock

    对于数据库的某一对象,仅存在一个与其名称(MDL_key)对应的锁对象MDL_lock。当数据库的对象初次被访问时,由lock-free HASH在其内存中创建和管理MDL_lock。当后续访问到来时,对于相同对象的访问会引用到同一个MDL_lock。

    MDL_lock中既有当前正在等待该锁对象的m_waiting队列,也有该对象已经授予的m_granted队列,队列中的元素用MDL_ticket表示。

    使用静态bitmap对象组成的MDL_lock_strategy用于存放上述范围锁和对象锁的兼容性矩阵,根据MDL_lock的命名空间便可获取到该锁的兼容性情况。

  • MDL_ticket

    MDL_lock与enum_mdl_type共同组成了MDL_ticket,表示当前线程对数据库对象的访问权限。MDL_ticket在每个查询请求MDL锁时创建,内存由MDL系统分配,在事务结束时销毁。

    MDL_ticket中包含两组指针分别将该线程获取到的所有ticket连接起来和将该ticket参与的锁对象的waiting状态或者granted状态的ticket连接起来。

  • MDL_context

    一个线程获取MDL锁的上下文,每个连接都对应一个,包含了该连接获取到的所有MDL_ticket。按照不同的生命周期存放在各自的链表中,由MDL_ticket_store管理。

    一个连接获得的所有锁根据生命周期可以划分为三种:语句级、事务级和显式锁。语句级和事务级的锁都是具有自动的生命周期和作用范围,它们在一个事务过程中进行积累。语句级的锁在最外层的语句结束后自动释放,事务级的锁在COMMIT、ROLLBACK和ROLLBACK TO SAVEPOINT之后释放,它们不会被手动释放。具有显式生命周期的ticket是为了跨事务和checkpoint的锁所获取的,包括HANDLER SQL locks、LOCK TABLES locks和用户级的锁GET_LOCK()/RELEASE_LOCK()。语句级和事务级的锁会按照时间顺序的反序被加到对应链表的前面,当回滚到某一检查点时,就会从链表的前面将对应的ticket释放出栈,直到检查点创建前最后一个获取到的ticket。

    当一个线程想要获取某个MDL锁时,会优先在自己的MDL_ticket_store中查找是否在事务内已经获取到相同锁对象更强类型的MDL_ticket。因此MDL_ticket_store会提供根据MDL_request请求查找MDL_ticket的接口,一种是在不同生命周期的MDL_ticket链表中查找。如果当前线程获取的MDL_ticket数量超过阈值(默认256),会将所有的MDL_ticket维护在额外的std::unordered_multimap中,来加速查找。

    MDL_ticket_store::MDL_ticket_handle MDL_ticket_store::find(
        const MDL_request &req) const {
    #ifndef DBUG_OFF
      if (m_count >= THRESHOLD) {
        MDL_ticket_handle list_h = find_in_lists(req);
        MDL_ticket_handle hash_h = find_in_hash(req);
    
        DBUG_ASSERT(equivalent(list_h.m_ticket, hash_h.m_ticket, req.duration));
      }
    #endif /*! DBUG_OFF */
      return (m_map == nullptr || m_count < THRESHOLD) ? find_in_lists(req)
                                                       : find_in_hash(req);
    }

MDL获取过程

几乎所有的查询语句(包括DML和DDL第一阶段)都是在parse阶段,由LEX和YACC根据语句的类型给需要访问的表初始化MDL锁请求,例如,SELECT语句是SR,INSERT语句是SW,ALTER TABLE语句是SU。这个过程存在于以下调用栈中:
PT_table_factor_table_ident::contextualize()
  |--SELECT_LEX::add_table_to_list()
    |--MDL_REQUEST_INIT -> MDL_request::init_with_source()
在执行前首先通过open_tables_for_query函数将所有需要访问的表打开,获得TABLE表对象。在这个过程中会先获取MDL锁,然后才获取表资源,防止对同一个表的元信息出现并发读写。对MDL锁的请求都是由当前线程的上下文MDL_context调用MDL_context::acquire_lock进行的,调用栈如下所示:

open_tables_for_query()
  |--open_table() // 循环打开每一个表
    |--open_table_get_mdl_lock()
      |--MDL_context::acquire_lock() // 获取lock,如果遇到锁冲突,那么等待冲突的锁被释放
        |--MDL_context::try_acquire_lock_impl()
  • MDL_context::try_acquire_lock_impl

    此函数包含了各种类型锁(兼容性好的,兼容性差的)的获取以及锁冲突检测,传入参数是当前的MDL_request,输出参数为获取到的MDL_ticket。

    首先根据MDL_request在当前线程已持有的相同对象MDL_ticket中查找类型更强、生命周期相同或不同的ticket。如果已经持有相同生命周期的,则直接返回;持有不同生命周期的,根据ticket克隆出一个相同周期的返回即可。

    根据锁类型的兼容性情况,锁可以划分为unobtrusive和obtrusive,在锁获取过程中分别对应fast path和slow path,表示获取的难易度不同。

    • Unobtrusive(fast path)
      对于一些弱类型(unobtrusive,例如SR/SW等)的MDL请求,由于这部分的请求占绝大多数,且兼容性较好,获取后无需记录是哪个具体的MDL_ticket,只需要记录有多少请求已获取。因此在MDL_lock中使用整型原子变量std::atomic m_fast_path_state来统计该锁授予的所有unobtrusive的锁类型数量,每种unobtrusive的锁有不同的数值表示,固定的bit范围存放该锁类型累加后的结果,相当于用一个longlong类型统计了所有unobtrusive锁的授予个数,同时可以通过CAS无锁修改。另外在m_fast_path_state的高位bit,还存在三个状态指示位,分别是IS_DESTROYED/HAS_OBTRUSIVE/HAS_SLOW_PATH
      /**
         Array of increments for "unobtrusive" types of lock requests for
         per-object locks.
      
         @sa MDL_lock::get_unobtrusive_lock_increment().
      
         For per-object locks:
         - "unobtrusive" types: S, SH, SR and SW
         - "obtrusive" types: SU, SRO, SNW, SNRW, X
      
         Number of locks acquired using "fast path" are encoded in the following
         bits of MDL_lock::m_fast_path_state:
      
         - bits 0 .. 19  - S and SH (we don't differentiate them once acquired)
         - bits 20 .. 39 - SR
         - bits 40 .. 59 - SW and SWLP (we don't differentiate them once acquired)
      
         Overflow is not an issue as we are unlikely to support more than 2^20 - 1
         concurrent connections in foreseeable future.
      
         This encoding defines the below contents of increment array.
      */
      {0, 1, 1, 1ULL << 20, 1ULL << 40, 1ULL << 40, 0, 0, 0, 0, 0},
      根据MDL_request的请求类型,获取对应类型的unobtrusive整型递增值,如果递增值为0,则代表该类型锁为obtrusive,需要走slow path。
      /**
        @returns "Fast path" increment for request for "unobtrusive" type
                  of lock, 0 - if it is request for "obtrusive" type of
                  lock.
      
        @sa Description at method declaration for more details.
      */
      MDL_lock::fast_path_state_t MDL_lock::get_unobtrusive_lock_increment(
          const MDL_request *request) {
        return MDL_lock::get_strategy(request->key)
            ->m_unobtrusive_lock_increment[request->type];
      }
      如果递增值不是0,代表该类型锁是unobtrusive,就会走fast path,直接通过CAS给MDL_lock::m_fast_path_state递增上对应的整型值即可。
      说明 需要确认该对象没有被其他线程以obtrusive的方式锁住,因为unobtrusive和obtrusive的锁类型有些是互斥的,只有在没有obtrusive的锁存在时,其他的unobtrusive锁彼此兼容,才可以不用判断其他线程的锁持有情况直接获取。
      MDL_lock::fast_path_state_t old_state = lock->m_fast_path_state;
      
      do {
        /*
          Check if hash look-up returned object marked as destroyed or
          it was marked as such while it was pinned by us. If yes we
          need to unpin it and retry look-up.
        */
        if (old_state & MDL_lock::IS_DESTROYED) {
          if (pinned) lf_hash_search_unpin(m_pins);
          goto retry;
        }
      
        /*
          Check that there are no granted/pending "obtrusive" locks and nobody
          even is about to try to check if such lock can be acquired.
      
          In these cases we need to take "slow path".
        */
        if (old_state & MDL_lock::HAS_OBTRUSIVE) goto slow_path;
      
        } while (!lock->fast_path_state_cas(
            &old_state, old_state + unobtrusive_lock_increment));
      CAS完成后,设置相关数据结构的状态和引用,将当前MDL_ticket加入到线程的MDL_ticket_store中即可返回:
      /*
        Since this MDL_ticket is not visible to any threads other than
        the current one, we can set MDL_ticket::m_lock member without
        protect of MDL_lock::m_rwlock. MDL_lock won't be deleted
        underneath our feet as MDL_lock::m_fast_path_state serves as
        reference counter in this case.
      */
      ticket->m_lock = lock;
      ticket->m_is_fast_path = true;
      m_ticket_store.push_front(mdl_request->duration, ticket);
      mdl_request->ticket = ticket;
      
      mysql_mdl_set_status(ticket->m_psi, MDL_ticket::GRANTED);
    • Obtrusive(slow path)

      对于一些比较强类型(obtrusive,例如SU/SRO/X等)的MDL请求,会在对应MDL_lock的m_granted链表中存放对应的MDL_ticket。因此在获取时也需要遍历这个链表和其他的bitmap来判断与其他线程已获取或者正在等待的MDL_ticket是否存在锁冲突。

      需要走slow path获取锁之前,当前线程需要将MDL_lock::m_fast_path_state中由当前线程之前通过fast path获取到的锁物化,从bitmap中移出,加入到MDL_lock::m_granted中。因为在MDL_lock::m_fast_path_state中包含的bitmap是无法区分线程的,而当前线程获取的多个锁之间是不构成锁冲突的,所以在通过bitmap判断前,需要确保MDL_lock::m_fast_path_state的ticket都是属于其他线程的。
      /**
        "Materialize" requests for locks which were satisfied using
        "fast path" by properly including them into corresponding
        MDL_lock::m_granted bitmaps/lists and removing it from
        packed counter in MDL_lock::m_fast_path_state.
      */
      void MDL_context::materialize_fast_path_locks() {
        int i;
      
        for (i = 0; i < MDL_DURATION_END; i++) {
          MDL_ticket_store::List_iterator it = m_ticket_store.list_iterator(i);
      
          MDL_ticket *matf = m_ticket_store.materialized_front(i);
          for (MDL_ticket *ticket = it++; ticket != matf; ticket = it++) {
            if (ticket->m_is_fast_path) {
              MDL_lock *lock = ticket->m_lock;
              MDL_lock::fast_path_state_t unobtrusive_lock_increment =
                  lock->get_unobtrusive_lock_increment(ticket->get_type());
              ticket->m_is_fast_path = false;
              mysql_prlock_wrlock(&lock->m_rwlock);
              lock->m_granted.add_ticket(ticket);
              /*
                Atomically decrement counter in MDL_lock::m_fast_path_state.
                This needs to happen under protection of MDL_lock::m_rwlock to make
                it atomic with addition of ticket to MDL_lock::m_granted list and
                to enforce invariant [INV1].
              */
              MDL_lock::fast_path_state_t old_state = lock->m_fast_path_state;
              while (!lock->fast_path_state_cas(
                  &old_state, ((old_state - unobtrusive_lock_increment) |
                               MDL_lock::HAS_SLOW_PATH))) {
              }
              mysql_prlock_unlock(&lock->m_rwlock);
            }
          }
        }
        m_ticket_store.set_materialized();
      }
      在物化完成后,通过当前锁正在等待的ticket 类型(m_waiting)、已经授予的ticket类型(m_granted)和 unobtrusive 的锁类型状态(MDL_lock::m_fast_path_state),结合前面的兼容性矩阵来判断当前请求的锁类型是否能获取到,这个过程主要存在于MDL_lock::can_grant_lock中。
      bool MDL_lock::can_grant_lock(enum_mdl_type type_arg,
                                    const MDL_context *requestor_ctx) const {
        bool can_grant = false;
        bitmap_t waiting_incompat_map = incompatible_waiting_types_bitmap()[type_arg];
        bitmap_t granted_incompat_map = incompatible_granted_types_bitmap()[type_arg];
      
        /*
          New lock request can be satisfied iff:
          - There are no incompatible types of satisfied requests
          in other contexts
          - There are no waiting requests which have higher priority
          than this request.
        */
        if (!(m_waiting.bitmap() & waiting_incompat_map)) {
          if (!(fast_path_granted_bitmap() & granted_incompat_map)) {
            if (!(m_granted.bitmap() & granted_incompat_map))
              can_grant = true;
            else {
              Ticket_iterator it(m_granted);
              MDL_ticket *ticket;
      
              /*
                There is an incompatible lock. Check that it belongs to some
                other context.
              */
              while ((ticket = it++)) {
                if (ticket->get_ctx() != requestor_ctx &&
                    ticket->is_incompatible_when_granted(type_arg))
                  break;
              }
              if (ticket == NULL) /* Incompatible locks are our own. */
                can_grant = true;
            }
          }
        }
        return can_grant;
      }
      在m_waiting和m_granted中,除了通过链表将ticket连接起来,也会使用bitmap收集链表中所有ticket的类型,方便直接进行比较。在m_granted中发现不兼容类型后,还需遍历链表,判断不兼容类型的ticket是否为当前线程获取的,只有为非当前线程获取的情况下,才出现锁冲突。unobtrusive的锁如果能够获取,会直接加入到MDL_lock::m_granted链表中。
  • 锁等待和通知

    上述过程中,如果能顺利获取到MDL_ticket,就完成了MDL的获取,可以继续查询过程。如果无法获取到MDL_ticket(无论是unobtrusive的锁由于obtrusive的锁存在而被迫走slow path,还是本身obtrusive的锁无法获取),就需要进行锁等待,锁等待的过程是不区分是否为unobtrusive还是obtrusive的,会统一进行处理。

    每个线程的MDL_context中包含一个MDL_wait成员,因为锁等待以及死锁检测都是以线程为对象,通过将对应请求的MDL_ticket加入到锁等待者队列中来订阅通知。有一组mutex、condition variable和枚举状态用来完成线程间的等待、通知。等待的状态包括五种:
    // WS_EMPTY since EMPTY conflicts with #define in system headers on some
    // platforms.
    enum enum_wait_status { WS_EMPTY = 0, GRANTED, VICTIM, TIMEOUT, KILLED };
    WS_EMPTY为初始状态,其他的都是等待的结果状态,从命令可以看出,等待的结果可能如下:
    • GRANTED:该线程获取到了等待的MDL锁。
    • VICTIM:该线程作为死锁的受害者,要求重新执行事务。
    • TIMEOUT:等待超时。
    • KILLED:该线程在等待过程中被kill。
    等待的线程首先将自己想要获取的ticket加入到MDL_lock 的 m_waiting队列,然后根据配置的等待时间调用MDL_wait的函数进行超时等待:
    /**
      Wait for the status to be assigned to this wait slot.
    */
    MDL_wait::enum_wait_status MDL_wait::timed_wait(
        MDL_context_owner *owner, struct timespec *abs_timeout,
        bool set_status_on_timeout, const PSI_stage_info *wait_state_name) {
      enum_wait_status result;
      int wait_result = 0;
    
      mysql_mutex_lock(&m_LOCK_wait_status);
    
      while (!m_wait_status && !owner->is_killed() && !is_timeout(wait_result)) {
        wait_result = mysql_cond_timedwait(&m_COND_wait_status, &m_LOCK_wait_status,
                                           abs_timeout);
      }
    
      if (m_wait_status == WS_EMPTY) {
        if (owner->is_killed())
          m_wait_status = KILLED;
        else if (set_status_on_timeout)
          m_wait_status = TIMEOUT;
      }
      result = m_wait_status;
    
      mysql_mutex_unlock(&m_LOCK_wait_status);
    
      return result;
    }
    当其他持有不兼容类型锁的线程查询完成或者事务结束时,会一起释放持有的所有锁,同时根据是否为fast path还是slow path路径获取到的,恢复MDL_lock::m_fast_path_state的状态和MDL_lock::m_granted链表。除此之外,如果MDL_lock::m_waiting存在正在等待的ticket,则会调用MDL_lock::reschedule_waiters()来唤醒可以获取到锁的线程,并设置等待状态为GRANTED:
    void MDL_lock::reschedule_waiters() {
      MDL_lock::Ticket_iterator it(m_waiting);
      MDL_ticket *ticket;
    
      while ((ticket = it++)) {
        if (can_grant_lock(ticket->get_type(), ticket->get_ctx())) {
          if (!ticket->get_ctx()->m_wait.set_status(MDL_wait::GRANTED)) {
            m_waiting.remove_ticket(ticket);
            m_granted.add_ticket(ticket);
     ...
    /**
      Set the status unless it's already set. Return false if set,
      true otherwise.
    */
    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 == WS_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;
    }
    被唤醒的等待线程,如果发现ticket是GRANTED状态,则继续执行。否则根据不同情况报错。
  • 死锁检测
    每个线程在进入锁等待之前,都会进行一次死锁检测,避免当前线程陷入死等。在检测死锁前,首先将当前线程所获取到的unobtrusive锁物化,这样这些锁才会出现在MDL_lock::m_granted链表中,死锁检测才有可能探测到。并且设置当前线程的等待锁MDL_context::m_waiting_for为当前的ticket,每个进入等待的线程都会设置等待对象,通过这条等待链就可以检测死锁。
    /** Inform the deadlock detector there is an edge in the wait-for graph. */
    void will_wait_for(MDL_wait_for_subgraph *waiting_for_arg) {
      /*
        Before starting wait for any resource we need to materialize
        all "fast path" tickets belonging to this thread. Otherwise
        locks acquired which are represented by these tickets won't
        be present in wait-for graph and could cause missed deadlocks.
    
        It is OK for context which doesn't wait for any resource to
        have "fast path" tickets, as such context can't participate
        in any deadlock.
      */
      materialize_fast_path_locks();
    
      mysql_prlock_wrlock(&m_LOCK_waiting_for);
      m_waiting_for = waiting_for_arg;
      mysql_prlock_unlock(&m_LOCK_waiting_for);
    }
    • MDL_wait_for_subgraph

      表示等待图中一条边的抽象类,会由死锁检测算法进行遍历。MDL_ticket派生于MDL_wait_for_subgraph,通过实现accept_visitor()函数来让辅助检测类顺着边寻找等待环。

    • Deadlock_detection_visitor

      表示在等待图中检测等待环的辅助类,包含检测过程中的状态信息,例如死锁检测的起始线程m_start_node。在搜索过程中发生死锁后,根据权重选择的受害者线程m_victim。搜索的线程深度,假如线程的等待链过长,超过了阈值(默认32),即使没检测到死锁也认为死锁发生。

      实现enter_node()和leave_node()函数来进入下一线程节点和退出,通过inspect_edge()来发现是否当前线程节点已经是起始节点从而判断成环。通过opt_change_victim_to()来比较受害者的死锁权重来决定受害者。
      /**
        Inspect a wait-for graph edge from one MDL context to another.
      
        @retval true   A loop is found.
        @retval false  No loop is found.
      */
      bool Deadlock_detection_visitor::inspect_edge(MDL_context *node) {
        m_found_deadlock = node == m_start_node;
        return m_found_deadlock;
      }
      
      /**
        Change the deadlock victim to a new one if it has lower deadlock
        weight.
      
        @param new_victim New candidate for deadlock victim.
      */
      void Deadlock_detection_visitor::opt_change_victim_to(MDL_context *new_victim) {
        if (m_victim == NULL ||
            m_victim->get_deadlock_weight() >= new_victim->get_deadlock_weight()) {
          /* Swap victims, unlock the old one. */
          MDL_context *tmp = m_victim;
          m_victim = new_victim;
          m_victim->lock_deadlock_victim();
          if (tmp) tmp->unlock_deadlock_victim();
        }
      }
    • 检测过程
      死锁检测的思路:首先广度优先,从当前线程等待的锁出发,遍历MDL_lock的等待队列和授予队列,看看是否有非当前线程获取的、与等待的锁不兼容的锁存在,如果这个持有线程与算法遍历的起点线程相同,那么锁等待链存在死锁;其次深度优先,从不兼容的锁的持有或等待线程出发,如果该线程也处于等待状态,那么递归重复前述过程,直到找到等待起点线程,否则判断不存在死锁。代码逻辑如下:
      MDL_context::find_deadlock()
        |--MDL_context::visit_subgraph(MDL_wait_for_graph_visitor *) // 如果存在m_waiting_for的话,调用对应ticket的accept_visitor()
          |--MDL_ticket::accept_visitor(MDL_wait_for_graph_visitor *) // 根据对应MDL_lock的锁获取情况检测
            |--MDL_lock::visit_subgraph() // 递归遍历锁的授予链表(m_granted)和等待链表(m_waiting)去判断是否存在等待起始节点(死锁)情况
               // 依次递归授予链表和等待链表的MDL_context来寻找死锁
              |--Deadlock_detection_visitor::enter_node() // 首先进入当前节点
              |--遍历授予链表(m_granted),判断兼容性
                |--如果不兼容的话,调用Deadlock_detection_visitor::inspect_edge()判断是否死锁
              |--遍历等待链表(m_waiting),同上
              |--遍历授予链表,判断兼容性
                |--如果不兼容的话,递归调用MDL_context::visit_subgraph()寻找连通子图。如果线程等待的ticket已经有明确的状态,非WS_EMPTY,可以直接返回
              |--遍历等待链表,同上
              |--Deadlock_detection_visitor::leave_node() // 离开当前节点
      /*
        We do a breadth-first search first -- that is, inspect all
        edges of the current node, and only then follow up to the next
        node. In workloads that involve wait-for graph loops this
        has proven to be a more efficient strategy [citation missing].
      */
      while ((ticket = granted_it++)) {
        /* Filter out edges that point to the same node. */
        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;
        }
      }
      ...
      /* Recurse and inspect all adjacent nodes. */
      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;
        }
      }
      ...
    • 受害者权重

      在检测到死锁后,沿着线程等待链退出的时,会根据每个线程等待ticket的权重,选择权重最小的作为受害者,让其放弃等待并释放持有的锁,参考Deadlock_detection_visitor::opt_change_victim_to函数。

      在权重方面并不考虑事务进行的阶段,以及执行的语句内容,仅仅是根据锁资源的类型和锁种类有一个预设的权重,参考MDL_ticket::get_deadlock_weight() 函数。
      • DEADLOCK_WEIGHT_DML:DML类型语句的权重最小为0。
      • DEADLOCK_WEIGHT_ULL:用户手动上锁的权重居中为50。
      • DEADLOCK_WEIGHT_DDL:DDL类型语句的权重最大为100。

      由此可见,在发生死锁时更偏向于让DML语句报错回滚,让DDL语句继续执行。当同类型语句构成死锁时,更偏向让后进入等待链的线程成为受害者,让等待的比较久的线程继续等待。

      当前线程将死锁环上受害者线程的状态设置为VICTIM并唤醒后,当前线程即可进入等待状态。

MDL 监控

通过MySQL performance_schema可以清晰的监控当前MDL锁的获取情况,performance_schema是一个只读变量,设置需要重启,在配置文件中添加如下内容:
[mysqld]
performance_schema = ON
通过performance_schema.setup_instruments表设置MDL监控项:
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES', TIMED = 'YES'
WHERE NAME = 'wait/lock/metadata/sql/mdl';
之后便可访问performance_schema.metadata_locks表来监控MDL获取情况,例如有两个线程处于以下状态:
connect-1 > BEGIN;                    |
Query OK, 0 rows affected (0.00 sec)  |
                                      |
connect-1 > SELECT * FROM t1;         |
+------+------+------+                |
| a    | b    | c    |                |
+------+------+------+                |
|    1 |    2 |    3 |                |
|    4 |    5 |    6 |                |
+------+------+------+                |
2 rows in set (0.00 sec)              | # DDL will hang
                                      | connect-2 > ALTER TABLE t1 ADD INDEX i1(a);
线程1事务没提交,导致线程2执行DDL hang住,通过访问performance_schema.metadata_locks可以看到是因为线程1持有t1的SHARED_READ锁,导致需要获取EXCLUSIV 锁的线程2处于等待状态。
mysql > SELECT * FROM performance_schema.metadata_locks;
+-------------+--------------------+----------------+-------------+-----------------------+---------------------+---------------+-------------+--------------------+-----------------+----------------+
| OBJECT_TYPE | OBJECT_SCHEMA      | OBJECT_NAME    | COLUMN_NAME | OBJECT_INSTANCE_BEGIN | LOCK_TYPE           | LOCK_DURATION | LOCK_STATUS | SOURCE             | OWNER_THREAD_ID | OWNER_EVENT_ID |
+-------------+--------------------+----------------+-------------+-----------------------+---------------------+---------------+-------------+--------------------+-----------------+----------------+
| TABLE       | test               | t1             | NULL        |       140734873224192 | SHARED_READ         | TRANSACTION   | GRANTED     | sql_parse.cc:6759  |              68 |             23 |
| GLOBAL      | NULL               | NULL           | NULL        |       140734862726080 | INTENTION_EXCLUSIVE | STATEMENT     | GRANTED     | sql_base.cc:6137   |              69 |              6 |
| SCHEMA      | test               | NULL           | NULL        |       140734862726240 | INTENTION_EXCLUSIVE | TRANSACTION   | GRANTED     | sql_base.cc:6124   |              69 |              6 |
| TABLE       | test               | t1             | NULL        |       140734862726400 | SHARED_UPGRADABLE   | TRANSACTION   | GRANTED     | sql_parse.cc:6759  |              69 |              6 |
| BACKUP LOCK | NULL               | NULL           | NULL        |       140734862726560 | INTENTION_EXCLUSIVE | TRANSACTION   | GRANTED     | sql_base.cc:6144   |              69 |              6 |
| TABLESPACE  | NULL               | test/t1        | NULL        |       140734862727040 | INTENTION_EXCLUSIVE | TRANSACTION   | GRANTED     | lock.cc:811        |              69 |              6 |
| TABLE       | test               | #sql-5a52_a    | NULL        |       140734862726720 | EXCLUSIVE           | STATEMENT     | GRANTED     | sql_table.cc:17089 |              69 |              6 |
| TABLE       | test               | t1             | NULL        |       140734862726880 | EXCLUSIVE           | TRANSACTION   | PENDING     | mdl.cc:4337        |              69 |              6 |
| TABLE       | performance_schema | metadata_locks | NULL        |       140734887891904 | SHARED_READ         | TRANSACTION   | GRANTED     | sql_parse.cc:6759  |              67 |              4 |
+-------------+--------------------+----------------+-------------+-----------------------+---------------------+---------------+-------------+--------------------+-----------------+----------------+
9 rows in set (0.00 sec)

PolarDB在MDL上的优化

在MySQL社区版中,对分区表数据的访问操作(DML)与分区维护操作(DDL)是相互阻塞的,主要的原因是DDL需要获取分区表上的MDL_EXCLUSIVE锁,导致分区维护操作只能在业务低峰时段进行,而且对分区表进行创建/删除分区的需求是比较频繁的,极大限制了分区表的使用。

在PolarDB中,引入了分区级别的MDL锁,使DML和DDL获取的锁粒度降低到分区级,提高了并发度,实现了“在线”分区维护功能。使得分区表的数据访问和分区维护互不影响,用户可以更自由的进行分区维护,而不影响分区表业务流量,大大增强了分区表使用的灵活度。