【华为云MySQL技术专栏】TaurusDB MDL实现机制解析

举报
GaussDB 数据库 发表于 2024/12/27 17:33:41 2024/12/27
【摘要】 1. 背景介绍为了满足数据库在高并发请求下的事务隔离性和一致性要求,TaurusDB使用MDL(metadata lock,元数据锁)机制来管理对数据库对象的并发访问。使用MDL可以避免以下几类问题的发生:1)读取结果的不一致性:在可重复读(Repeatable Read,简称RR)隔离级别下,一个事务中的第一次查询可能返回某些结果,但在第二次查询时,由于表被另一个事务删除,导致查询结果为空...

1. 背景介绍

为了满足数据库在高并发请求下的事务隔离性和一致性要求,TaurusDB使用MDL(metadata lock,元数据锁)机制来管理对数据库对象的并发访问。使用MDL可以避免以下几类问题的发生:

1)读取结果的不一致性:在可重复读(Repeatable Read,简称RR)隔离级别下,一个事务中的第一次查询可能返回某些结果,但在第二次查询时,由于表被另一个事务删除,导致查询结果为空。

2)二进制日志(Binlog)记录的混乱:在Binlog中,如果先记录了表的删除操作,随后又记录了向同一表中插入数据的操作,导致数据恢复时的逻辑错误。

本文将基于MDL中常用的数据结构及含义,从实现的角度来讨论MDL的获取、释放、升降级与死锁检测,最终聚焦于华为云TaurusDB中MDL在主备同步的实现机制。

2. MDL的实现

MDL 是在 MySQL server 层实现的一个模块,通过定义的接口和server层其它组件进行交互,核心功能sql/mdl.h和sql/mdl.cc中实现。

MDL和传统的表锁有以下几点的区别:

在TaurusDB中,无论是DDL、DML还是查询语句,所有操作均需先获取Server层的MDL,然后才能进一步获取InnoDB存储引擎层所需的特定锁。

2.1锁类型

为了提高并发度,MDL被细分为了11种类型/级别,其数据结构为enum_mdl_type

2.2 持续时间

持续时间代表要持有某个MDL多久对应代码enum_mdl_duration,包含以下三种类型

3. MDL锁的标识

MDL锁的标识对应代码MDL_key

/**
  Metadata lock object key.
  A lock is requested or granted based on a fully qualified name and type.
  E.g. They key for a table consists of @<0 (=table)@>+@<database@>+@<tablename@>. 
  Elsewhere in the comments this triple will be referred to simply as "key" or "name".
*/

'namespace'+'db_name'+'object_name'三元组,是构成一个MDL的唯一标识。即不管请求的key是什么类型或什么时间范围 ,只要三元组相同,所有请求都使用同一个MDL_key对象。其中,namespace是一个系统内部的命名空间,db_name为数据库名,object_name为对象名,如表名、视图名等。

MDL namespace表示一个MDL针对的对象,对应代码enum_mdl_namespace

其中,GLOBAL, TABLESPACE, SCHEMA, COMMIT(全局唯一,FLUSH TABLES WITH READ LOCK使用,阻止其他事务提交), BACKUP_LOCK(用于阻止可能破坏备份一致性的语句), RESOURCE_GROUPS, FOREIGN_KEY, CHECK_CONSTRAINT, BINLOG属于scoped lock。

TABLE, FUNCTION, PROCEDURE, TRIGGER,EVENT, USER_LEVEL_LOCK(用于user-level的锁, GET_LOCK(), RELEASE_LOCK()等函数), LOCKING_SERVICE(用于locking service), SRID(空间参照系统), ACL_CACHE ACL(access-control list缓存), COLUMN_STATISTICS属于object lock。

4. MDL锁类型的兼容性

MDL_lock::MDL_lock_strategy类中定义了不同MDL锁的不同处理策略,包括但不限于锁的兼容矩阵

4.1 Scoped lock

Scoped lock,锁定的是一个较大范围内的对象,而不是某个单独的对象。例如GLOBAL, COMMIT, TABLESPACE, BACKUP_LOCK, SCHEMA 这些namespace。其对应代码MDL_lock_strategy m_scoped_lock_strategy

