Metadata Lock,顾名思义,是对元数据的保护。MDL 是在 5.5 中引入的,之前版本对于元数据也有保护,但实现为语句级别的,当语句结束后元数据相关的锁就会被释放掉。这种实现会导致两个主要的问题:
tx1: BEGIN;tx1: SELECT * FROM tbl; -- 获取元数据锁,返回(c1,c2),释放元数据锁tx2: ALTER TABLE tbl DROP COLUMN c2; -- 获取元数据锁,成功,释放元数据锁tx1: SELECT * FROM tbl; -- 返回(c1),不可重复读tx1: END;
tx1: BEGIN;tx1: INSERT INTO tbl(c1, c2) VALUES(v1, v2);tx2: ALTER TABLE tbl DROP COLUMN c2; -- commit并写入binlog 1tx1: END; -- commit,写入binlog 2,备库先重放binlog 1,重放binlog 2时出错
于是 5.5 版本引入了 MDL,实现为事务级别,事务中获取的 MDL 锁在事务提交或回滚时释放,并在后续版本中针对最初实现做了很多优化,包括:
本文将介绍 5.7 中的实现。
内核开发过程中有时候需要使用 MDL 子系统,关于 MDL 使用有几个重要的概念:
struct MDL_key { enum enum_mdl_namespace { GLOBAL = 0, // global范围锁 TABLESPACE, // 表空间范围锁 SCHEMA, // 数据库范围锁 TABLE, // 表目标锁 ... COMMIT, // commit范围锁 }; char m_ptr[]; }
三元组以字符串的方式连续存储在 m_ptr 中。目标锁理解为当要访问某些具体对象时需要对该目标加元数据锁,范围锁理解为当要进入某些范围内并且会在该范围内造成数据库改动时,需要加范围锁,其中 GLOBAL 范围锁几乎所有的会造成改动的语句都需要获取,COMMIT 锁是在做事务提交之前获取,通过 GLOBAL 和 COMMIT 范围锁可以控制 SQL 语句的进入以及提交 (FTWRL 实现方式)。
MDL_INTENTION_EXCLUSIVE IX // 只用于范围锁 MDL_SHARED S // 只能读元数据,不能读写表数据,如检查表是否存在时用这个锁 MDL_SHARED_HIGH_PRIO SH // 只能读元数据,不能读写表数据,用于填充INFORMATION_SCHEMA,或者SHOW CREATE TABLE时,和S模式区别在于若其他线程持有S锁,且等待队列中有X锁时,可以直接获取锁(S锁需要等待X模式先被授予, SH可以优先于等待队列中的X) MDL_SHARED_READ SR // 可以读表数据,SELECT语句使用 MDL_SHARED_WRITE SW // 可以写表数据,DML语句和SELECT FOR UPDATE使用 ... MDL_SHARED_UPGRADABLE SU // 可升级锁,类似SR,可以读表数据,但可以升级为SNW或者X锁,ALTER TABLE用 ... MDL_SHARED_NO_WRITE SNW // 其它线程能读元数据和表数据,但不能写表数据,持锁线程可以读写表数据,可以升级成X锁,ALTER TABLE用 MDL_SHARED_NO_READ_WRITE SNRW // 其它线程能读元数据,但不能读写表数据,持锁者可以读写表数据,可以升级成X锁,LOCK TABLES WRITE用 MDL_EXCLUSIVE X // 排它锁,可以写元数据,禁止其它线程读元数据,CREATE/DROP/RENAME/ALTER TABLE用
值得注意的两点:
tx1: acquire SR lock mode tx2: acquire SR lock mode tx1: acquire X lock mode // 阻塞,因为tx2拿着SR,X和SR冲突 tx2: acquire X lock mode // 阻塞,因为tx1拿着SR,死锁
将上述场景中的 SR 替换为 SU 就能避免死锁,tx2 在获取 SU 时就会阻塞,因为和 tx1 持有的 SU 冲突;5.7 中的 ALTER TABLE ... ALGORITHM = COPY 就是这么实现的,先在打开表时获取 SR 锁,然后升级为 SNW 并读取表数据(其他线程不能写入表数据了),构建临时表,在最后 rename 表阶段升级为 X 模式。
另一个涉及锁升级的 DDL 语句是 CREATE TABLE,首先获取 S 模式的 MDL 锁,用于检查表是否存在,然后升级为 X 模式并真正建表;它是直接从 S 升级到 X,所以会存在上述的死锁问题,解决方法是代码中加了一个重试逻辑;
各种表模式的核心含义在于它们相互之间的兼容与互斥关系,这种关系可以划分到两类里,范围锁模式的互斥关系和目标锁模式的互斥关系,每一类下又包括两种互斥关系,一种是与已经被授予的锁模式的互斥关系,另一种是与等待队列中锁模式的互斥关系,获取锁时需要先判断是否和已经授予的锁模式冲突,如果不冲突再判断和等待队列中的请求是否冲突,都不冲突才能获取锁。第二个冲突检测的目的是给各种锁模式赋予了优先级,后来的锁只有优先级高于等待队列中的锁才能进行抢占。冲突矩阵从语义中已经可以推演出来了,具体为:
| Type of active | Request | scoped lock | type | IS(*) IX S X | ---------+------------------+ IS | + + + + | IX | + + - - | S | + - + - | X | + - - - | (*) IS和所有范围锁模式兼容,所以并没有相应的实现 | Pending | Request | scoped lock | type | IS(*) IX S X | ---------+------------------+ IS | + + + + | IX | + + - - | S | + + + - | X | + + + + |
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 | + + + + + + + + + + |
enum enum_mdl_duration { MDL_STATEMENT // 语句结束释放 MDL_TRANSACTION // 事务结束释放 MDL_EXPLICIT // 需显式释放 }
class MDL_request { enum enum_mdl_type type; enum enum_mdl_duration duration; MDL_ticket *ticket; // 返回的锁获取结果 MDL_key key; }
在介绍 MDL 锁实现之前,有一些关于锁的背景先介绍一下。在各种不同的锁的实现中,个人认为有两个非常关键的基本要素:
__pthread_mutex_lock --> LLL_MUTEX_LOCK(macro) --> lll_lock(mutex->__data.__lock) --> assembly checking contension |__ __lll_lock_wait --> lll_futex_wait(macro) --> lll_futex_timed_wait(macro) --> syscall futex __pthread_cond_wait --> lll_lock(cond->__data.__lock) |__ __pthread_mutex_unlock_usercnt(mutex) |__ cond->__data.__futex++ // integer |__ cond->__data.__mutex = mutex // save the mutex for re-acquiring later |__ futex_val = cond->__data.__futex |__ lll_unlock(cond->__data.__lock) |__ lll_futex_wait(&cond->__data.__futex, futex_val)(macro) --> lll_futex_timed_wait --> syscall futex
除此之外,MySQL 在 MDL 中还自己维护了一个等待队列,具体在 MDLlock::mwaiting 中;
代码中的重要数据结构和类定义包括:
class MDL_ticket: public MDL_wait_for_subgraph // 一次锁请求的结果,可能是获取成功,可能是获取不成功需要等待{ enum enum_mdl_type m_type; // 锁模式 enum enum_mdl_duration m_duration; // 持续时间 MDL_context *m_ctx; // 对应的线程 MDL_lock *m_lock; // 对应的锁}class MDL_wait // 等待行为的抽象,实现等待逻辑{ enum enum_wait_status m_wait_status; // 等待状态 mysql_mutex_t m_LOCK_wait_status; // 保护m_wait_status的mutex mysql_cond_t m_COND_wait_status; // 当m_wait_status发生变化时线程唤醒用的pthread_cond_t,睡眠唤醒逻辑的封装 timed_wait() // pthread_cond_t的经典用法,实现睡眠等待 { mysql_mutex_lock(); while(!m_wait_status) { mysql_cond_timedwait(); } mysql_mutex_unlock(); }}class MDL_context // 代表每个请求锁的线程{ /* * ... 线程对于MDL锁子系统的各种接口函数声明 */ MDL_wait m_wait; // 用于实现等待行为 Ticket_list m_tickets; // 线程获得的所有锁 MDL_wait_for_subgraph *m_waiting_for; // 正在等待的锁,同一时间只会有一个}class MDL_map // 全局变量,用于存储所有的锁{ LF_HASH m_locks; // Lock-free hash table,存储除COMMIT和GLOBAL外所有锁 MDL_lock *m_global_lock; // GLOBAL范围锁,全局只一个 MDL_LOCK *m_commit_lock; // COMMIT范围锁,全局只一个}class MDL_lock // 标识每个锁,和unique MDL_key一对一, 不管duration和锁模式{ MDL_key key; mysql_prlock_t m_rwlock; // 保护内部成员的rwlock, prefer reader, self-implemented using pthread_mutex_t, and pthread_cond_t; Ticket_list m_granted; // 已经授予的请求 Ticket_list m_waiting; // 等待这个锁的请求 // fast path releated volatile fast_path_state_t m_fast_path_state; // flag和counter的组合, for atomic operations, use fast_path_state_cas()/add()/reset() wrappers struct MDL_lock_strategy { bitmap_t m_granted_incompatible; // 互斥矩阵 bitmap_t m_waiting_incompatible; // 互斥矩阵 fast_path_state_t m_unobtrusive_lock_increment[MDL_TYPE_END]; // longlong, base_increment, 0 means obtrusive mode } static const MDL_lock_strategy m_scoped_lock_strategy; // static, members hard-coded, IMPORTANT static const MDL_lock_strategy m_object_lock_strategy; // static, members hard-coded, IMPORTANT}
MDL 子系统的初始化调用栈为:
#0 MDL_map::init#1 mdl_init#2 init_server_components#3 mysqld_main#4 main
锁获取的流程为:
acquire_lock --> try_acquire_lock_impl --> find_ticket // check if we have already hold this lock, MDL_contex::m_tickets, similar to PG locallock | |__ clone_ticket // if duration is not same | |__ fix_pins // get LF_PINS for lock-free hash access | |__ MDL_ticket::create // m_lock is null now | |__ get_unobtrusive_lock_increment | |__ retry label | |__ mdl_locks.find_or_insert(m_pins, key, &pinned) // pinned must be true for non-singleton locks, i.e, "locked" | |__ fast path branch --> old_state = lock->m_fast_path_state // save instant one | | |__ do{old_state flag checks} while(!lock->fast_path_state_cas(&old_state, old_state + unobtrusive_lock_increment)) // cas would update old_state if not equal, after get out from this loop, lock is granted | | |__ cleanups: lf_hash_search_unpin, save lock in ticket->m_lock, append the ticket to MDL_context::m_tickets, set mdl_request->ticket | |__ slow path branch --> mysql_prlock_wrlock(&lock->m_rwlock) // protecting list and counters | |__ lf_hash_search_unpin // m_rwlock can protect the lock object now | |__ do{} while(!lock->fast_path_state_cas()) // to mark HAS_SLOW_PATH and optional HAS_OBTRUSIVE | |__ ticket->m_lock = lock | |__ if lock->can_grant_lock --> lock->m_granted.add_ticket(ticket) // can_grant_lock checks granted, fast-path granted, and waiting bitmaps | | |__ mysql_prlock_unlock | | |__ append the ticket to MDL_context::m_tickets | | |__ set mdl_request->ticket = ticket | |__ else *out_ticket = ticket // not successful, HOLDING the m_rwlock |__ check mdl_request->ticket and return if granted |__ lock->m_waiting.add_ticket(ticket) // add itself to waiting list |__ m_wait.reset_status() // under protection of m_rwlock |__ mysql_prlock_unlock |__ MDL_context::m_waiting_for = ticket // inform deadlock detector, will_wait_for() |__ find_deadlock |__ m_wait.timed_wait() // two classes, one(with high prio) would notify other threads to release locks, then wait; the other would just wait |__ MDL_context::m_waiting_for = NULL // done_waiting_for() |__ check wait_status and clean up
释放锁的逻辑相较于获取简单:
release_lock --> if ticket->m_is_fast_path --> get_unobtrusive_lock_increment | |__ old_state = lock->m_m_fast_path_state | |__ do{mysql_prlock_wrlock();reschedule_waiters();mysql_prlock_unlock()} while(!lock->fast_path_state_cas()) |__ slow path --> lock->remove_ticket --> mysql_prlock_wrlock() | |__ remove_ticket | |__ update flags, fast_path_state_cas() | |__ reschedule_waiters() | |__ mysql_prlock_unlock() |__ MDL_context::m_tickets.remove() |__ MDL_ticket::destroy() reschedule_waiters(rwlock held) --> iterate m_waiting --> can_grant_lock --> yes --> m_wait.set_status(GRANTED) --> mysql_mutex_lock() | |__ m_wait_status = status_arg | |__ mysql_cond_signal() | |__ mysql_mutex_unlock() |__ cleanups, m_waiting.remove_ticket, m_granted.add_ticket
其他的一些 MDL 接口都是基于这两个基本操作实现的,比如 MDLcontext::upgradesharedlock() 是调用 MDLcontext::acquirelock() 获取新的 ticket,并移除老的 ticket;MDLcontext::releaselocksstoredbefore() 是循环调用 MDLcontext::release_lock(),实现锁获取的回滚。
上面章节中提到了锁获取过程中,线程在进入睡眠状态前会调用 find_deadlock() 检测是否存在死锁。死锁产生的根本原因是线程在持有锁的同时去等待另一个锁,所以就会存在线程之间的依赖关系,如果依赖关系中出现了环,则产生了死锁。
上面章节还提到,MDLcontext, MDLticket 和 MDL_lock 中存了很多状态信息,这些状态信息是死锁检测的依据。死锁检测过程具体为:
调用栈为:
MDL_context::find_deadlock --> while(1) --> Deadlock_detection_visitor // each round would spot one victim, remove one node from graph |__ MDL_context::visit_subgraph --> MDL_ticket::accept_visitor --> MDL_lock::visit_subgraph --> mysql_prlock_rdlock(&m_rwlock) | |__ enter_node // depth++, if overflow, terimate and say deadlock found | |__ BFS direct neibors, including granted and waiting, inspect_edge() --> check if it is starting MDL_context | |__ DFS neibors, including granted and waiting, recursive MDL_context::visit_subgraph | |__ leave_node // depth-- | |__ mysql_prlock_unlock |__ set victim m_waiting_status VICTIM
上面章节也提到了,所有的锁对象 (除了 COMMIT 和 GLOBAL) 都存放在一个全局的 hash 表中(LFHASH),这个 hash 表是实现为 lock-free 的。在 5.7 之前的版本中,hash 表被实现为带分区的形式,但访问需要加锁。LFHASH 的实现是很经典的 lock-free 数据结构实现方式:用 Copy-on-Write 的方式,每次需要修改共享对象时都重新构造一个新的对象,然后原子地将指向对象的全局指针替换掉。这种方式有个问题是旧对象内存什么时候释放,如果在指针替换后立即释放,之前线程可能会在内存 free 掉之后还会通过之前存在 local 的指针访问该对象,那么就可能会出现 segment fault。
解决这个问题的方法有很多:
thd1: save object pointer to register(old one) //1 thd2: save object pointer to register(old one) //2 thd2: override global pointer to new object //3 thd2: read and check reference counter of old object //4 thd2: free old object memory //5 thd1: atomic increment reference counter, SIGSEGV //6 key here is 1 and 6 are not atomic
还是没能避免 segment fault;进一步解决这个问题的方式是用一个独立的读写锁保护引用计数,或者引入一个中间层结构体,给每个结构体加一个版本号,将引用计数和版本号一起存放在一个原子数据类型中,并通过原子读写指令在写之前判断版本和读时是否一致,通过这种方式将保护引用计数的读写锁去掉。后者的关键在于中间层结构体需要在并发周期内一直可访问,所以中间层结构体需要专门的分配器。
void *g_hp_local_array[MAX_THREAD_ID]; // global void *ref = NULL; while(1) { ref = g_ptr; // save global pointer locally //1 g_hp_local_array[thread_id] = ref; //2 if (ref == g_ptr) // not overriden, safe now on //3 break; } // the double check is very important, if thd1 executes line 1, then is scheduled before line 2, then thd 2 // find no reference for the old g_ptr in g_hp_local_array, so frees the object, then thd1 gets back to line 2
这个逻辑在 MySQL 中也是存在的,可以在 my_lfind() 函数中看到;
本文介绍了 MDL 子系统的使用和实现细节,包括锁获取与释放,死锁检测和实现中用到的相关 lock free 优化。