MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

MariaDB binlog事件格式详解

2022-04-294.5k 阅读

MariaDB binlog 简介

在 MariaDB 数据库中,二进制日志(binlog)起着至关重要的作用。它记录了数据库的更改操作,比如数据的插入、更新、删除以及数据库结构的修改等。这些日志主要用于数据备份、恢复以及主从复制。

MariaDB 的 binlog 是基于事件模型的,每个事件都包含了特定的数据库操作信息。通过解析这些事件,我们可以深入了解数据库的变更历史,这对于数据库的运维、故障排查以及数据恢复等工作都有着极大的帮助。

binlog 事件格式概述

MariaDB 的 binlog 事件具有特定的格式,每个事件都由以下几个主要部分组成:

  1. 事件头(Event Header):包含了事件的基本信息,例如事件类型、事件大小、时间戳等。
  2. 事件体(Event Body):这部分具体包含了该事件所代表的数据库操作的详细数据,例如插入的数据行、更新的字段等。
  3. 事件尾(Event Footer):通常用于标记事件的结束,可能包含一些校验和等信息(在某些事件类型中)。

事件头结构详解

事件头的结构相对固定,其格式如下:

struct binlog_event_header {
    uint32_t timestamp;        // 事件发生的时间戳
    uint16_t event_type;       // 事件类型,如 1 表示 START_EVENT_V3,2 表示 QUERY_EVENT 等
    uint32_t server_id;        // 生成该事件的服务器 ID
    uint32_t event_size;       // 整个事件(包括头、体和尾)的大小
    uint16_t flags;            // 事件的标志位,用于一些特殊设置
    // 不同事件类型可能会有额外的头字段
};
  • timestamp:以秒为单位记录事件发生的时间。这个时间戳对于分析数据库操作的顺序以及基于时间的恢复操作非常重要。
  • event_type:这是一个关键的字段,它决定了事件体的结构。不同的事件类型对应不同的数据库操作,例如 QUERY_EVENT 表示执行一条 SQL 查询,ROTATE_EVENT 表示日志文件的切换。
  • server_id:在主从复制环境中,这可以帮助识别事件是由哪台服务器生成的。如果有多台主服务器,通过这个 ID 可以区分不同主服务器产生的变更。
  • event_size:记录整个事件占用的字节数。在解析 binlog 时,通过这个字段可以准确地定位到下一个事件的起始位置。
  • flags:包含了一些事件相关的标志,例如是否是事务相关的事件等。不同的事件类型可能会使用不同的标志位来表示特定的特性。

常见事件类型及其事件体结构

  1. START_EVENT_V3
    • 事件类型值:1
    • 事件体结构
struct start_event_v3_body {
    char binlog_version[4];    // binlog 版本
    char mysql_version[50];    // MariaDB 版本
    uint32_t create_timestamp; // binlog 创建时间戳
    // 其他版本特定的字段
};

这个事件通常是 binlog 文件的第一个事件,用于标识 binlog 的版本以及数据库的版本信息,还有 binlog 创建的时间。 2. QUERY_EVENT - 事件类型值:2 - 事件体结构

struct query_event_body {
    uint32_t execution_time;    // 查询执行时间(以秒为单位)
    uint8_t error_code;        // 执行查询时的错误码,如果为 0 表示无错误
    char database[CHAR_MAX];   // 执行查询的数据库名称
    uint32_t sql_length;        // SQL 语句的长度
    char sql[sql_length];       // 实际的 SQL 语句
    // 其他一些辅助字段,如状态标志等
};

QUERY_EVENT 用于记录执行的 SQL 查询语句。execution_time 字段可以帮助我们分析查询的性能,error_code 则有助于排查查询执行过程中出现的问题。 3. ROTATE_EVENT - 事件类型值:3 - 事件体结构

struct rotate_event_body {
    char next_binlog_name[FN_REFLEN]; // 下一个 binlog 文件的名称
    uint64_t position;                // 下一个 binlog 文件的起始位置
};

当当前 binlog 文件达到一定大小或者根据配置切换日志文件时,就会生成 ROTATE_EVENT。它记录了下一个 binlog 文件的名称和起始位置,这对于连续读取 binlog 文件非常重要。 4. TABLE_MAP_EVENT - 事件类型值:19 - 事件体结构

struct table_map_event_body {
    uint64_t table_id;         // 表的唯一标识符
    char database[CHAR_MAX];   // 表所在的数据库名称
    char table[CHAR_MAX];      // 表名
    uint8_t column_count;      // 表的列数
    // 后续跟着列的元数据信息,如列类型、长度等
};

