背景
DDL与DCL一直是工作中高频的手工操作,但两者之间的相互影响导致的BUG,笔者还是第一次见。请看下图:
![7d01183247ada29f13eb2f2c7b157aa6.png](https://i-blog.csdnimg.cn/blog_migrate/efff918a5c68def987d485ff051fa0b6.jpeg)
说明:在一个完全为空的MySQL实例上,如上红色框框中的SQL语句,第一句为创建用户,第二句为针对某表sakila.actor授权,第三句为创建表sakila.actor。细心的同学很快会发现,如果把该Binlog放到从库去执行,肯定会失败,因为授权语句先执行,但是要授权的表还不存在。排除sql_mode之类的原因,主从的参数一摸一样,怎么会这样?
初步分析
根据笔者前面文章关于提交的分析,自然的反应就是create table语句与grant privileges语句乱序了。为了验证笔者的猜测,在mysqld.trace文件中搜索该线程的debug_sync信号同步点,找到了提交前的信号同步点create_table_like_before_binlog,源码位置如下所示,
![4c5b4558991e18db1b59f89364a2e168.png](https://i-blog.csdnimg.cn/blog_migrate/57aa9ccc7f4c1c024984d67b70411b9d.jpeg)
结合mysqld.trace的日志与源码位置,可以确认该点位于语句提交前。
测试过程如下:
该测试通过使用MySQL官方测试工具debug_sync完成线程同步,具体可查看源码中大量的官方测试案例。
// MySQL config: debug
// MySQL config: debug_sync_timeout = 120
// Session 1
mysql> set debug_sync = 'reset';
Query OK, 0 rows affected (0.00 sec)
mysql> set debug_sync='create_table_like_before_binlog WAIT_FOR go';
Query OK, 0 rows affected (0.00 sec)
mysql> create table if not exists private.drop_if_exists_10 like private.drop_if_exists_1;
// ... Hangs.
// Session 2
mysql> show processlistG
...
*************************** 3. row ***************************
Id: 7
User: root
Host: localhost
db: NULL
Command: Query
Time: 3
State: debug sync point: create_table_like_before_binlog
Info: create table if not exists private.drop_if_exists_10 like private.drop_if_exists_1
3 rows in set (0.00 sec)
mysql> grant select on private.drop_if_exists_10 to 'acl_user'@'localhost';
Query OK, 0 rows affected, 1 warning (0.01 sec)
// ... Grant successfully.
如上所示,线程1卡在提交前,而线程却授权成功!到此,BUG已确认属实。进一步,将对源码剖析,分析原因。
源码剖析
- grant语句源码剖析
为了分析授权过程,找到函数mysql_table_grant设置断点,寻找过程本来不再陈述。有兴趣的同学可以根据grant语句的报错eng "Table '%-.192s.%-.192s' doesn't exist",找到相关的函数mysql_table_grant。调试到如下步骤,
![d31cd7020ec1334dffc4a023d4ebd773.png](https://i-blog.csdnimg.cn/blog_migrate/af3a894449d6759319df1f0c82f81486.png)
如上所示,从上往下,第一个红色框框为授权失败的逻辑流程,它直接跳出了授权逻辑,返回了错误。下面的展示窗口,展示了内部把库名和表名变成绝对路径下库表名/data/mysql/3306/data/private/drop_if_exists_10.frm的过程。此处的逻辑很简单,通过C函数access检查文件是否存在,如果不存在,就报错退出,否则就继续下去,执行授权。
要验证此处逻辑,很简单,只要授权之前touch一个对应的frm文件就行。如下,
以下两条语句分别为创建文件前与创建文件后,创建文件命令为 touch /data/mysql/3306/data/private/drop_if_exists_12.frm
mysql> grant select on private.drop_if_exists_12 to 'acl_user'@'localhost';
ERROR 1146 (42S02): Table 'private.drop_if_exists_12' doesn't exist
mysql> grant select on private.drop_if_exists_12 to 'acl_user'@'localhost';
Query OK, 0 rows affected, 1 warning (2.80 sec)
- create语句源码剖析
为了分析create语句执行过程,可以从mysqld.trace日志该线程的日志中快速通过grep -i enter_stage以及grep debug_sync的方式快速熟悉流程,并找到关键函数create_table_impl设置断点进行调试。关键代码如下,
create_table_impl // 创建表主函数
>>if (check_engine(thd, db, table_name, create_info)) // 检查存储引擎是否支持该类型的表
>>prepare_error= mysql_prepare_create_table(thd, db, error_table_name, // 对字段进行检查并准备创建
>> create_info, alter_info,
>> internal_tmp_table,
>> &db_options, file,
>> key_info, key_count,
>> select_field_count);
>>THD_STAGE_INFO(thd, stage_creating_table); // 显示线程状态“creating table”
>>if (rea_create_table(thd, path, db, table_name, // 真正创建表
create_info, alter_info->create_list,
*key_count, *key_info, file, no_ha_table))
>>>>if (mysql_create_frm(thd, frm_name, db, table_name, create_info, // 创建.frm文件
create_fields, keys, key_info, file))
>>>>if (file->ha_create_handler_files(path, NULL, CHF_CREATE_FLAG, // 创建存储引擎层数据库描述符
create_info))
>>>>if (!no_ha_table && // 创建聚簇索引,即InnoDB层创建.ibd文件
ha_create_table(thd, path, db, table_name, create_info, 0))
经过以上流程的梳理,整理的乱序分析如下图所示,
![72db1e3c4f139f75b4feba925472ed93.png](https://i-blog.csdnimg.cn/blog_migrate/6a4b2f2e225df4f1ca1e9c8901f7571e.jpeg)
结论
DDL的非原子性导致了与DCL的乱序,而MySQL 8.0已经废弃了.frm文件,此BUG将不复存在。