Scoped lock有三种类型:IX、S和X其中,IS锁类型与所有类型都兼容,所以代码中实际没有任何处理

MDL请求类型与已授予类型的兼容性

在评估MDL请求类型与已授予类型的兼容性时,"+"被用来表示锁类型间相互兼容,这意味着多个线程可以同时持有与已授予类型兼容的MDL。

The first array specifies if particular type of request can be
satisfied if there is granted scoped lock of certain type.
                 | Type of active   |
         Request |   scoped lock    |
          type   | IS(*)  IX   S  X |
        ---------+------------------+
        IS       |  +      +   +  + |
        IX       |  +      +   -  - |
        S        |  +      -   +  - |
        X        |  +      -   -  - |

MDL请求类型与等待中类型的优先级

在处理MDL请求类型与等待中类型的优先级时,"+"表示某类型的锁具有较高的优先级,更有可能被优先处理。

The first array specifies if particular type of request can be
satisfied if there is granted scoped lock of certain type.
                 | Type of active   |
         Request |   scoped lock    |
          type   | IS(*)  IX   S  X |
        ---------+------------------+
        IS       |  +      +   +  + |
        IX       |  +      +   -  - |
        S        |  +      -   +  - |
        X        |  +      -   -  - |

4.2 Object lock

Object lock,对应代码MDL_lock_strategy m_object_lock_strategy

MDL请求类型与已授予类型的兼容性

Request  |  Granted requests for lock                       |
  type    | S  SH  SR  SW  SWLP  SU    SRO  SNW  SNRW  X  SPW|
----------+--------------------------------------------------+
S         | +   +   +   +    +    +     +    +    +    -   + |
SH        | +   +   +   +    +    +     +    +    +    -   + |
SR        | +   +   +   +    +    +     +    +    -    -   + |
SW        | +   +   +   +    +    +     -    -    -    -   + |
SWLP      | +   +   +   +    +    +     -    -    -    -   + |
SU        | +   +   +   +    +    -     +    -    -    -   - |
SRO       | +   +   +   -    -    +     +    +    -    -   - |
SNW       | +   +   +   -    -    -     +    -    -    -   - |
SNRW      | +   +   -   -    -    -     -    -    -    -   - |
X         | -   -   -   -    -    -     -    -    -    -   - |
SPW       | +   +   +   +    +    -     -    -    -    -   - |

MDL请求类型与等待中类型的优先级

Request  |         Pending requests for lock               |
  type    | S  SH  SR  SW  SWLP  SU    SRO  SNW  SNRW  X SPW|
----------+-------------------------------------------------+
S         | +   +   +   +    +    +     +    +     +   -  + |
SH        | +   +   +   +    +    +     +    +     +   +  + |
SR        | +   +   +   +    +    +     +    +     -   -  + |
SW        | +   +   +   +    +    +     +    -     -   -  + |
SWLP      | +   +   +   +    +    +     -    -     -   -  + |
SU        | +   +   +   +    +    +     +    +     +   -  + |
SRO       | +   +   +   -    +    +     +    +     -   -  + |
SNW       | +   +   +   +    +    +     +    +     +   -  + |
SNRW      | +   +   +   +    +    +     +    +     +   -  + |
X         | +   +   +   +    +    +     +    +     +   +  + |
SPW       | +   +   +   +    +    +     +    +     +   -  + |

4.3 unobtrusive(fast path)obtrusive(slow path)

根据锁类型的兼容性,MDL可以划分为 unobtrusive (非侵入性)和 obtrusive(侵入性) 类型的锁,其获取过程分别对应 fast path(快速路径)和 slow path(慢速路径)。

判断锁类型为unobtrusive或obtrusive的代码MDL_lock::get_unobtrusive_lock_increment()。

unobtrusive与fast path

unobtrusive类型的锁,在DML中使用频繁,并与其他类型锁能兼容。unobtrusive锁通过使用fast path来获取和释放,系统不会对等待队列(m_waiting)和已授权队列(m_granted)进行检查,而是替换为整数计数器的来检查,通过递增或递减计数器来管理。

Fast path优化了unobtrusive锁的获取和释放,从而提升了DML的性能。