在记录数据变更事件(如 WRITE_ROWS_EVENTUPDATE_ROWS_EVENT 等)之前,通常会先有一个 TABLE_MAP_EVENT。它为后续的数据变更事件提供了表的相关信息,比如表的结构和列的定义,这样在解析数据变更事件时就能准确知道每个数据对应的列。 5. WRITE_ROWS_EVENT - 事件类型值:24 - 事件体结构

struct write_rows_event_body {
    uint64_t table_id;         // 关联的表 ID,与 TABLE_MAP_EVENT 中的 table_id 对应
    uint16_t flags;            // 标志位,例如是否包含唯一键等信息
    uint16_t number_of_rows;   // 插入的行数
    // 后续跟着每一行的数据,按照 TABLE_MAP_EVENT 中定义的列顺序排列
};

WRITE_ROWS_EVENT 用于记录插入数据的操作。通过 table_id 关联到 TABLE_MAP_EVENT,从而确定插入数据的表结构,number_of_rows 则明确了插入的行数。 6. UPDATE_ROWS_EVENT - 事件类型值:25 - 事件体结构

struct update_rows_event_body {
    uint64_t table_id;         // 关联的表 ID
    uint16_t flags;            // 标志位
    uint16_t number_of_rows;   // 更新的行数
    // 对于每一行,先记录旧数据,再记录新数据,按照 TABLE_MAP_EVENT 中定义的列顺序排列
};

该事件记录了数据更新操作。同样通过 table_id 关联表结构,number_of_rows 表示更新的行数,并且会按照列顺序记录每一行更新前后的数据。 7. DELETE_ROWS_EVENT - 事件类型值:26 - 事件体结构

struct delete_rows_event_body {
    uint64_t table_id;         // 关联的表 ID
    uint16_t flags;            // 标志位
    uint16_t number_of_rows;   // 删除的行数
    // 后续跟着每一行要删除的数据,按照 TABLE_MAP_EVENT 中定义的列顺序排列
};

DELETE_ROWS_EVENT 用于记录删除数据的操作。通过 table_id 明确要删除数据的表,number_of_rows 表明删除的行数。

解析 binlog 事件的代码示例(以 C++ 为例)

下面是一个简单的 C++ 代码示例,用于解析 MariaDB binlog 文件中的事件头部分。这个示例假设我们已经打开了一个 binlog 文件,并可以读取其中的数据。

#include <iostream>
#include <fstream>
#include <cstring>

struct binlog_event_header {
    uint32_t timestamp;
    uint16_t event_type;
    uint32_t server_id;
    uint32_t event_size;
    uint16_t flags;
};

int main() {
    std::ifstream binlog_file("your_binlog_file.bin", std::ios::binary);
    if (!binlog_file.is_open()) {
        std::cerr << "无法打开 binlog 文件" << std::endl;
        return 1;
    }

    binlog_event_header header;
    while (binlog_file.read(reinterpret_cast<char*>(&header), sizeof(binlog_event_header))) {
        std::cout << "时间戳: " << header.timestamp << std::endl;
        std::cout << "事件类型: " << header.event_type << std::endl;
        std::cout << "服务器 ID: " << header.server_id << std::endl;
        std::cout << "事件大小: " << header.event_size << std::endl;
        std::cout << "标志位: " << header.flags << std::endl;

        // 跳过事件体和事件尾,因为这里只解析事件头
        binlog_file.seekg(header.event_size - sizeof(binlog_event_header), std::ios::cur);
    }

    binlog_file.close();
    return 0;
}

在这个示例中,我们首先定义了 binlog_event_header 结构体来匹配 binlog 事件头的格式。然后通过 std::ifstream 打开 binlog 文件,并循环读取事件头。每次读取到一个事件头后,输出其中的关键信息。最后,根据事件头中的 event_size 跳过事件体和事件尾,继续读取下一个事件头。

如果要完整解析事件体,需要根据不同的 event_type 进一步编写相应的解析代码。例如,对于 QUERY_EVENT,可以在读取完事件头后,继续读取事件体中的 execution_timeerror_codedatabasesql_lengthsql 等字段:

// 在上面代码的基础上,添加对 QUERY_EVENT 的解析
if (header.event_type == 2) {
    struct query_event_body {
        uint32_t execution_time;
        uint8_t error_code;
        char database[256];
        uint32_t sql_length;
        char sql[1];
    };

    query_event_body query_body;
    binlog_file.read(reinterpret_cast<char*>(&query_body.execution_time), sizeof(uint32_t));
    binlog_file.read(reinterpret_cast<char*>(&query_body.error_code), sizeof(uint8_t));
    binlog_file.read(query_body.database, 256);
    binlog_file.read(reinterpret_cast<char*>(&query_body.sql_length), sizeof(uint32_t));
    char* sql = new char[query_body.sql_length + 1];
    binlog_file.read(sql, query_body.sql_length);
    sql[query_body.sql_length] = '\0';

    std::cout << "执行时间: " << query_body.execution_time << std::endl;
    std::cout << "错误码: " << static_cast<int>(query_body.error_code) << std::endl;
    std::cout << "数据库: " << query_body.database << std::endl;
    std::cout << "SQL 语句: " << sql << std::endl;

    delete[] sql;
}

