在MySQL5.5版本中引入了MDL锁(metadata lock),即元数据锁(表锁)。其目的是为了在并发环境下维护表元数据的数据一致性。
试想一下,一个查询正在遍历表数据,执行期间另一个线程对表结构进行修改,那么查询线程拿到结果就和表结构对不上。
MDL锁的加锁规则:在对表做增删改查操作时,会加MDL读锁;在对表做结构变更操作时,会加MDL写锁。且读写互斥,读读不互斥。
我们准备四个MySQL会话来复现因为加索引而引发的数据库崩溃。
会话1
开启事务,并且查询表user。(不提交事务)
会话2
查询表user。
会话3
为user表中的name字段添加索引。
此时,这条SQL已被阻塞。
会话4
查询user表。
这条查询语句也被阻塞。且后续我们后面对user表的所有操作都会被阻塞,这也是容易引起线上服务奔溃的原因(线程爆满)。
此时我们新开一个会话来查看现在数据库中线程的执行情况。(SHOW PROCESSLIST;)
出现这个状态表示的是,现在有一个线程正在表 user 上请求或者持有 MDL 写锁,把 select 语句堵住了。
可以看到有两个线程正在等待MDL锁释放,Info中对应也就是我们会话3、4的SQL。
当然,如果此时我们将会话1的事务提交,其他会话也能顺利执行。
这类问题的处理方式,就是找到谁持有 MDL 写锁,然后把它 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
返回结果如下:
直接根据返回的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;
返回结果:
我们可以通过对比时间,找出可能是造成阻塞的线程ID,将其kill掉。并且我们当前的会话id也会出现在上面,那么只需要kill掉另一个早一点的线程即可。