天天看点

[MySQL Bug]DDL操作导致备库复制中断

————————————————-

在mysql5.1及之前的版本中,如果有未提交的事务trx,当执行drop/rename/alter table rename操作时,不会被其他事务阻塞住。这会导致如下问题(mysql bug#989)

master:

未提交的事务,但sql已经完成(binlog也准备好了),表schema发生更改,在commit的时候不会被察觉到。

slave:

在binlog里是以事务提交顺序记录的,ddl隐式提交,因此在备库先执行ddl,后执行事务trx,由于trx作用的表已经发生了改变,因此trx会执行失败。

在ddl时的主库dml压力越大,这个问题触发的可能性就越高

一个简单的例子:

session1,set autocommit=0,对表b执行一条dml

root@xxx 11:48:28>set autocommit = 0;

query ok, 0 rows affected (0.00 sec)

root@xxx 11:48:35>insert into b values (null,4);

query ok, 1 row affected (0.00 sec)

session2,执行rename table a to tmp_b

root@xxx 11:48:23>rename table b to tmp_b;

query ok, 0 rows affected (0.01 sec)

session1:commit;

root@xxx 11:49:00>show binlog events;

+——————+—–+—-———+———–+——–—–+—————————————+

| log_name         | pos | event_type  | server_id | end_log_pos | info                                  |

| mysql-bin.000001 |   4 | format_desc |        12 |         106 | server ver: 5.1.48-log, binlog ver: 4 |

| mysql-bin.000001 | 106 | query       |        12 |         191 | use `xxx`; rename table b to tmp_b    |

| mysql-bin.000001 | 191 | query       |        12 |         258 | begin                                 |

| mysql-bin.000001 | 258 | table_map   |        12 |         298 | table_id: 195 (xxx.b)                 |

| mysql-bin.000001 | 298 | write_rows  |        12 |         336 | table_id: 195 flags: stmt_end_f       |

| mysql-bin.000001 | 336 | xid         |        12 |         363 | commit /* xid=737 */                  |

显然当这样的binlog同步到备库的话,必然会导致复制中断。

在5.1里可以通过如下步骤绕过bug:

>set autocommit = 0;

>lock tables t1 write;

> drop table t1 / alter table t1 rename to t2

rename table t1 to t2这样的ddl不适用于上述方法。

在5.5引入了mdl(meta data lock)锁来解决在这个问题,至于5.1,官方已经明确回复不会fix,太伤感了。。。

我们来看看mdl是如何解决这个问题的,还是以rename为例吧

—————————————————————————————

我们知道,在5.5之前,当事务的一条语句执行完后,就会释放占有的数据字典锁,mdl的作用就是延迟这种锁的释放。

对于非事务表或者运行在autocommit=1时没有什么影响。

mdl的相关api和定义都在文件mdl.cc和mdl.h中

mdl锁的类型包括:

1. mdl_intention_exclusive

意向排他mdl锁,只用于范围锁(mdl_scoped_lock)。拥有该锁的可以获得单个对象升级的排他锁

该类型锁与其他ix锁相容,但和范围x和s锁不相容。

2.mdl_shared

共享mdl锁

当只对对象元数据感兴趣而无需访问对象的数据时使用

3.mdl_shared_high_prio

更高优先级的共享mdl锁

更高优先级意味着和其他共享锁不一样,它可以忽略等待的排他锁请求。主要用于只需要访问metadata而非数据时。例如,填充一个i_s表时。

4.mdl_shared_read

共享mdl读锁当试图从表中读取数据时加上该锁。持有该锁时,可以读取表的元数据和表的数据

5.mdl_shared_write

共享mdl写锁,当试图修改表的数据时使用

持有该锁时可以读取表的元数据和表中的数据

主要适用于insert/delete/update,select…for update。不会作用于lock table write或者ddl。

6.mdl_shared_no_write

可升级的共享mdl锁,会阻塞所有的dml,但不阻塞读,可以升级为x mdl锁

和snrw,sw不相容

在alter table的第一阶段(表之间拷贝数据),允许并发的对表做select,但不允许更新

7.mdl_shared_no_read_write

可升级的mdl锁,允许其他连接获取metadata信息,但不允许访问数据。

该锁会阻塞所有试图读/写表的数据,但允许information_schema及show操作

可以升级为x mdl锁

用于lock tables write这样的语句。除了s和sh锁外,不与其他的锁相容。

8.mdl_exclusive

持有该锁时,可以修改表的metadata和数据,当持有该锁时,不能持有其他类型的任何mdl锁。用于create/drop/rename语句以及其他ddl语句的某些部分。

有三种类型的mdl锁持久化,在不同的时候释放:

enum enum_mdl_duration {

mdl_statement:在sql完成或事务结束时释放

mdl_transaction:在事务完成时释放

mdl_explicit:显式的加锁,在事务或sql完成后依旧持续,需要显式的调用  mdl_context::release_lock()来释放锁。

}

一个mdl锁的lock key由以下几个部分组成:

<mdl_namespace>+<database name>+<table name>

mdl_namespace是枚举类型,用于区分对象的类型

另外几个类:

############################################

mdl_wait_for_graph_visitor:an abstract class for inspection of a connected subgraph of the wait-for graph.

mdl_wait_for_subgraph:abstract class representing an edge in the waiters graph to be traversed by deadlock detection algorithm.

class mdl_ticket : public mdl_wait_for_subgraph

mdl_ticket是mdl子系统的私有成员。

在同一个对象上的多个共享锁由一个ticket来表示,其他类型的锁不是这样。

有两组mdl_ticket成员

—可外部访问的(externally accessible)

—线程私有(context private)

mdl_savepoint:用于事务中存在savepoint时,作回滚用。

mdl_wait:定义了等待锁的方式

mdl_context:context of the owner of metadata locks. i.e. each server connection has such a context.

主要函数都在文件mdl.cc中定义。

################################################

举一个简单的例子来阐述mdl锁吧,还是以rename为例

系统启动时,会在init_server_components里mdl_init

初始化mdl相关变量,例如m_locks hash结构体,用于存储所有的mdl锁。

创建一个简单的表t1

create table t1 (a int auto_increment primary key , b int );

session 1:

 > set autocommit = 0;

>  insert into t1 values (null,2);

在打开表的时候会申请mdl锁,调用栈

mysql_execute_command

             mysql_insert

                     open_and_lock_tables

                             open_tables

                                  open_and_process_table

                                        open_table

                                                mdl_context::acquire_lock(sql_base.cc:2920)

                                                open_table_get_mdl_lock(sql_base.cc:2929)

                                                        mdl_context::acquire_lock

在本例中table->mdl_request.type为mdl_shared_write

session2: rename table t1 to t2;

mysql_rename_tables

      lock_table_names

session2需要请求表上的mdl_exclusive锁,这是强度最大的锁,与其他所有的mdl锁类型都不兼容,由于之前的insert操作的session持有mdl_shared_write锁,因此这里需要等待

lock_table_names          ——-搜集需要的锁

            ->mdl_context::acquire_locks

                     ->mdl_context::acquire_lock  —等待

>commit;

     ->mdl_context::release_transactional_locks  释放持有的mdl锁

mdl子系统的代码很复杂,以上也只是一个非常简单的例子,从如下的pdf中,你可以获得更详细的关于mdl如何设计的信息。

<a href="http://forge.mysql.com/w/images/0/0a/mdl.pdf">http://forge.mysql.com/w/images/0/0a/mdl.pdf</a>

以下两个连接是mdl的worklog

<a href="http://forge.mysql.com/worklog/task.php?id=3726">http://forge.mysql.com/worklog/task.php?id=3726</a>

<a href="http://forge.mysql.com/worklog/task.php?id=4284">http://forge.mysql.com/worklog/task.php?id=4284</a>