检测版本兼容性

    FOR UPGRADE选项用于检测表与当前版本MySQL的兼容性。它用于检测在创建表之后,是否在数据类型或者索引上发生了一些不兼容的修改操作。如果检测到一些不兼容的操作,它会在表上执行完整的检测过程,这需要较长的检测时间。不兼容性可能在数据类型的存储格式发生变化或者它的排序顺序发生变化时发生,比如在MySQL 5.0.3和5.0.5两个版本间DECIMAL类型存储结构的变化,在MySQL 4.1和5.0两个版本间TEXT列索引顺序的变化。

    检测数据一致性

    CHECK TABLE还提供了一些其它检查选项,这些选项信息被传递到存储引擎层,用于检测数据的一致性:

    1. /* 类型: 含义 */
    2. QUICK: 不扫描记录去检查索引结构的正确性,适用于InnoDB/MyISAM
    3. FAST: 只检查哪些没有被正常关闭的表,仅适用于MyISAM
    4. CHANGED: 检查那些在没有被正常关闭或上一次检查后被修改的表,仅适用于MyISAM
    5. MEDIUM: 扫描记录验证那些删除链接的正确性,同时验证checksum的正确性,仅适用于MyISAM
    6. EXTENDED: 扫描所有的记录,确保整张表数据100%的正确性,需要较长的执行时间。仅适用于MyISAM

    如果没有指定QUICK,MEDIUM或者EXTENED,在MyISAM中默认的检查类型是MEDIUM。这些检测选项也可以组合使用,例如CHECK TABLE test_table FAST QUICK,在表上执行一个快速的检查去检测它是否被正常关闭。但在InnoDB中,它只有QUICK和非QUICK两种类型。本文以InnoDB为代表,下面分析CHECK TABLE在InnoDB中的注意事项。

    CHECK TABLE在InnoDB中的注意事项

    如果CHECK TABLE遇到损坏的页面,MySQL实例将退出以防止错误的传播(Bug #10132)。如果数据损坏发生在二级索引中,但表数据依然是可读的,运行CHECK TABLE仍将导致MySQL实例停止。

    如果CHECK TABLE在主键索引中遇到错误的DB_TRX_ID或DB_ROLL_PTR项,CHECK TABLE将导致InnoDB访问到一个错误的undo log日志记录,导致MVCC相关服务崩溃。

    CHECK TABLE检查索引页结构,然后检查每个条目,但它不检查指向主键记录的键指针或遵循BLOB指针的指针。

    当一个InnoDB表存储在自己的.ibd文件中时,.ibd文件的前3页包含的是头部元数据,而不是表或索引数据。CHECK TABLE语句不检测这部分数据的不一致性。要验证innodb.ibd文件的全部内容,请使用innochecksum命令。

    在大型表上运行CHECK TABLE时,可能会在执行CHECK TABLE期间阻塞其他线程。为了避免超时,CHECK TABLE操作的信号量等待阈值(600秒)将延长2小时(7200秒)。如果InnoDB检测到信号量等待240秒或更长时间,它将开始向错误日志打印监控信息。如果锁请求超出信号量等待阈值,InnoDB将中止进程。

    从MySQL 8.0.14开始,InnoDB支持并行访问主键索引,这有效提高了CHECK TABLE操作的性能。InnoDB在CHECK TABLE期间读取主键索引两次,第二次读取可以并行执行。要并行访问主键索引,必须将innodb_parallel_read_threads变量设置为大于1的值(默认值为4)。并行访问主键索引的线程数由innodb_parallel_read_threads设置或要扫描的索引子树数确定,以较小的值为准。

    本文以MySQL 8.0.14代码为例,分析CHECK TABLE的实现。

    CHECK TABLE的代码实现

    1. /* 检查索引结构的一致性 */
    2. bool btr_validate_index(dict_index_t *index, const trx_t *trx, bool lockout)
    3. {
    4. ...
    5. bool ok = true;
    6. mtr_t mtr;
    7. mtr_start(&mtr);
    8. /* 持有index的sx或者x锁 */
    9. if (lockout) mtr_x_lock(dict_index_get_lock(index), &mtr);
    10. else mtr_x_lock(dict_index_get_lock(index), &mtr)
    11. /* 获取索引的根节点 */
    12. page_t *root = btr_root_get(index, &mtr);
    13. /* 验证每一层树结构的正确性 */
    14. for (ulint i = 0; i <= n; ++i) {
    15. if (!btr_validate_level(index, trx, n - i, lockout)) {
    16. ok = false;
    17. break;
    18. }
    19. }
    20. mtr_commit(&mtr);
    21. return ok;
    22. }
    1. /* 针对COUNT(*)或者CHECK TABLE扫描索引。如果是CHECK TABLE,检查所有记录的顺序 */
    2. dberr_t row_scan_index_for_mysql(row_prebuilt_t *prebuilt, const dict_index_t *index, size_t n_threads, bool check_keys, ulint *n_rows)
    3. {
    4. ...
    5. /* 进行一系列检查,满足条件后执行多线程CHECK TABLE */
    6. if (prebuilt->select_lock_type == LOCK_NONE && index->is_clustered() &&
    7. (check_keys || prebuilt->trx->mysql_n_tables_locked == 0) &&
    8. !prebuilt->ins_sel_stmt && n_threads > 1) {
    9. /* 开启事务,设置视图 */
    10. trx_start_if_not_started_xa(prebuilt->trx, false);
    11. trx_assign_read_view(prebuilt->trx);
    12. Key_reader reader(prebuilt->table, trx, index, prebuilt, n_threads);
    13. /* 进入多线程检查函数 */
    14. if (!check_keys) {
    15. return (parallel_select_count_star(reader, n_rows));
    16. }
    17. return (parallel_check_table(reader, n_rows));
    18. }
    19. /* 以下单线程处理部分和5.6源码类似 */
    20. ...
    21. /* 定位到index的起始cursor */
    22. row_search_for_mysql(buf, PAGE_CUR_G, prebuilt, 0, 0);
    23. loop:
    24. /* 比较rec的大小,确保有序的状态是正确的 */
    25. ...
    26. next_rec:
    27. /* 获取下一个rec */
    28. ret = row_search_for_mysql(buf, PAGE_CUR_G, prebuilt, 0, ROW_SEL_NEXT);
    29. /* 循环执行 */
    30. goto loop;
    31. }

    Key_reader在持有index的SX锁情况下,针对所有子树创建cursor,然后释放index的SX锁。子树的扫描过程为:

    1. 从根结点开始读取每一层最左边的树节点;

    2. 如果这一层能划分的子树数量少于指定线程数,就继续往下搜索。划分的方法包括按照page或者key值划分,分别在Phy_reader/Key_reader中实现;

    我们以parallel_check_table函数为例,分析多线程代码实现,具体原理读者可以查询 。