MySQL 5.7中MDL锁问题排查

MDL锁

在MySQL5.5版本中引入了MDL锁(metadata lock),即元数据锁(表锁)。其目的是为了在并发环境下维护表元数据的数据一致性。

试想一下,一个查询正在遍历表数据,执行期间另一个线程对表结构进行修改,那么查询线程拿到结果就和表结构对不上。

MDL锁的加锁规则:在对表做增删改查操作时,会加MDL读锁;在对表做结构变更操作时,会加MDL写锁。且读写互斥,读读不互斥。

场景复现

我们准备四个MySQL会话来复现因为加索引而引发的数据库崩溃。

会话1

开启事务,并且查询表user。(不提交事务)

MySQL 5.7中MDL锁问题排查_第1张图片

会话2

查询表user。

MySQL 5.7中MDL锁问题排查_第2张图片

会话3

为user表中的name字段添加索引。

此时,这条SQL已被阻塞。

会话4

查询user表。

这条查询语句也被阻塞。且后续我们后面对user表的所有操作都会被阻塞,这也是容易引起线上服务奔溃的原因(线程爆满)。


此时我们新开一个会话来查看现在数据库中线程的执行情况。(SHOW PROCESSLIST;)

MySQL 5.7中MDL锁问题排查_第3张图片

出现这个状态表示的是,现在有一个线程正在表 user 上请求或者持有 MDL 写锁,把 select 语句堵住了。

可以看到有两个线程正在等待MDL锁释放,Info中对应也就是我们会话3、4的SQL。

当然,如果此时我们将会话1的事务提交,其他会话也能顺利执行。

如何解决?

这类问题的处理方式,就是找到谁持有 MDL 写锁,然后把它 kill 掉。

  • 通过下面命令直接找到造成阻塞的SQL的线程ID,直接kill掉
SELECT locked_schema,
locked_table,
locked_type,
waiting_processlist_id,
waiting_age,
waiting_query,
waiting_state,
blocking_processlist_id,
blocking_age,
substring_index(sql_text,"transaction_begin;" ,-1) AS blocking_query,
sql_kill_blocking_connection
FROM 
( 
SELECT 
b.OWNER_THREAD_ID AS granted_thread_id,
a.OBJECT_SCHEMA AS locked_schema,
a.OBJECT_NAME AS locked_table,
"Metadata Lock" AS locked_type,
c.PROCESSLIST_ID AS waiting_processlist_id,
c.PROCESSLIST_TIME AS waiting_age,
c.PROCESSLIST_INFO AS waiting_query,
c.PROCESSLIST_STATE AS waiting_state,
d.PROCESSLIST_ID AS blocking_processlist_id,
d.PROCESSLIST_TIME AS blocking_age,
d.PROCESSLIST_INFO AS blocking_query,
concat('KILL ', d.PROCESSLIST_ID) AS sql_kill_blocking_connection
FROM performance_schema.metadata_locks a JOIN performance_schema.metadata_locks b ON a.OBJECT_SCHEMA = b.OBJECT_SCHEMA AND a.OBJECT_NAME = b.OBJECT_NAME
AND a.lock_status = 'PENDING'
AND b.lock_status = 'GRANTED'
AND a.OWNER_THREAD_ID <> b.OWNER_THREAD_ID
AND a.lock_type = 'EXCLUSIVE'
JOIN performance_schema.threads c ON a.OWNER_THREAD_ID = c.THREAD_ID JOIN performance_schema.threads d ON b.OWNER_THREAD_ID = d.THREAD_ID
) t1,
(
SELECT thread_id, group_concat( CASE WHEN EVENT_NAME = 'statement/sql/begin' THEN "transaction_begin" ELSE sql_text END ORDER BY event_id SEPARATOR ";" ) AS sql_text
FROM
performance_schema.events_statements_history
GROUP BY thread_id
) t2
WHERE t1.granted_thread_id = t2.thread_id \G

返回结果如下:

MySQL 5.7中MDL锁问题排查_第4张图片

直接根据返回的sql_kill_blocking_connection,kill掉对应线程即可。

注意:可能会返回Empty set (0.00 sec),这是因为以上SQL用到了MySQL5.7.22中新增的performance_schema库中的metadata_locks表,用于专门记录MDL的相关信息。必须提前要开启这个instrument !!!。

UPDATE performance_schema.setup_instruments SET ENABLED = 'YES', TIMED = 'YES' where name='wait/lock/metadata/sql/mdl';
  • 查看未提交事务
SELECT trx_state, trx_started, trx_mysql_thread_id, trx_query FROM information_schema.innodb_trx;

返回结果:

MySQL 5.7中MDL锁问题排查_第5张图片

我们可以通过对比时间,找出可能是造成阻塞的线程ID,将其kill掉。并且我们当前的会话id也会出现在上面,那么只需要kill掉另一个早一点的线程即可。

安全建议

  1. 开启metadata_locks表记录MDL锁。
  2. 尽量不要在业务高峰期执行DDL操作。
  3. 在进行DDL前,先查看是否有正在执行的事务,如果要做 DDL 变更的表刚好有长事务在执行,要考虑先暂停 DDL,或者 kill 掉这个长事务。
  4. 尽量避免出现长事务。

你可能感兴趣的:(mysql,数据库)