暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

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

GaussDB数据库 2024-12-28
291


1. 背景介绍


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


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


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


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


2. MDL的实现


MDL 是在 TaurusDB 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。


    而TABLEFUNCTIONPROCEDURETRIGGER,EVENTUSER_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请求类型与等待中类型的优先级


        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请求类型与等待中类型的优先级时,"+"表示某类型的锁具有较高的优先级,更有可能被优先处理。


        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.
              */
                  {0111ULL << 201ULL << 401ULL << 40000000},


              在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 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 MDL_context


              如图2所示,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_granted和m_waiting链表。如果ticket对应的线程和死锁检测的发起线程相同,则说明有回路,随即退出;


                  最后,再进行深度搜索:重新遍历当前锁的m_granted和m_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_EXCLUSIVE和TABLE-TRANSACTION-SHARED_WRITE。而在commit阶段,会获取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同步到备机:


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

                    2)Redo 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在主备节点间的高效同步机制,确保了只读节点数据的一致性。


                    END


                    华为云数据库 新用户


                    Flexus云数据库RDS 3个月30元

                    TaurusDB 标准版 3个月261元 


                    扫码抢购

                            

                    活动时间:2024/12/1-2025/1/15

                      戳“阅读原文”,了解更多

                    文章转载自GaussDB数据库,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                    评论