乐趣区

关于数据库:MySQL-深潜-MDL-锁的实现与获取机制

简介:本文将介绍在 MDL 零碎中罕用的数据结构及含意,而后从实现角度探讨 MDL 的获取机制与死锁检测,最初分享在实践中如何监控 MDL 状态。

作者 | 泊歌
起源 | 阿里技术公众号

一 背景

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

本文将介绍在 MDL 零碎中罕用的数据结构及含意,而后从实现角度探讨 MDL 的获取机制与死锁检测,最初分享在实践中如何监控 MDL 状态。

二 基本概念

1 MDL_key

MDL 的对象是采纳键值对(key-value)的形式形容的,每一个 key 值都惟一的代表了锁的对象(value 代表数据库的某种资源)。key 是由 MDL_key 示意的,用字符串的模式示意了对象的名称。

残缺的字符串由 namespace、按档次每一级的名称组成,多种命名空间能够将不同类型的同名对象辨别开。命名空间包含 GLOBAL、SCHEMA、TABLE、FUNCTION、PROCEDURE 等数据库中能够创立的不同对象类型组成。

对象的名称依据类型的不同能够由多种档次组成。比方表对象就由数据库名和表名惟一的形容;如果是 SCHEMA 对象,那就只有数据库名这一个档次。名称之间用字符串结束符 ‘\0’ 分隔。因而由这几局部组成的字符串整体就能作为 key 惟一的示意数据库的某种对象。

2 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 将锁类型划分为范畴锁和对象锁。

1)范畴锁

范畴锁品种较少(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

2)对象锁

对象锁蕴含的 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 类型与某种 m_type 类型兼容的 MDL 不兼容,那么 type 类型更强;否则 m_type 类型雷同或更强。或者较弱的类型不兼容的 MDL 类型,较强的 MDL 都不兼容。

三 重要数据结构

1 关系示意图

2 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 的销毁而开释。

3 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 的命名空间就能够获取到该锁的兼容性状况。

4 MDL_ticket

MDL_lock 与 enum_mdl_type 独特组成了 MDL_ticket,代表着以后线程对数据库对象的拜访权限。MDL_ticket 在每个查问申请 MDL 锁时创立,内存由 MDL 零碎调配,在事务完结时捣毁。

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

5 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()

1 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 链表中。

2 锁期待和告诉

上述过程中,如果能顺利获取到 MDL_ticket,就实现了 MDL 的获取,能够持续查问过程。如果无奈获取到(不论是 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 状态,就会继续执行;否则依据不同状况报错。

3 死锁检测

每个线程在进入锁期待之前,都会进行一次死锁检测,防止以后线程陷入死等。在检测死锁前,首先将以后线程所获取到的 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 锁,导致须要获取 EXCLUSIVE 锁的线程 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 获取的锁粒度升高到分区级,进步了并发度,实现了“在线”分区保护性能。使得分区表的数据拜访和分区保护不相互影响,用户能够更自在的进行分区保护,而不影响分区表业务流量,大大加强了分区表应用的灵便度。

该性能曾经在 PolarDB 8.0.2.2.0 及以上版本中公布,欢送用户应用。

七 参考

[1] Source code mysql / mysql-server 8.0.18:https://github.com/mysql/mysq…
[2] MySQL · 源码剖析 · 罕用 SQL 语句的 MDL 加锁源码剖析:http://mysql.taobao.org/month…
[3] 在线分区保护性能:https://help.aliyun.com/docum…

原文链接
本文为阿里云原创内容,未经容许不得转载。

退出移动版