MySQL实现DDL NOWAIT

author:sufei

版本:mysql 8.0.18

说明:本文主要记录DDL NOWAIT功能,实现以及测试。


一、生产MDL锁问题

MySQL数据库为了保证表结构的完整性和一致性,服务层提供了一套MDL锁机制,对表的所有访问都需要获得相应级别的MDL锁,从而保护表的元数据。但是在生产中会出现如下情况:

会话1:(忘记提交)
mysql> begin;
Query OK, 0 rows affected (0.01 sec)

mysql> select * from test.t1;
+----+-------+
| id | name  |
+----+-------+
|  1 | tom   |
|  2 | herry |
+----+-------+
2 rows in set (0.00 sec)
会话2:
mysql> alter table test.t1 add age int default 0;
阻塞
从而造成其他会话,无论是读还是写,都无法操作该表。如下会话3:
mysql> select * from test.t1;
阻塞
而且此时通过show processlist查看仅仅看到大量的Waiting for table metadata lock,并且真正阻塞的会话1在show processlist并没有显示任何信息,如下:
mysql> show processlist;
+----+-----------------+-----------+------+---------+------+---------------------------------+-------------------------------------------+
| Id | User            | Host      | db   | Command | Time | State                           | Info                                      |
+----+-----------------+-----------+------+---------+------+---------------------------------+-------------------------------------------+
|  4 | event_scheduler | localhost | NULL | Daemon  |  997 | Waiting on empty queue          | NULL                                      |
|  7 | root            | localhost | test | Sleep   |  974 |                                 | NULL                                      |
|  8 | root            | localhost | test | Query   |    5 | Waiting for table metadata lock | alter table test.t1 add age int default 0 |
| 11 | root            | localhost | NULL | Query   |    0 | starting                        | show processlist                          |
| 13 | root            | localhost | NULL | Query   |    2 | Waiting for table metadata lock | select * from test.t1                     |
+----+-----------------+-----------+------+---------+------+---------------------------------+-------------------------------------------+
5 rows in set (0.00 sec)
## 真正阻塞的线程为Id=7,此时其状态为sleep

总结如下:

  • 会话1在事务中对t1表进行了查询,那么其将获取t1表的MDL_SHARED_READ级别MDL锁。而锁一直持续到commit结束,才能释放。

  • 会话2如果此时对t1表进行DDL操作,需要获取t1表的MDL_EXCLUSIVE级别MDL锁,因为MDL_SHARED_READ与MDL_EXCLUSIVE不相容,所以会话2被阻塞,然后进入等待队列。

  • 同样,如果此时会话3对t1表做查询,因为等待队列中有MDL_EXCLUSIVE级别MDL锁请求,所以会话3也将也被阻塞,进入等待队列。

    这种情况是目前比较常见的情况,因为数据库开发者无法确保每一位数据库使用者及时进行事务提交。从而操作整个数据库因为MDL锁导致业务不可用,同时在排查问题时(如果相关的performance没有开启,通常生产并未开启),由于show processlist中显示的为大量Waiting for table metadata lock,往往真正造成阻塞的线程从信息中并不显目。

二、NOWAIT思路

MySQL 8.0.1在select语法中给出了NOWAIT特性([官方博客](https://mysqlserverteam.com/mysql-8-0-1-using-skip-locked-and-nowait-to-handle-hot-rows/)),也就是我们可以在查询时如果需要等待行锁,则立即退出报错。这种特性其实也可以用在MDL锁,这样我们如果在进行DDL语句时,通过NOWAIT检测一下是否有相关元数据锁等待,如果有则立即退出,这样运维人员即可及时判断出能否进行ddl操作,而不至于在执行ddl时,造成大量业务中断。

DDL NOWAIT虽然并没有解决真正DDL过程中的阻塞问题,但避免了因为DDL操作没有获取锁,进而导致业务其他查询/更新语句阻塞的问题。下面简要说明其实现:

我们要实现的目标是支撑如下语法:

alter table test.t1 nowait add age int default 0;

当存在nowait关键字,则在执行ddl语句时,遇到mdl锁不进行等待

  • 首先在sql_yacc.yy语法文件添加相关语法支持,大致如下:
alter_table_stmt:
          ALTER TABLE_SYM table_ident NOWAIT_SYM opt_alter_table_actions
          {
            $$= NEW_PTN PT_alter_table_stmt(
                  YYMEM_ROOT,
                  $3,
                  true,
                  $5.actions,
                  $5.flags.algo.get_or_default(),
                  $5.flags.lock.get_or_default(),
                  $5.flags.validation.get_or_default());
          }
        | ALTER TABLE_SYM table_ident opt_alter_table_actions
          {
            $$= NEW_PTN PT_alter_table_stmt(
                  YYMEM_ROOT,
                  $3,
                  false,
                  $4.actions,
                  $4.flags.algo.get_or_default(),
                  $4.flags.lock.get_or_default(),
                  $4.flags.validation.get_or_default());
          }
        | ALTER TABLE_SYM table_ident standalone_alter_table_action
          {
            $$= NEW_PTN PT_alter_table_standalone_stmt(
                  YYMEM_ROOT,
                  $3,
                  $4.action,
                  $4.flags.algo.get_or_default(),
                  $4.flags.lock.get_or_default(),
                  $4.flags.validation.get_or_default());
          }
        ;
  • 在各ddl语法树根类中添加是否支持nowait标记字段,如在PT_alter_table_stmt类中定义成员变量
const bool not_wait_mdl_action;
  • 在进行ddl操作的实际获取DML锁时,如果not_wait_mdl_action为ture则不进行等待,如在mysql_alter_table函数中获取元数据锁时,调整acquire_lock函数的timeout参数为0,如下
/* 
 mdl_request为要求获得锁的类型
 lock_wait_timeout
*/
bool MDL_context::acquire_lock(MDL_request *mdl_request,
                               Timeout_type lock_wait_timeout)
通过上述源码修改,基本实现如下功能:

当DDL语句指定了nowait,如果获取DML失败,客户端将得到报错信息:

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

三、实验

步骤 会话1 会话2
1
nowait_1

开启事务,但忘记提交
2
nowait_2

如果未添加nowait关键字,则阻塞;
此时ddl添加了nowait属性,直接报错。
3
nowait_3

事务提交
4
nowait_4

可以立即获得DML锁,ddl语句执行成功。
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • MDL锁 对表的增删改查,都需要MDL锁,无所不在 MDL读锁之间不互斥,但MDL读写锁互斥 #举个栗子 假设t是...
    飞翔的Tallgeese阅读 1,089评论 1 0
  • author:sufei源码版本:8.0.18 说明:如果对于online ddl还不了解可以参考 一、Onlin...
    真之棒2016阅读 789评论 0 1
  • 概述 在早期的 MySQL 版本中,DDL 操作(如创建索引等)通常都需要对数据表加锁,操作过程中 DML 操作都...
    mylxsw阅读 655评论 1 8
  • 1.1 什么是锁? 锁是一种用于保证在并发场景下每个事务仍能以一致性的方式读取和修改数据的方式,当一个事务对某一条...
    nieniemin阅读 273评论 0 0
  • 数据库锁设计的初衷是处理并发问题。作为多用户共享的资源,当出现并发访问的时候,数据库需要合理地控制资源的访问规则。...
    yywfy的昵称阅读 556评论 0 0