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 1
tx1: END; -- commit,写入binlog 2,备库先重放binlog 1,重放binlog 2时出错
于是5.5版本引入了MDL,实现为事务级别,事务中获取的MDL锁在事务提交或回滚时释放,并在后续版本中针对最初实现做了很多优化,包括:
本文将介绍5.7中的实现。搜索关注“腾讯云数据库”官方微信立得10元腾讯云无门槛代金券,体验移动端一键管理数据库,学习更多数据库技术实战教程。
内核开发过程中有时候需要使用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实现方式)。搜索关注“腾讯云数据库”官方微信立得10元腾讯云无门槛代金券,体验移动端一键管理数据库,学习更多数据库技术实战教程。
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用
值得注意的两点:
这几种模式中,SU比较特别,其他的模式都很容易理解。SU特别之处在于它的语义类似SR,但SR和SR之间是兼容的,而SU和SU之间是冲突的。SU的存在是为了解决一类死锁问题。对于某些DDL语句的实现(如ALTER TABLE),为了不长时间阻塞其他数据库操作,需要先对表获取一个低级别MDL锁以读取数据,然后做一些很耗时的操作,最后关键步骤(如rename)将之前的低级别MDL锁升级为X锁以修改元数据。如果没有SU锁模式的存在,最合适的低级别MDL锁模式即为SR,那么就会存在如下场景:
tx1: acquire SR lock mode
tx2: acquire SR lock mode
tx1: acquire X lock mode
将上述场景中的SR替换为SU就能避免死锁,tx2在获取SU时就会阻塞,因为和tx1持有的SU冲突;5.7中的ALTER TABLE ... ALGORITHM = COPY就是这么实现的,先在打开表时获取SR锁,然后升级为SNW并读取表数据(其他线程不能写入表数据了),构建临时表,在最后rename表阶段升级为X模式。
另一个涉及锁升级的DDL语句是CREATE TABLE,首先获取S模式的MDL锁,用于检查表是否存在,然后升级为X模式并真正建表;它是直接从S升级到X,所以会存在上述的死锁问题,解决方法是代码中加了一个重试逻辑;
各种表模式的核心含义在于它们相互之间的兼容与互斥关系,这种关系可以划分到两类里,范围锁模式的互斥关系和目标锁模式的互斥关系,每一类下又包括两种互斥关系,一种是与已经被授予的锁模式的互斥关系,另一种是与等待队列中锁模式的互斥关系,获取锁时需要先判断是否和已经授予的锁模式冲突,如果不冲突再判断和等待队列中的请求是否冲突,都不冲突才能获取锁。第二个冲突检测的目的是给各种锁模式赋予了优先级,后来的锁只有优先级高于等待队列中的锁才能进行抢占。搜索关注“腾讯云数据库”官方微信立得10元腾讯云无门槛代金券,体验移动端一键管理数据库,学习更多数据库技术实战教程。
冲突矩阵从语义中已经可以推演出来了,具体为:
范围锁互斥矩阵:
| 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) --> ll_futex_timed_wait --> syscall futex
除此之外,MySQL在MDL中还自己维护了一个等待队列,具体在MDL_lock::m_waiting中 3.2 MDL具体实现
代码中的重要数据结构和类定义包括:
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
锁获取的流程为:
代码中用一个原子数据类型变量m_fast_path_state的各个比特位段记录各种nonobtrusive锁的授予计数,以及当前锁上是否授予过obtrusive mode锁,由于obtrusive mode锁之间不冲突,如果之前被授予的锁模式全部为nonobtrusive mode,则可以直接原子地将相应模式的授予计数加一即可成功获取锁,在这个代码路径中,整个锁获取过程只原子地操作m_fast_path_state, 是lock free的,即为fast path优化;
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接口都是基于这两个基本操作实现的,比如MDL_context::upgrade_shared_lock()是调用MDL_context::acquire_lock()获取新的ticket,并移除老的ticket;MDL_context::release_locks_stored_before()是循环调用MDL_context::release_lock(),实现锁获取的回滚。
上面章节中提到了锁获取过程中,线程在进入睡眠状态前会调用find_deadlock()检测是否存在死锁。死锁产生的根本原因是线程在持有锁的同时去等待另一个锁,所以就会存在线程之间的依赖关系,如果依赖关系中出现了环,则产生了死锁。
上面章节还提到,MDL_context, MDL_ticket和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表中(LF_HASH),这个hash表是实现为lock-free的。在5.7之前的版本中,hash表被实现为带分区的形式,但访问需要加锁。LF_HASH的实现是很经典的lock-free数据结构实现方式:用Copy-on-Write的方式,每次需要修改共享对象时都重新构造一个新的对象,然后原子地将指向对象的全局指针替换掉。这种方式有个问题是旧对象内存什么时候释放,如果在指针替换后立即释放,之前线程可能会在内存free掉之后还会通过之前存在local的指针访问该对象,那么就可能会出现segment fault。搜索关注“腾讯云数据库”官方微信立得10元腾讯云无门槛代金券,体验移动端一键管理数据库,学习更多数据库技术实战教程。
解决这个问题的方法有很多:
thd1: save object pointer to register(old one)
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优化。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。