mysql创建索引导致死锁,数据库崩溃,mysql的表级锁之【元数据锁(meta data lock,MDL)】全解

文章目录

问题提出

一次生产上优化查询,需要给表添加索引,于是乎,造成了数据库的崩溃,迫不得已重启了数据库才解决问题。

为什么添加一个索引,会导致这么严重的问题呢?

平时添加一个索引也没见数据库崩溃啊?

难道是造成了死锁?

别急,接下来我们慢慢分析。

什么是元数据锁(meta data lock,MDL)

在MySQL5.5.3之前,有一个著名的bug#989(bug链接: MySQL Bugs: #989: If DROP TABLE while there's an active transaction, wrong binlog order),大致如下:

sql 复制代码
# session1:
BEGIN;
INSERT INTO t ... ;
COMMIT;
 
# session2:
DROP TABLE t;
 
# 如果user1在一个表上有一个活动的事务,然后user2删除这个表,那么user1会提交,那么在binlog中我们有如下内容(binlog记录的操作顺序顺序):
1. 
DROP TABLE t;
 
2.
BEGIN;
INSERT INTO t ... ;
COMMIT;

很显然mysql执行时会先删除表t,然后执行insert 会报1032 error。

再举一个简单的例子,如果你在查询一个表的过程中,另外一个session对该表删除了一个列,那前面的查询到底该显示什么呢?如果在RR隔离级别下,事物中再次执行相同的语句还会和之前结果一致吗?为了防止这种情况,表查询开始MySQL会在表上加一个锁,来防止被别的session修改了表定义,这个锁就叫'metadata lock',简称MDL,翻译成中文也叫'元数据锁',它是一个表级锁

什么情况下会添加元数据锁

首先给出答案:mysql所有的增删改查以及ddl都会加mdl锁。

什么是DDL(Data Definition Languages)语句:数据定义语言,这些语句定义了不同的数据段,数据库,表,列,索引等数据库对象。常用的语句关键字主要包括create,drop,alter等。

也就是说,增删改查、数据库的创建、销毁、索引的修改、字段的调整等等操作,都会加一个表级锁------元数据锁(meta data lock,MDL)

这是不是听起来很恐怖的事情?所有的操作都会加一个表级锁。

但是!不要慌张,请继续往下看。

元数据锁工作原理

在 MySQL 5.5 版本中引入了 MDL(元数据锁),当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁。

读写锁大家应该都很清楚吧。

  • 读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。
  • 读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。

MDL写锁只有在执行DDL语句的时候才会加,平时我们的增删改查只是加了MDL读锁,MDL读锁之间是可以共享的,并不会出现锁等待的情况。

创建索引导致数据库崩溃

线上某数据库意外发现缺少索引,并且该表的数据量很少,只有几万条记录而已,因此很随意地尝试给该表添加索引。原本预期该表的记录很少,添加索引的耗时应该很短,结果却直接导致该表被锁,所有该表的增删改查操作全部阻塞,继而影响到了线上业务。

发现锁表后,执行show processlist发现大量线程阻塞,状态显示Waiting for table metadata lock。通过命令终止了DDL线程,该表恢复正常。mysql从5.6版本起支持Online DDL,理论上执行DDL语句不会阻塞诸如INSERT、UPDATE、DELETE这类DML操作。

事后排查发现,该表有个持续了很久未提交的事务,正是该事务导致DDL语句执行时锁表。

复现

1、新建表

sql 复制代码
CREATE TABLE `lock_table` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `content` varchar(32) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2、新建一个会话1,开启事务执行以下命令后不要提交事务

sql 复制代码
begin;
select * from lock_table limit 1;

3、新建另一个会话2,执行DDL命令,发现DDL语句执行被阻塞

sql 复制代码
ALTER TABLE `lock_table` ADD INDEX content_index(content);

4、此时表被锁定,再新建一个会话3,执行查询语句发现该操作同样被阻塞住

sql 复制代码
select id from lock_table limit 1;

5、我们使用SHOW PROCESSLIST; 命令,发现Waiting for table metadata lock,就是元数据锁。

6、我们使用以下语句,也会发现metadata_locks

sql 复制代码
select * from performance_schema.metadata_locks ;

metadata_locks是5.7中被引入,记录了metadata lock的相关信息,包括持有对象、类型、状态等信息。但5.7默认设置是关闭的(8.0默认打开),需要通过下面命令打开设置:

sql 复制代码
UPDATE performance_schema.setup_instruments SET ENABLED = 'YES', TIMED = 'YES'WHERE NAME = 'wait/lock/metadata/sql/mdl';

如果要永久生效,需要在配置文件中加入如下内容:

sql 复制代码
[mysqld]performance-schema-instrument='wait/lock/metadata/sql/mdl=ON'

单纯查询这个表无法得出具体的阻塞关系,也无法得知什么语句造成的阻塞,这里要关联另外两个表performance_schema.thread和performance_schema.events_statements_history,thread表可以将线程id和show processlist中id关联,events_statements_history表可以得到事务的历史sql,关联得到一条监控元数据锁的sql语句。

关联后的完整sql如下:

sql 复制代码
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
 
*************************** 1. row ***************************
               locked_schema: report-vision
                locked_table: students
                 locked_type: Metadata Lock
      waiting_processlist_id: 13
                 waiting_age: 938
               waiting_query: alter table students add column col1 int
               waiting_state: Waiting for table metadata lock
     blocking_processlist_id: 12
                blocking_age: 947
              blocking_query: SELECT * from students where id=1
