MySQL存储引擎内部之:`InnoDB`的`Online DDL`:其在`inplace`和`instant`操作中的底层实现。

好的,下面是一篇关于MySQL InnoDB存储引擎中Online DDL的文章,以讲座模式呈现,包含代码示例,逻辑严谨,并使用正常人类的语言表述。

MySQL InnoDB Online DDL:Inplace与Instant操作的底层实现

各位同学,今天我们来深入探讨MySQL InnoDB存储引擎中的Online DDL (Data Definition Language) 功能。Online DDL允许我们在执行表结构变更操作时,尽可能地减少对业务的影响,避免长时间的锁表。我们将重点分析 inplaceinstant 两种操作的底层实现机制。

1. Online DDL 的概念与分类

传统的DDL操作,例如 ALTER TABLE,通常需要对整个表进行锁定,这会导致业务停顿,尤其是在大型表上。Online DDL旨在解决这个问题,它允许在一定程度上并发地执行DDL操作和DML (Data Manipulation Language) 操作。

InnoDB 的 Online DDL 主要分为以下几类:

  • COPY: 最原始的方式,创建一个包含新结构的临时表,将数据从原始表复制到临时表,然后重命名临时表。期间需要长时间锁定原表。
  • INPLACE: 在原始表上直接进行操作,无需创建临时表。但仍然可能需要短暂的锁表或共享锁。
  • INSTANT: 最快的DDL方式,只需要修改元数据,几乎不需要锁定表。

选择哪种DDL操作取决于具体的变更类型以及MySQL的版本。

2. INPLACE DDL 的底层实现

INPLACE DDL 操作是相对于 COPY 操作的改进,它避免了创建临时表和大量的数据拷贝,从而减少了锁定时间和资源消耗。

2.1 基本原理

INPLACE DDL 的基本原理是,在原始表上直接进行结构变更,同时允许并发的DML操作。为了保证数据一致性,InnoDB 会采用以下一些机制:

  • 元数据锁 (Metadata Lock, MDL): DDL操作会获取MDL锁,防止其他DDL或DML操作同时修改表的结构。
  • 共享锁 (Shared Lock): 在某些阶段,DDL操作可能需要获取共享锁,允许并发的读操作,但阻止写操作。
  • 行格式转换 (Row Format Conversion): 如果DDL操作涉及到行格式的改变(例如,添加新的非空列),InnoDB 会在后台逐渐将旧的行格式转换为新的行格式。
  • 日志记录: DDL 操作会被记录到 Redo Log 中,以便在崩溃恢复时能够正确地应用变更。

2.2 具体操作流程

以添加一个允许为NULL的列为例,说明 INPLACE DDL 的流程:

  1. Prepare阶段:

    • 获取 exclusive MDL 锁,阻止其他 DDL 操作。
    • 分配新的数据字典对象,包含新的列信息。
    • 在存储引擎层面,标记表需要进行 INPLACE 操作。
  2. Commit DDL Log 阶段:

    • 将 DDL 操作记录到 Redo Log 中。
    • 释放 exclusive MDL 锁,降级为 shared MDL 锁,允许并发的读操作。
  3. 在线数据变更阶段 (Optional):

    • 如果需要转换行格式 (例如,添加非空列),则在此阶段进行。InnoDB 会在后台逐渐地将旧的行格式转换为新的行格式。对于新增的允许为NULL的列,这个阶段通常是跳过的
  4. Finish 阶段:

    • 升级为 exclusive MDL 锁。
    • 更新数据字典,使新的表结构生效。
    • 释放 MDL 锁。

2.3 代码示例 (伪代码)

虽然我们无法直接看到InnoDB的内部实现代码,但可以用伪代码来模拟 INPLACE DDL 的关键步骤:

// 假设这是一个 InnoDB DDL 操作的简化版本
class OnlineDDL {
public:
    bool execute_inplace_add_column(Table& table, Column& new_column) {
        // 1. Prepare 阶段
        if (!acquire_exclusive_mdl_lock(table)) {
            return false; // 获取锁失败
        }
        DataDictionary new_dd = table.data_dictionary().clone(); // 克隆数据字典
        new_dd.add_column(new_column); // 添加新列
        table.set_new_data_dictionary(new_dd); // 设置新的数据字典对象

        // 2. Commit DDL Log 阶段
        if (!write_ddl_log(table, "add column " + new_column.name())) {
            release_exclusive_mdl_lock(table);
            return false; // 写入日志失败
        }
        release_exclusive_mdl_lock(table);
        if (!acquire_shared_mdl_lock(table)) {
            return false;
        }

        // 3. 在线数据变更阶段 (如果需要)
        //  这里添加非空列的时候需要行格式转换,如果是NULL列,可以跳过
        // if (new_column.is_not_null()) {
        //     row_format_conversion(table, new_column);
        // }

        // 4. Finish 阶段
        release_shared_mdl_lock(table);
        if (!acquire_exclusive_mdl_lock(table)) {
            return false;
        }

        table.set_data_dictionary(new_dd); // 最终更新数据字典
        release_exclusive_mdl_lock(table);

        return true;
    }

private:
    bool acquire_exclusive_mdl_lock(Table& table) {
        // 获取独占 MDL 锁
        // ...
        return true;
    }

    bool acquire_shared_mdl_lock(Table& table) {
        // 获取共享 MDL 锁
        // ...
        return true;
    }

    void release_exclusive_mdl_lock(Table& table) {
        // 释放独占 MDL 锁
        // ...
    }
    void release_shared_mdl_lock(Table& table) {
        // 释放共享 MDL 锁
        // ...
    }

    bool write_ddl_log(Table& table, const std::string& log_message) {
        // 写入 DDL 日志
        // ...
        return true;
    }

    void row_format_conversion(Table& table, Column& new_column) {
        // 后台行格式转换
        // ...
    }
};

注意: 这只是一个简化的伪代码,实际的InnoDB实现要复杂得多。

2.4 INPLACE DDL 的适用场景

INPLACE DDL 适用于以下场景:

  • 添加允许为NULL的列。
  • 更改列的数据类型 (在某些情况下)。
  • 重命名列。
  • 修改列的顺序。
  • 添加或删除索引 (在某些情况下)。

2.5 INPLACE DDL 的限制

INPLACE DDL 仍然存在一些限制:

  • 某些操作仍然需要长时间的锁表,例如,更改列的数据类型,如果涉及大量的数据转换。
  • 如果并发的DML操作与DDL操作冲突,可能会导致DDL操作失败。
  • 在DDL执行期间,数据库的性能可能会受到影响。
  • 添加非空列,需要在线构建数据,时间较长。

3. INSTANT DDL 的底层实现

INSTANT DDL 是 InnoDB Online DDL 的一个重要进步,它几乎不需要锁定表,只需要修改元数据即可完成DDL操作。

3.1 基本原理

INSTANT DDL 的基本原理是,利用 InnoDB 的内部数据结构和机制,将一些 DDL 操作转化为元数据的修改,而不需要实际地修改数据文件。

3.2 具体操作流程

以删除一个索引为例,说明 INSTANT DDL 的流程:

  1. Prepare 阶段:

    • 获取 exclusive MDL 锁。
    • 验证操作的合法性 (例如,索引是否存在)。
  2. Commit DDL Log 阶段:

    • 将 DDL 操作记录到 Redo Log 中。
    • 修改数据字典,将索引标记为 "不可用"。
    • 释放 exclusive MDL 锁。

InnoDB 会在后台异步地清理 "不可用" 的索引数据。

3.3 代码示例 (伪代码)