在这个扩展的代码中,当检测到事件类型为 QUERY_EVENT(值为 2)时,我们定义了 query_event_body 结构体,并按照其结构依次读取各个字段。注意,sql 字段的长度是动态的,所以我们在读取前先获取其长度 sql_length,然后动态分配内存来存储 SQL 语句。

事件尾的作用与解析

事件尾在不同的事件类型中作用有所不同。在一些事件中,事件尾可能包含校验和信息,用于验证事件在传输或存储过程中是否发生了错误。例如,在一些基于网络传输 binlog 事件的场景下(如主从复制),校验和可以帮助从服务器确认接收到的事件是否完整和正确。

对于事件尾的解析,同样依赖于事件类型。一些简单的事件可能没有复杂的事件尾结构,而像 XID_EVENT(用于事务提交)等事件,其事件尾可能包含事务相关的一些标识信息。在实际解析中,需要根据具体的事件类型文档来准确解析事件尾的内容。

基于 binlog 事件格式的应用场景

  1. 数据恢复:通过重放 binlog 中的事件,可以将数据库恢复到某个特定的时间点。这在数据库出现故障或者误操作后非常有用。例如,如果不小心删除了一张表,可以通过从备份点开始重放 binlog 事件,重新创建表并插入被删除的数据。
  2. 主从复制:主服务器将 binlog 事件发送给从服务器,从服务器通过解析这些事件来同步数据。准确理解 binlog 事件格式对于确保主从复制的正确性和高效性至关重要。例如,从服务器需要正确解析 TABLE_MAP_EVENT 来了解表结构的变化,以便正确应用后续的数据变更事件。
  3. 数据库审计:分析 binlog 事件可以了解数据库的操作历史,包括谁在什么时候执行了什么操作。这对于合规性检查以及安全审计非常有帮助。例如,可以通过解析 QUERY_EVENT 中的 SQL 语句,检查是否有未经授权的敏感数据查询操作。

binlog 事件格式的演进与兼容性

随着 MariaDB 的版本演进,binlog 事件格式也可能会发生变化。新的版本可能会引入新的事件类型,或者对现有事件类型的结构进行扩展。例如,为了支持新的数据库特性,可能会添加新的标志位或者字段到事件头或事件体中。

在进行数据库升级或者跨版本交互(如主从复制中主从服务器版本不同)时,需要注意 binlog 事件格式的兼容性。通常,较新版本的 MariaDB 会尽量保持对旧版本 binlog 事件格式的向后兼容性,以确保数据备份、恢复以及主从复制等功能的正常运行。但是,在一些特殊情况下,可能需要手动进行一些配置或者转换操作,以确保跨版本的兼容性。例如,如果主服务器升级到了一个新版本,而从服务器仍然是旧版本,可能需要检查新版本引入的新事件类型是否会影响从服务器的解析。如果有影响,可能需要采取一些措施,如暂时不使用新特性,或者对从服务器进行适当的升级或配置调整。

总结 binlog 事件格式的要点

  1. 事件头:是每个 binlog 事件的起始部分,包含了时间戳、事件类型、服务器 ID、事件大小和标志位等关键信息,这些信息对于识别和定位事件以及了解事件的基本属性非常重要。
  2. 事件体:依据事件类型的不同而具有不同的结构,准确解析事件体是理解数据库操作细节的关键,例如 QUERY_EVENT 的 SQL 语句、WRITE_ROWS_EVENT 的插入数据等。
  3. 事件尾:虽然在不同事件类型中作用各异,但对于保证事件的完整性和正确性(如通过校验和)以及特定事务标识等方面有着重要意义。
  4. 应用场景:数据恢复、主从复制和数据库审计等场景都高度依赖对 binlog 事件格式的准确理解和解析。
  5. 版本兼容性:随着 MariaDB 版本的发展,要关注 binlog 事件格式的变化以及跨版本的兼容性问题,确保数据库相关功能的稳定运行。

通过深入了解 MariaDB binlog 事件格式,数据库管理员和开发人员可以更好地进行数据库的维护、优化以及故障排查等工作,充分发挥 MariaDB 数据库的强大功能。