MDL 顾名思义为元数据锁,是 MySQL 计算层的锁,并非 innodb 的行锁,本文主要将对该锁的机制进行代码级分析, 欢迎大家一起交流联系!
基本知识
-
MDL_key(锁键):它表示了一个锁的标识,通常与需要保护的资源相关联。例如,一个表或索引的名称可以用作锁的键。MDL_key 用于在 MDL 系统中唯一标识一个锁请求或锁对象。
-
MDL_lock(锁对象):它代表了一个实际的锁,用于保护资源。在 MDL 系统中,每个 MDL_lock 对象都与一个特定的 MDL_key 相关联,表示该锁对象保护的资源。MDL_lock 维护了与该锁对象相关的锁请求队列和其他必要的状态信息。
-
MDL_request(锁请求):它表示一个需要获取(或释放)特定锁对象的请求。每个 MDL_request 对象都与一个特定的 MDL_key 相关联。一个 MDL 请求包括锁类型(例如,共享锁、排他锁等)和作用范围(例如,事务级别、语句级别等)等信息。
-
MDL_ticket(锁票据):它代表一个特定的锁请求在 MDL 系统中的状态。每个 MDL_ticket 对象都包含一个 MDL_request 对象以及与之关联的锁对象和上下文信息。MDL_ticket 对象被用于在 MDL 系统中跟踪和管理锁的状态,包括锁的持有者、等待者等。
综上所述,MDL_key 用于唯一标识一个锁请求或锁对象。MDL_lock 是代表实际锁的对象,并与 MDL_key 相关联。MDL_request 表示一个需要获取或释放特定锁对象的请求,并与 MDL_key 相关联。而 MDL_ticket 是锁请求在 MDL 系统中的状态表示,包含了 MDL_request、锁对象和上下文信息。
-
Mdl 在使用过程中被细分为 lock granted metadata (class MDL _ticket) 和 lock request metadata (class MDL_request)
-
Ticket 可以理解为入场券,即已经被授予的锁
-
Request 即加锁请求
-
-
MDL_context 数据结构概览
class MDL_context {
public:
MDL_wait m_wati;
private:
MDL_ticket_store m_ticket_store;
MDL_context_owner *m_owner;
bool m_needs_thr_lock_abort;
bool m_force_dml_deadlock_weight;
mysql_prlock_t m_LOCK_waiting_for;
MDL_wait_for_subgraph *m_waiting_for;
LF_PINS *m_pins;
uint m_rand_state;
};
- MDL_context 是 session 级的,在全局通过 MDL_map 作为入口
MDL 生命周期
锁系统初始化
init_server_components() -> mdl_init() -> mdl_locks.init()
关键数据结构:static MDL_map mdl_locks; (全局 MDL_map 对象)
class MDL_map {
/** LF_HASH with all locks in the server. */
LF_HASH m_locks;
/** Pre-allocated MDL_lock object for GLOBAL namespace. */
MDL_lock *m_global_lock;
/** Pre-allocated MDL_lock object for COMMIT namespace. */
MDL_lock *m_commit_lock;
/** Pre-allocated MDL_lock object for ACL_CACHE namespace. */
MDL_lock *m_acl_cache_lock;
/** Pre-allocated MDL_lock object for BACKUP_LOCK namespace. */
MDL_lock *m_backup_lock;
std::atomic<int32> m_unused_lock_objects;
/** Pre-allocated MDL_lock object for Percona BACKUP TABLES namespace. */
MDL_lock *m_backup_tables_lock;
};
锁上下文初始化
对于每个 session 会初始化一个 mdl_context
class MDL_context {
void init(MDL_context_owner *arg) { m_owner = arg; }
};
class THD : public MDL_context_owner,
public Query_arena,
public Open_tables_state {
public:
MDL_context mdl_context;
};
锁的申请
构造 MDL_request 对象,然后将该对象交给 mdl_context 函数 acquire_locks() 来获取锁
一个典型的示例
// 入口不只这一个,还有很多如 lock_table_names() 等
bool lock_object_name(THD *thd, MDL_key::enum_mdl_namespace mdl_type,
const char *db, const char *name) {
MDL_request_list mdl_requests;
MDL_request global_request;
MDL_request schema_request;
MDL_request mdl_request;
MDL_request backup_lock_request;
MDL_key mdl_key;
if (thd->locked_tables_mode) {
my_error(ER_LOCK_OR_ACTIVE_TRANSACTION, MYF(0));
return true;
}
assert(name);
/* create_mdl_key 实际上是走的
mdl_key->mdl_key_init(
type == RT_FUNCTION ? MDL_key::FUNCTION : MDL_key::PROCEDURE,
schema_name.c_str(), normalized_name, len, name.c_str());
*/
switch (mdl_type) {
case MDL_key::FUNCTION:
dd::Function::create_mdl_key(db, name, &mdl_key);
break;
case MDL_key::PROCEDURE:
dd::Procedure::create_mdl_key(db, name, &mdl_key);
break;
case MDL_key::EVENT:
dd::Event::create_mdl_key(db, name, &mdl_key);
break;
case MDL_key::RESOURCE_GROUPS:
dd::Resource_group::create_mdl_key(name, &mdl_key);
break;
default:
assert(false);
return true;
}
DEBUG_SYNC(thd, "before_wait_locked_pname");
if (thd->global_read_lock.can_acquire_protection()) return true;
// GLOBAL 空间内申请语句级别的 意向排他锁
MDL_REQUEST_INIT(&global_request, MDL_key::GLOBAL, "", "",
MDL_INTENTION_EXCLUSIVE, MDL_STATEMENT);
// SCHEMA 空间中申请事务级别的 意向排他锁
MDL_REQUEST_INIT(&schema_request, MDL_key::SCHEMA, db, "",
MDL_INTENTION_EXCLUSIVE, MDL_TRANSACTION);
// 用给定的 mdl_key 创建一个事务级别的 排他锁
MDL_REQUEST_INIT_BY_KEY(&mdl_request, &mdl_key, MDL_EXCLUSIVE,
MDL_TRANSACTION);
MDL_REQUEST_INIT(&backup_lock_request, MDL_key::BACKUP_LOCK, "", "",
MDL_INTENTION_EXCLUSIVE, MDL_TRANSACTION);
mdl_requests.push_front(&mdl_request);
mdl_requests.push_front(&schema_request);
mdl_requests.push_front(&global_request);
mdl_requests.push_front(&backup_lock_request);
if (thd->mdl_context.acquire_locks(&mdl_requests,
thd->variables.lock_wait_timeout))
return true;
/*
Now when we have protection against concurrent change of read_only
option we can safely re-check its value.
*/
if (check_readonly(thd, true)) return true;
/*
We have an IX lock on the schema name, so we can check the read
only option of the schema without worrying about a concurrent
ALTER SCHEMA.
*/
if (check_schema_readonly(thd, db)) return true;
DEBUG_SYNC(thd, "after_wait_locked_pname");
return false;
}
锁的施加
大部分是通过 mdl_context->acquire_locks 来进行的,但所有的锁申请的入口都是 acquire_lock()
/*
这段代码其实核心就是按序获取了锁 acquire_lock(*p_req, lock_wait_timeout)
*/
bool MDL_context::acquire_locks(MDL_request_list *mdl_requests,
Timeout_type lock_wait_timeout) {
MDL_request_list::Iterator it(*mdl_requests);
MDL_request **p_req;
MDL_savepoint mdl_svp = mdl_savepoint();
/*
Remember the first MDL_EXPLICIT ticket so that we can release
any new such locks taken if acquisition fails.
*/
MDL_ticket *explicit_front = m_ticket_store.front(MDL_EXPLICIT);
const size_t req_count = mdl_requests->elements();
if (req_count == 0) return false;
/* Sort requests according to MDL_key. */
Prealloced_array<MDL_request *, 16> sort_buf(
key_memory_MDL_context_acquire_locks);
if (sort_buf.reserve(req_count)) return true;
for (size_t ii = 0; ii < req_count; ++ii) {
sort_buf.push_back(it++);
}
std::sort(sort_buf.begin(), sort_buf.end(), MDL_request_cmp());
size_t num_acquired = 0;
for (p_req = sort_buf.begin(); p_req != sort_buf.end(); p_req++) {
if (acquire_lock(*p_req, lock_wait_timeout)) goto err;
++num_acquired;
}
return false;
err:
/*
Release locks we have managed to acquire so far.
Use rollback_to_savepoint() since there may be duplicate
requests that got assigned the same ticket.
*/
rollback_to_savepoint(mdl_svp);
/*
Also release the MDL_EXPLICIT locks taken in this call.
The locking service plugin API needs acquisition of such
locks to be atomic as well.
*/
release_locks_stored_before(MDL_EXPLICIT, explicit_front);
/* Reset lock requests back to its initial state. */
for (p_req = sort_buf.begin(); p_req != sort_buf.begin() + num_acquired;
p_req++) {
(*p_req)->ticket = nullptr;
}
return true;
}
/**
Acquire one lock with waiting for conflicting locks to go away if needed.
@param [in,out] mdl_request Lock request object for lock to be acquired
@param lock_wait_timeout Seconds to wait before timeout.
@retval false Success. MDL_request::ticket points to the ticket
for the lock.
@retval true Failure (Out of resources or waiting is aborted),
*/
bool MDL_context::acquire_lock(MDL_request *mdl_request,
Timeout_type lock_wait_timeout) {
// 找到与锁请求可相容(代码中标记为 fast path)的 MDL_ticket 锁,如果存在直接用,如果不存在则创建新的 MDL_ticket 对象
// 另外,在找 MDL_ticket 对象的时候会先看所属大类型的 MDL_lock 是否存在,如果不存在会先在
// MDL_map 中创建
MDL_lock *lock;
MDL_ticket *ticket = nullptr;
try_acquire_lock_impl(mdl_request, &ticket);
// 把 MDL_ticket 对象与 MDL_lock 类互相注册
lock = ticket->m_lock;
lock->m_waiting.add_ticket(ticket);// 把 ticket 加入到等待队列
// 死锁检测
.........
done_waiting_for(); // 标志死锁检测的结束
// 状态检查:原因是 ticket 处于等待状态,这个期间可能会在其它 session 释放了锁之后通过
// reschedule_waiters() 被授予锁,因此需要检查
if (wait_status != MDL_wait::GRANTED) {
// 锁不能被授予则去除注册到 MDL_lock 中的 ticket 对象
lock->remove_ticket(this, m_pins, &MDL_lock::m_waiting, ticket);
/*
If SEs were notified about impending lock acquisition, the failure
to acquire it requires the same notification as lock release.
*/
if (ticket->m_hton_notified) {
mysql_mdl_set_status(ticket->m_psi, MDL_ticket::POST_RELEASE_NOTIFY);
m_owner->notify_hton_post_release_exclusive(&mdl_request->key);
}
// 销毁 ticket 对象
MDL_ticket::destroy(ticket);
// 通知原因
switch (wait_status) {
case MDL_wait::VICTIM: // 死锁发生
my_error(ER_LOCK_DEADLOCK, MYF(0));
break;
case MDL_wait::TIMEOUT: // 超时
my_error(ER_LOCK_WAIT_TIMEOUT, MYF(0));
break;
case MDL_wait::KILLED: // session 被 kill 了
if (get_owner()->is_killed() == ER_QUERY_TIMEOUT)
my_error(ER_QUERY_TIMEOUT, MYF(0));
else
my_error(ER_QUERY_INTERRUPTED, MYF(0));
break;
default: // 不能走到这,如果走到这里,则引擎有 bug
assert(0);
break;
}
return true;
}
// 所有检测都已经完成,说明符合锁授予情况,则授予锁
m_ticket_store.push_front(mdl_request->duration, ticket);
mdl_request->ticket = ticket; // 把 ticket 也注册到 MDL_request 对象上
mysql_mdl_set_status(ticket->m_psi, MDL_ticket::GRANTED);
return false;
}
bool MDL_context::try_acquire_lock_impl(MDL_request *mdl_request,
MDL_ticket **out_ticket) {
// 寻找能用的 ticket
if ((ticket = find_ticket(mdl_request, &found_duration))){.....}
// MDL_map 中创建大类
if (fix_pins()) return true;
}
这里对锁复用做一个说明,如果执行下列 SQL
S1 BEGIN;
S2 INSERT INTO t1 VALUES(1);
S3 INSERT INTO t1 VALUES(2);
S2 会创建一个 mdl,S3 要的锁会和 S2 是一致的,所以直接通过 find_ticket 找到对应的锁
这里单独说一下如果锁类型不存在,是如何在 MDL_map 里创建的,实际上是走了 MDL_map::find_or_insert
MDL_lock *MDL_map::find_or_insert(LF_PINS *pins, const MDL_key *mdl_key,
bool *pinned) {
MDL_lock *lock = nullptr;
while ((lock = find(pins, mdl_key, pinned)) == nullptr) {
/*
MDL_lock for key isn't present in hash, try to insert new object.
This can fail due to concurrent inserts.
*/
int rc = lf_hash_insert(&m_locks, pins, mdl_key);
if (rc == -1) /* If OOM. */
return nullptr;
else if (rc == 0) {
/*
New MDL_lock object is not used yet. So we need to
increment number of unused lock objects.
*/
++m_unused_lock_objects;
}
}
if (lock == MY_LF_ERRPTR) {
/* If OOM in lf_hash_search. */
return nullptr;
}
return lock;
}
锁的释放
直接走 MDL_context::release_lock 就好, 该函数也会在回滚阶段被调用
针对快速释放和非快速释放有不同的算法,快速释放通常是指 MDL_key 不为 GLOBAL 和 COMMIT 的
void MDL_context::release_lock(enum_mdl_duration duration, MDL_ticket *ticket) {
MDL_lock *lock = ticket->m_lock;
MDL_key key_for_hton;
DBUG_TRACE;
DBUG_PRINT("enter", ("db=%s name=%s", lock->key.db_name(), lock->key.name()));
assert(this == ticket->get_ctx());
mysql_mutex_assert_not_owner(&LOCK_open);
// Remove ticket from the Ticket_store before actually releasing the lock,
// so this removal process can safely reference MDL_lock::m_key in cases
// when Ticket_store uses hash-based secondary index.
m_ticket_store.remove(duration, ticket);
/*
If lock we are about to release requires post-release notification
of SEs, we need to save its MDL_key on stack. This is necessary to
be able to pass this key to SEs after corresponding MDL_lock object
might be freed as result of lock release.
*/
if (ticket->m_hton_notified) key_for_hton.mdl_key_init(&lock->key);
if (ticket->m_is_fast_path) {
/*
We are releasing ticket which represents lock request which was
satisfied using "fast path". We can use "fast path" release
algorithm of release for it as well.
*/
MDL_lock::fast_path_state_t unobtrusive_lock_increment =
lock->get_unobtrusive_lock_increment(ticket->get_type());
bool is_singleton = mdl_locks.is_lock_object_singleton(&lock->key);
/* We should not have "fast path" tickets for "obtrusive" lock types. */
assert(unobtrusive_lock_increment != 0);
/*
We need decrement part of m_fast_path_state which holds number of
acquired "fast path" locks of this type. This needs to be done
by atomic compare-and-swap.
The same atomic compare-and-swap needs to check:
*) If HAS_OBSTRUSIVE flag is set. In this case we need to acquire
MDL_lock::m_rwlock before changing m_fast_path_state. This is
needed to enforce invariant [INV1] and also because we might
have to atomically wake-up some waiters for our "unobtrusive"
lock to go away.
*) If we are about to release last "fast path" lock and there
are no "slow path" locks. In this case we need to count
MDL_lock object as unused and maybe even delete some
unused MDL_lock objects eventually.
Similarly to the case with "fast path" acquisition it is OK to
perform ordinary read of MDL_lock::m_fast_path_state as correctness
of value returned by it will be validated by atomic compare-and-swap.
Again, in theory, this algorithm will work correctly if the read will
return random values.
*/
MDL_lock::fast_path_state_t old_state = lock->m_fast_path_state;
bool last_use;
do {
if (old_state & MDL_lock::HAS_OBTRUSIVE) {
mysql_prlock_wrlock(&lock->m_rwlock);
/*
It is possible that obtrusive lock has gone away since we have
read m_fast_path_state value. This means that there is possibility
that there are no "slow path" locks (HAS_SLOW_PATH is not set) and
we are about to release last "fast path" lock. In this case MDL_lock
will become unused and needs to be counted as such eventually.
*/
last_use = (lock->fast_path_state_add(-unobtrusive_lock_increment) ==
unobtrusive_lock_increment);
/*
There might be some lock requests waiting for ticket being released
to go away. Since this is "fast path" ticket it represents
"unobtrusive" type of lock. In this case if there are any waiters
for it there should be "obtrusive" type of request among them.
*/
if (lock->m_obtrusive_locks_granted_waiting_count)
lock->reschedule_waiters();
mysql_prlock_unlock(&lock->m_rwlock);
goto end_fast_path;
}
/*
If there are no "slow path" locks (HAS_SLOW_PATH is not set) and
we are about to release last "fast path" lock - MDL_lock object
will become unused and needs to be counted as such.
*/
last_use = (old_state == unobtrusive_lock_increment);
} while (!lock->fast_path_state_cas(
&old_state, old_state - unobtrusive_lock_increment));
end_fast_path:
/* Don't count singleton MDL_lock objects as unused. */
if (last_use && !is_singleton) mdl_locks.lock_object_unused(this, m_pins);
} else {
/*
Lock request represented by ticket was acquired using "slow path"
or ticket was materialized later. We need to use "slow path" release.
*/
lock->remove_ticket(this, m_pins, &MDL_lock::m_granted, ticket);
}
if (ticket->m_hton_notified) {
mysql_mdl_set_status(ticket->m_psi, MDL_ticket::POST_RELEASE_NOTIFY);
m_owner->notify_hton_post_release_exclusive(&key_for_hton);
}
MDL_ticket::destroy(ticket);
}
锁降级/升级
可以对锁降级,但不会自动升级锁,代码里说的锁可升级也是指的利用锁相容性原理对锁进行升级,而不是为了减少资源的使用而进行的策略
Savepoint 机制
打 savepoint
// 一个典型的使用方式
MDL_savepoint mdl_savepoint = thd->mdl_context.mdl_savepoint()
..........
thd->mdl_context.rollback_to_savepoint(mdl_savepoint);
// 这里实际上就是走的 MDL_context::rollback_to_savepoint()
MDL_savepoint mdl_savepoint() {
return MDL_savepoint(m_ticket_store.front(MDL_STATEMENT),
m_ticket_store.front(MDL_TRANSACTION));
}
class MDL_savepoint {
public:
MDL_savepoint() = default;
private:
MDL_savepoint(MDL_ticket *stmt_ticket, MDL_ticket *trans_ticket)
: m_stmt_ticket(stmt_ticket), m_trans_ticket(trans_ticket) {}
friend class MDL_context;
private:
/**
Pointer to last lock with statement duration which was taken
before creation of savepoint.
*/
MDL_ticket *m_stmt_ticket;
/**
Pointer to last lock with transaction duration which was taken
before creation of savepoint.
*/
MDL_ticket *m_trans_ticket;
};
MDL 回滚
void MDL_context::rollback_to_savepoint(const MDL_savepoint &mdl_savepoint) {
DBUG_TRACE;
/* If savepoint is NULL, it is from the start of the transaction. */
release_locks_stored_before(MDL_STATEMENT, mdl_savepoint.m_stmt_ticket);
release_locks_stored_before(MDL_TRANSACTION, mdl_savepoint.m_trans_ticket);
}
// 显然这里是个前向链表,依次遍历释放 svp 之后获得的锁
void MDL_context::release_locks_stored_before(enum_mdl_duration duration,
MDL_ticket *sentinel) {
DBUG_TRACE;
MDL_ticket *ticket;
MDL_ticket_store::List_iterator it = m_ticket_store.list_iterator(duration);
if (m_ticket_store.is_empty(duration)) {
return;
}
while ((ticket = it++) && ticket != sentinel) {
DBUG_PRINT("info", ("found lock to release ticket=%p", ticket));
release_lock(duration, ticket);
}
}