对于scoped lock,IX类型是unobtrusive,可以使用fast path来获取和释放。

对于object lock,S, SH, SR, SW和SWLP类型是unobtrusive,可以使用fast path进行优化。以object lock为例,其fast_path_state_tm_unobtrusive_lock_increment[MDL_TYPE_END]定义如下:

For per-object locks:
      - "unobtrusive" types: S, SH, SR and SW
      - "obtrusive" types: SU, SPW, 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, 0},

MDL_lock中使用整型原子变量std::atomic m_fast_path_state ,用来统计该锁授予的所有 unobtrusive锁类型的数量。具体而言,每种 unobtrusive 锁类型的数量由固定长度的bit位来表示,相当于用一个 longlong 类型统计了所有 unobtrusive 锁类型的授予个数,同时可以通过 CAS (Compare-And-Swap,原子操作)进行无锁修改。

根据MDL_request的请求类型,获取对应类型的unobtrusive值。如果值为0,则代表该类型锁为obtrusive,需要走slow path。

obtrusive与slow path

obtrusive类型的锁与其他类型或自身不兼容。对于DML操作,这类锁并不常见,但因为其获取与释放过程涉及对m_waiting/m_granted进行复杂的检查和操作,即slow path。如果当前锁对象已经存在obtrusive类型的锁,则只能使用slow path处理,因为已经存在不兼容的类型,无法直接授予新的锁。

当前线程如果申请obtrusive的锁,则该线程所有使用fast path获得的unobtrusive的锁,都需要进行物化(对应代码MDL_context::materialize_fast_path_locks()),从fast path的m_fast_path_state中移除,添加到锁对象m_granted链表中,用于后续检查。由于m_fast_path_state无法区分线程,而当前线程获取的多个锁之间不构成锁冲突,所以在通过bitmap判断锁状态前,需要确保m_fast_path_state中所有的ticket都是属于其他线程的,从而避免当前线程获取多个锁的冲突。

5. MDL状态

MDL状态,即MDL锁的获取结果。对应代码enum enum_wait_status,具体函数如下

6. 重要数据结构

MDL_request

MDL_request表示语句对MDL的请求,由MDL_key(锁的唯一标识)、MDL_ticket(锁的授权凭证)、enum_mdl_type(锁的类型)和enum_mdl_duration(锁的持续时间)组成。

MDL_request和MDL_ticket由不同的类表示,生命周期也不同。

MDL_request是在MDL系统外部分配的,可以是一个临时变量;而MDL_ticket的分配由MDL系统内部控制,并不会随着MDL_request的销毁而释放。

MDL_ticket

MDL_ticket表示当前线程(THD)对数据库对象的访问权限,由MDL_context(上下文信息)和enum_mdl_type(锁的类型)组成。

MDL_ticket由MDL系统分配,在线程请求MDL时被创建,在事务结束时销毁。

MDL_lock

MDL_lock表示对应名称的锁对象。对于给定对象,系统中只有一个MDL_lock实例存在,并且只有在锁被授予时才处于活动状态,如图1所示。

1.png

1 MDL_lock

MDL_lock对象被统一保存在全局的MDL_map mdl_locks中,每个MDL_lock实例由MDL_key、两个Ticket_list、MDL_lock_strategy组成。其中,两个Ticket_list分别为m_granted和m_waiting,用于存储已获取和正在等待的MDL_ticket。

MDL_context

MDL_context是THD获取MDL锁时的上下文环境。

2.png

2 MDL_context

THD通过MDL_context来申请和释放MDL锁,对于持有的每个MDL锁会有一个对应的MDL_ticket,存放在m_ticket_store中。另外,MDL_context还包含了一个m_waiting_for成员,用于记录当前会话正在等待的MDL_ticket信息。

7. MDL相关代码流程

下面介绍MDL的相关流程函数。

7.1 加锁

加锁的入口函数:MDL_context::acquire_lock()

MDL_context::acquire_lock
|-MDL_context::try_acquire_lock_impl() // 尝试获取锁,如果成功,直接返回
|-MDL_context::find_deadlock() // 死锁检测
|-等待获取锁
|-异常处理逻辑
|-获取成功,保存MDL ticket