// 假设这是一个 InnoDB DDL 操作的简化版本
class OnlineDDL {
public:
    bool execute_instant_drop_index(Table& table, const std::string& index_name) {
        // 1. Prepare 阶段
        if (!acquire_exclusive_mdl_lock(table)) {
            return false; // 获取锁失败
        }

        Index* index = table.find_index(index_name);
        if (index == nullptr) {
            release_exclusive_mdl_lock(table);
            return false; // 索引不存在
        }

        // 2. Commit DDL Log 阶段
        if (!write_ddl_log(table, "drop index " + index_name)) {
            release_exclusive_mdl_lock(table);
            return false; // 写入日志失败
        }

        index->set_state(Index::State::UNUSABLE); // 将索引标记为 "不可用"
        release_exclusive_mdl_lock(table);

        // 后台异步清理索引数据
        // async_drop_index_data(index);

        return true;
    }

private:
    bool acquire_exclusive_mdl_lock(Table& table) {
        // 获取独占 MDL 锁
        // ...
        return true;
    }

    void release_exclusive_mdl_lock(Table& table) {
        // 释放独占 MDL 锁
        // ...
    }

    bool write_ddl_log(Table& table, const std::string& log_message) {
        // 写入 DDL 日志
        // ...
        return true;
    }

    void async_drop_index_data(Index* index) {
        // 异步清理索引数据
        // ...
    }
};

注意: 这只是一个简化的伪代码,实际的InnoDB实现要复杂得多。

3.4 INSTANT DDL 的适用场景

INSTANT DDL 适用于以下场景 (取决于 MySQL 版本):

  • 删除二级索引 (MySQL 8.0 及更高版本)。
  • 重命名表 (MySQL 8.0 及更高版本)。
  • 交换分区 (MySQL 5.7 及更高版本)。

3.5 INSTANT DDL 的优势

INSTANT DDL 的主要优势是:

  • 速度极快,几乎不需要锁定表。
  • 对业务的影响最小。

3.6 INSTANT DDL的限制

INSTANT DDL 也有一些限制:

  • 只适用于特定的DDL操作。
  • 删除索引后,InnoDB 会在后台异步地清理索引数据,这可能会占用一定的资源。
  • 不支持回滚。

4. 选择合适的 Online DDL 操作

选择哪种 Online DDL 操作取决于具体的DDL操作类型、MySQL版本以及业务需求。一般来说,我们应该尽可能地选择 INSTANT DDL,如果不支持,则选择 INPLACE DDL,尽量避免使用 COPY DDL。

5. Online DDL 的元数据锁(MDL)

MDL是Online DDL中非常关键的一部分,它用于保护数据库对象的元数据,防止并发的DDL和DML操作导致数据不一致。

5.1 MDL锁的类型

MDL锁主要分为以下几种类型:

  • MDL_SHARED: 共享锁,允许并发的读操作。
  • MDL_SHARED_HIGH_PRIO: 共享锁,具有更高的优先级。
  • MDL_SHARED_READ: 共享读锁,用于SELECT语句。
  • MDL_SHARED_WRITE: 共享写锁,用于UPDATE/DELETE语句。
  • MDL_EXCLUSIVE: 排他锁,用于DDL操作。
  • MDL_EXCLUSIVE_HIGH_PRIO: 排他锁,具有更高的优先级。

5.2 MDL锁的兼容性

不同的MDL锁之间存在兼容性关系,只有当锁之间兼容时,才能并发执行。例如,共享锁与共享锁兼容,但排他锁与任何锁都不兼容。

5.3 MDL锁的获取与释放

MDL锁由MySQL服务器自动管理,无需手动获取或释放。当执行一个SQL语句时,服务器会自动获取所需的MDL锁,并在语句执行完成后释放锁。

5.4 MDL锁的问题

MDL锁也可能导致一些问题,例如:

  • MDL锁等待: 如果一个SQL语句需要获取MDL锁,但该锁被其他语句占用,则该语句会进入等待状态。
  • MDL锁死锁: 如果多个SQL语句互相等待对方释放MDL锁,则可能导致死锁。

为了避免MDL锁的问题,我们应该尽量减少DDL操作的执行时间,并避免长时间运行的事务。

6. 不同版本的 Online DDL 支持

MySQL 的不同版本对 Online DDL 的支持程度不同。

MySQL 版本 主要 Online DDL 改进

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注