sql_kill_blocking_connection: KILL 12
1 row in set, 2 warnings (0.01 sec)

根据显示结果,processlist_id为12的线程阻塞了13的线程,我们需要kill 12即可解锁。

实际上,MySQL也提供了一个类似的视图来解决metadata lock问题,视图名称为sys.schema_table_lock_waits,但此视图查询结果有bug,不是很准确,建议大家还是参考上面sql。

7、我们使用语句查看一下当前事务,发现会话1长事务一直没有关闭,导致了元数据锁,导致后续所有的增删改查请求等待,数据库崩溃。

sql 复制代码
SELECT * FROM information_schema.INNODB_TRX;

DDL导致锁表的原因

To ensure transaction serializability, the server must not permit one session to perform a data definition language (DDL) statement on a table that is used in an uncompleted explicitly or implicitly started transaction in another session. The server achieves this by acquiring metadata locks on tables used within a transaction and deferring release of those locks until the transaction ends. A metadata lock on a table prevents changes to the table's structure. This locking approach has the implication that a table that is being used by a transaction within one session cannot be used in DDL statements by other sessions until the transaction ends.

mysql官方文档metadata-locking一节中指出,为了确保事务可序列化,mysql不允许一个会话对在另一会话中未完成的显式或隐式启动的事务中使用的表执行DDL语句。服务器通过获取事务中使用的表上的元数据锁并将这些锁的释放推迟到事务结束之前来实现。表上的元数据锁可防止更改表的结构。这种锁定方法的含义是,一个会话中事务正在使用的表在事务结束之前不能被其他会话在DDL语句中使用。

mysql对申请MDL锁的操作会形成一个队列,队列中写锁获取优先级高于读锁。一旦出现写锁等待,不但当前操作会被阻塞,同时还会阻塞后续该表的所有操作。由上可知当事务一旦申请到MDL锁后,直到事务执行完才会将锁释放,当长事物或未提交的事务未提交完成时,执行DDL语句会等待MDL排他锁而阻塞,继而阻塞该表的后续其他操作。

MySQL Online DDL的改进与应用

  • prepare阶段:尝试获取MDL排他锁,禁止其他线程读写;
  • ddl执行阶段:降级成MDL共享锁,允许其他线程读取;
  • commit阶段:升级成MDL排他锁,禁止其他线程读写;
  • finish阶段:释放MDL锁;
    1、3、4如果没有锁冲突,执行时间非常短。第2步占用了DDL绝大部分时间,这期间这个表可以正常读写数据,是因此称为"online "。
    如果第3步升级为MDL写锁的时候,这个表的MDL锁有其他事务占着,那么这个事务会阻塞,等到可以拿到MDL写锁,而且如果不幸一直拿不到,最后锁超时了,就只好回滚这个DDL操作。

所以,DDL语句只有才开始和结束的时候,才会禁止读和写,在语句执行的时候是可以进行读的。

如何安全地给表添加字段、添加索引

1、生产环境的任何大表或频繁操作的小表,ddl都要非常慎重,最好在业务低峰期执行。

2、设计上要尽可能避免大事务,大事务不仅仅会带来各种锁问题,还会引起复制延迟/回滚空间爆满等各类问题。

3、设置参数 lock_wait_timeout 为较小值,使被阻塞端主动停止。

4、增强监控告警,及时发现 MDL 锁。

5、或许这样操作也是一种好办法:按新结构创建新表 -> 将旧表数据迁移至新表 -> 重命名两个表(三步都通过编写sql语句完成,比手动操作快,第二步的数据迁移操作视情况而定)。过程中最好在没人用的时候操作

6、操作ddl之前,先用以下语句查一下有没有长事务:

sql 复制代码
SELECT * FROM information_schema.INNODB_TRX;

7、多副本(主从、集群)下可以做热更新。

8、MariaDB 已经合并了 AliSQL 的这个功能,所以这两个开源分支目前都支持 DDL NOWAIT/WAIT n 这个语法:

sql 复制代码
ALTER TABLE tbl_name NOWAIT add column ...
ALTER TABLE tbl_name WAIT N add column ... 

参考资料

blog.csdn.net/Leon_Jinhai...
一步步搞懂MySQL元数据锁(MDL)

《MYSQL实战》

相关推荐
Estar.Lee6 小时前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
2401_857610038 小时前
SpringBoot社团管理:安全与维护
spring boot·后端·安全
凌冰_8 小时前
IDEA2023 SpringBoot整合MyBatis(三)
spring boot·后端·mybatis
码农飞飞8 小时前
深入理解Rust的模式匹配
开发语言·后端·rust·模式匹配·解构·结构体和枚举
一个小坑货8 小时前
Rust 的简介
开发语言·后端·rust
monkey_meng9 小时前
【遵守孤儿规则的External trait pattern】
开发语言·后端·rust
Estar.Lee9 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
新知图书10 小时前
Rust编程与项目实战-模块std::thread(之一)
开发语言·后端·rust
盛夏绽放10 小时前
Node.js 和 Socket.IO 实现实时通信
前端·后端·websocket·node.js
Ares-Wang10 小时前
Asp.net Core Hosted Service(托管服务) Timer (定时任务)
后端·asp.net