加锁主要实现在函数MDL_context::try_acquire_lock_impl中,如图3所示。

3 MDL加锁流程

MDL_context::ticket_store()检测当前已持有的MDL能够满足请求的条件时,直接返回成功。若已持有的锁与请求的锁在duration上不匹配(例如,当前锁在事务结束时自动释放,而请求需要的是显式释放的锁),则clone一个MDL_ticket,再返回成功。当MDL_context::ticket_store()检测当前未持有MDL时,则创建新的锁对象。

随后,在static MDL_map::mdl_locks中查找或者新增对应的MDL_lock对象, 对于每个锁请求,评估其是否符合fast path条件。如果符合,将ticket加入m_granted,直接返回成功;如果不符合,只能使用slow path的锁,还需要MDL_lock::can_grant_lock判断能否直接授予。若能直接授予,将ticket加入m_granted后,返回成功。若不能直接授予,ticket加入m_waiting后,调用MDL_context::find_dead_lock行死锁检测。如果检测到死锁,返回失败。如果没有检测到死锁,请求进入等待状态,直到锁被成功授予或达到超时时间。

7.2 释放锁

释放锁的入口函数:MDL_context::release_lock(),主要作用是移除ticket,并调用MDL_lock::reschedule_waiters唤醒等待队列中的合适线程 。

7.3 锁升级

锁升级的入口函数:MDL_context::upgrade_shared_lock(),如果当前线程已持有共享锁,返回成功。否则,会申请新类型的锁,并通过MDL_context::acquire_lock()更新锁信息。

MDL_context::upgrade_shared_lock
|-MDL_ticket::has_stronger_or_equal_type // 如果已持有更强类型的锁,返回成功
|-MDL_context::acquire_lock // 申请新类型的锁
|-更新锁信息
|-如果获得一个新ticket,将其从m_granted移除,因为还要用原ticket
|-移除原ticket
|-更新原ticket类型,重新加到m_granted

7.4 锁降级

锁降级的入口函数:MDL_ticket::downgrade_lock()。首先,该函数会从granted中移除当前的ticket,再修改状态并重新加入。接着,通过调用MDL_lock::reschedule_waiters()评估是否唤醒合适的等待者。MDL的升降级适用于部分DDL,降级后,DDL与DML可以并行(Online DDL)执行,在DDL提交时,锁会再升级。

7.5 死锁检测

死锁检测的入口函数:MDL_context::find_deadlock(),在加锁前会进行死锁检测。其核心逻辑如下:

首先,搜索深度递增,然后判断是否超过最大搜索深度(MAX_SEARCH_DEPTH= 32),若超过,就无条件认为有死锁,退出;

其次,对当前层进行广度搜索,即遍历当前锁存放ticket的m_grantedm_waiting链表。如果ticket对应的线程和死锁检测的发起线程相同,则说明有回路,随即退出;

最后,再进行深度搜索:重新遍历当前锁的m_grantedm_waiting链表,对每个ticket对应的线程,递归调用MDL_context::visit_subgraph(),实现对死锁的全面检测。

MDL_context::find_deadlock()
|-MDL_context::visit_subgraph() // 如果存在m_waiting_for的话,调用对应ticket的accept_visitor()
  |-MDL_ticket::accept_visitor() // 遍历通过该ticket所代表的边可达的等待图,并搜索死锁。
    |-MDL_lock::visit_subgraph() // 递归遍历锁的m_granted和m_waiting去判断是否存在等待起始节点(死锁)情况
      // 依次递归授予链表和等待链表的MDL_context来寻找死锁
      |-Deadlock_detection_visitor::enter_node() // 进入当前节点,
        |-Deadlock_detection_visitor::opt_change_victim_to()// 如果发现有死锁,且当前节点死锁权重较低,则将当前节点更改为新的死锁受害者。
      |-遍历m_granted,判断兼容性
        |-如果不兼容的话,调用Deadlock_detection_visitor::inspect_edge()判断是否死锁
      |-遍历m_waiting,同上
      |-遍历m_granted,判断兼容性
        |-如果不兼容的话,递归调用MDL_context::visit_subgraph()寻找连通子图。如果线程等待的ticket已经有明确的状态,非WS_EMPTY,可以直接返回
      |-遍历m_waiting,同上
      |-Deadlock_detection_visitor::leave_node() // 离开当前节点

