背景

    在MySQL中,表是和操作系统中的文件对应的,而文件名在有的操作系统下是区分大小写的(比如linux),有的是不区分大小写(比如Windows),表名与文件名的大小写对应关系,MySQL 是通过lower_case_table_names 这个变量来控制的。

    这个变量的有效取值是0,1,2,按照官方文档 的解释:

    0表示,表在文件系统存储的时候,对应的文件名是按建表时指定的大小写存的,MySQL 内部对表名的比较也是区分大小写的; 1表示,表在文件系统存储的时候,对应的文件名都小写的,MySQL 内部对表名的比较是转成小写的,即不区分大小写; 2表示,表在文件系统存储的时候,对应的文件名是按建表时指定的大小写存的,但是 MySQL 内部对表名的比较是转成小写的,即不区分大小写。

    0适用于区分大小写的系统,1都适用,2适用于不区分大小写的系统。

    如果在开始使用MySQL选定了一个合适的值后,就不要改变,不然的话在之后使用中就会出现问题。

    问题描述

    这里给出一个在使用过程中改变 lower_case_table_names 导致 drop database 失败的案例。因为lower_case_table_names是个只读变量,只能在启动时指定参数设置值,或者 gdb 挂上去直接改内存。

    首先在启动 mysqld 的时候,指定 lower_case_table_names = 0,我们执行这样的语句:

    1. create database db1;
    2. use db1;
    3. create table t1(a int) engine = InnoDB;
    4. create table t2(a int) engine = MyISAM;
    5. create table T3(a int) engine = InnoDB;
    6. create table T4(a int) engine = MyISAM;

    查看对应数据库目录下的表文件:

    1. $ls db1
    2. db.opt t1.frm t1.ibd t2.frm t2.MYD t2.MYI T3.frm T3.ibd T4.frm T4.MYD T4.MYI

    然后重启mysqld,指定 lower_case_table_names =1,执行删除db1

    1. mysql> drop database db1;
    2. ERROR 1010 (HY000): Error dropping database (can't rmdir './db1', errno: 39)

    可以看到删库语句执行失败,我们再看下数据库目录下的表文件

    1. $ls a
    2. T3.frm T4.frm T4.MYD T4.MYI

    可以看到,大写的 T3 和 T4 表没有被删掉,为什么呢?

    问题分析

    mysqld 在执行 drop database 操作的时候,是调用 mysql_rm_db 这个函数,在删除时先把db下的所有表都删掉,然后再把db删掉。为了找出对应db下的所有表,mysqld 是通过遍历数据库目录下的文件来做的,具体是用 find_db_tables_and_rm_known_files 这个函数,遍历数据库目录下的所有文件,然后构造出要 drop 的table列表,然而在构造删除列表过程中,会有这样一个判断:

    1. if (lower_case_table_names)
    2. table_list->table_name_length= my_casedn_str(files_charset_info,
    3. table_list->table_name);

    意思就是如果lower_case_table_names非0的话,就把 table_name 转成小写的,T3 和 T4 就被转成 t3 和 t4,这样生成的 table_list 中的对应的表是 t1,t2,t3,t4。之后拿着这样的 table_list 通过 mysql_rm_table_no_locks 一个个删表,这样就只把t1,t2 给删了,t3和t4不存在,并且删表时的逻辑是带有 if exists 的,所以也不会报错。

    在list表都删除完后,调用rm_dir_w_symlink来删除db目录,此时db1目录下还有 T3 T4 对应的文件,这个函数会调用系统的 rmdir 函数,而当目录非空的时候,rmdir是执行失败的。

    所以我们看到最终的错误提示 Error dropping database (can’t rmdir ‘./db1’, errno: 39)

    建议

    上面的问题是改变 lower_case_table_names 导致 drop database 失败,其实还有许多其它的因为lower_case_table_names值改变导致的问题,比如主备库本来这个值本来是一致的,如果只改主库的值的话,就会导致备库复制中断,报找不到表的问题,或者本来是不区分大小写的,应用里的写的SQL语句有大写表名,也有小写表名,之后改成区分大小写,就会导致应用出错。

    所以建议是:

    • 不要轻易的改变lower_case_table_names的值,如果真要改的话,要先检查下已有的表是否有大小写的问题,保证目前的表名和要改的模式是一致的,比如从区分大小写改为不区分大小写,那就不应该有大写表存在,如果有的话,要先把大写表rename成小写的,如果本来有共存同名的大写表和小写表,就要想办法去掉一个。
    • 应用不要依赖于 mysql 的表名转换机制,应用里的sql语句应该和表名一致,在不区分大小写的时候,应用里对同一个表的使用,不要既有大写表名,也有小写表名。