本网站(662p.com)打包出售,且带程序代码数据,662p.com域名,程序内核采用TP框架开发,需要联系扣扣:2360248666 /wx:lianweikj
精品域名一口价出售:1y1m.com(350元) ,6b7b.com(400元) , 5k5j.com(380元) , yayj.com(1800元), jiongzhun.com(1000元) , niuzen.com(2800元) , zennei.com(5000元)
需要联系扣扣:2360248666 /wx:lianweikj
MySQL 深潜 - MDL 锁的实现与获取机制
xiaomei1994 · 245浏览 · 发布于2022-03-14 +关注

本文将介绍在 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]));
}

  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

表达式的写法有点绕,可以理解为,如果 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 中。

                                   数据库
                                   mysql
                                 

相关推荐

使用SELECT语句检索数据

奔跑的男人 · 810浏览 · 2019-06-03 09:33:43
部署MySQL延迟从库的几个好处

吴振华 · 691浏览 · 2019-05-14 21:57:51
MongoDB凭什么跻身数据库排行前五?

iamitnan · 732浏览 · 2019-06-18 10:04:56
Oracle开启和关闭的几种模式

qq2360248666 · 761浏览 · 2019-06-04 10:18:47
加载中

0评论

评论
分类专栏
小鸟云服务器
扫码进入手机网页