背景
MySQL的DDL没有被设计成事务操作,因此DDL操作是无法回滚的(像PgSQL把DDL也设计成事务操作,DDL就可以在执行成功后被回滚操作取消)。这就会导致如果某个DDL语句内部被拆分为多个原子的DDL调用,那么这个DDL语句就不具备中途执行失败后回滚整个DDL语句的能力,也就是说,即使语句逻辑内的某个原子DDL调用失败了,也无法回滚已经完成的那些原子DDL调用。
问题描述
DROP DATABASE 就是一个例子,对于MySQL而言,DROP DATABASE 并非是一个原子DDL操作,因为它是一个个删除DB下的每张表,而 DROP TABLE 操作本身是会做预检查的,无法删除就会取消删表操作返回失败,所以 DROP TABLE 才能认为是原子的DDL调用。 这就会引起一个问题,如果一个DB中的某张表DROP失败了,实际上 DROP DATABASE 作为一个整体是执行失败的,但是DB中已经有一些表被删除了,因此Binlog中会记录成多个 DROP TABLE 操作,而不是一个 DROP DATABASE 语句。 如果被删除的表的表名都不长,还是会记录成一个删除多张表的 DROP TABLE 语句(DROP TABLE tbl1, tbl2, ...),但是如果表名总长度太长,MySQL会拆分为多个 DROP TABLE 语句来记录。 没有GTID的时候这似乎也不是什么大问题,但是引入GTID之后就有一个问题:每个语句只分配一个GTID。如果一个 DROP DATABASE 语句被拆分为多个 DROP TABLE 语句,Binlog中就会出现多个 DROP TABLE 事件共用一个GTID的情况!
举个例子:
这里因为 db2.t3 表引用了 db1.t1 的字段作为外键约束,所以当 db1 做 DROP DATABASE 删除到 t1 表时就报错了,但此时很多表已经被删除了。我们看Binlog中记录的内容:
3个 DROP TABLE 语句都是同一个GTID:340d95b8-a699-11e4-868d-a0d3c1f20ae4:61
这就导致备库复制报错:
解决方案
怎么解决这个问题呢?
1. 让MySQL支持DDL事务
2. 对DROP DATABASE操作进行预检查
第一种方案对MySQL改动太大了,完全不现实。因此我们采用了第二种方案,也间接实现了 DROP DATABASE 这个操作的原子性。 DROP DATABASE 之所以出现上面的状况,就是因为没有先检查表是否可以删除,而是走一步看一步,一个个删的时候才看能不能删除。我们对MySQL做了修正,对于DB中的每张表,在 DROP DATABASE 执行之前,都先预检查所有可能导致删除表失败的条件,如果一旦发现某张表会无法删除,就放弃整个 DROP DATABASE 操作,提示用户删除错误,让用户先自行解决问题后,再重新执行 DROP DATABASE。
例如上面例子中的情况,本来 DROP DATABASE 执行到有外键约束的表时会报错:
但此时其他表已经删除了,而我们修正以后,同样的操作会报一个Error和一个Warning,并且没有真的删任何表:
这里提示了用户有表存在问题无法删除,让用户先处理掉之后,再来执行 DROP DATABASE。此时库下面所有的表都还在,一定要预检查通过才会真的删除。