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语句执行成功。

你可能感兴趣的:(MySQL实现DDL NOWAIT)