7.6 SQL执行流程的MDL

下面分别以SELECT语句INSERT/UPDATE/DELETE语句为例,分析执行流程中MDL申请释放

SELECT查询语句

对于一条简单的SELECT查询语句,其执行过程中的MDL操作如图4所示,对应的MDL锁为TABLE-TRANSACTION-SHARED_READ。

4 简单SELECT查询中的MDL

在SQL解析阶段,LEX 和 YACC会给待访问表初始化 MDL的 锁请求。不同的语句对应的锁类型不同,例如,SELECT 语句对应 SR(共享读锁),而INSERT/UPDATE/DELETE 语句对应 SW(独占写锁)。

在实际执行语句之前,系统将调用 open_tables_for_query 函数来访问所有需要的表,并为它们创建TABLE 表对象。这一步骤的首要任务是获取 MDL,以避免对同一个表的元数据并发读写。只有在成功获取了 MDL之后,系统才会继续进行表资源的获取流程。

在SQL执行结束前,mdl_context.release_transactional_locks函数会被调用,以释放先前获得的所有MDL。

INSERT/UPDATE/DELETE语句

对于一条简单的INSERT/UPDATE/DELETE语句,其执行过程中的MDL操作如图5所示。

5 DML中的MDL操作流程

open table阶段INSERT/UPDATE/DELETE语句先后获取两个锁,分别是GLOBAL-STATEMENT-INTENTION_EXCLUSIVETABLE-TRANSACTION-SHARED_WRITEcommit阶段获取COMMIT-MDL_EXPLICIT-INTENTION_EXCLUSIVE锁

8. TaurusDB中的MDL主备同步

TaurusDB采用存算分离的架构,其中,Master节点Replica节点share-storage共享存储即它们并没有各自独立的数据存储系统同时,也没有专门的协调者角色来居中处理各类锁请求。因此TaurusDB存在如下场景:

6 备机冲突

如图6所示,当TaurusDB的备机正在执行的查询涉及的表被主机修改了表结构或直接drop产生冲突。针对类似这种场景,需要将主机DDL时涉及的MDL操作同步到备机,以解决上述问题

TaurusDB基于 “无协调组件”(Coordination free)和“重做一切”(Redo everything)两大原则通过新增一种MDL类型的redo log,将主节点上的MDL同步到备机

1Coordination free,不引入额外的组件作为主备之间的协调者

2Redo everything,通过redo日志来进行同步,这也是主备之间已有的一个通道

7 MDL通过redo同步到备机

如图7所示,通过将主机上MDL的加锁与释放都记录在redo日志里,再于备机中进行回放,确保了TaurusDB备机数据和元数据的一致,并且不阻塞主机上业务的执行。

Master节点

在执行DDL过程中,主节点会将MDL加锁、失效、放锁的操作添加到一个DDL元数据请求列表meta_request_info_list中。在事务提交时系统会依据meta_request_info_list中保存的内容,生成响应的MDL类型redo日志,并将其发送给只读节点。

Read Replica节点

在只读节点,TaurusDB有一套通用的redo log处理流程,涉及以下多个线程。

Reader线程负责读取redo日志并将其交给Dispatcher线程

Dispatcher线程:接收到日志以后,分发给多个Parser线程进行解析并收集结果。其中,解析后的日志由Advancer线程及Impeller线程进行合并,并做相关处理。MDL相关处理逻辑主要位于Parser线程和Advancer线程

Parser线程解析日志,将解析到的MDL类型的redo日志保存到元数据请求列表meta_requests

Advancer线程:处理meta_requests中的MDL相关日志,将还未收到释放锁的MDL请求保存到ongoing_meta_request_infos将已经收到对应释放锁日志的MDL请求保存到ready_meta_request_infos 。后续再依据ready_meta_request_infos中的内容,进行相应的MDL加锁、失效、释放操作。

9. 总结

本文对TaurusDBMDL源码实现进行了深度分析,同时结合TaurusDB自身的架构设计,剖析实现MDL在主备节点间的高效同步机制,确保了只读节点数据的一致性。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。