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

MariaDB源代码结构概览

2022-05-237.7k 阅读

MariaDB 简介

MariaDB 是一款基于 MySQL 的开源关系型数据库管理系统,由 MySQL 的原开发者主导开发。它在保持与 MySQL 高度兼容的同时,引入了许多新的特性和性能优化,广泛应用于各种 Web 应用和数据存储场景。深入了解 MariaDB 的源代码结构,有助于开发者更好地理解数据库的内部机制,进行定制开发、性能优化和问题排查。

源代码获取与目录结构

获取 MariaDB 源代码通常可通过官方的版本控制系统(如 Git)进行克隆。其目录结构组织严谨,各个目录承担不同功能,下面介绍主要目录:

  1. sql 目录:这是 MariaDB 核心功能的实现所在,涵盖了 SQL 解析、查询优化、执行等关键部分。如 sql/parser.yy 文件负责 SQL 语法规则定义,通过 Yacc 工具生成解析器。
  2. storage 目录:存储引擎相关代码。不同存储引擎(如 InnoDB、MyISAM 等)的实现分别在各自子目录下。例如,InnoDB 引擎代码在 storage/innobase 目录,其中包含了 InnoDB 的缓冲池管理、事务处理等关键模块。
  3. include 目录:存放头文件,定义了数据库中各种数据结构、函数原型等。这些头文件被其他源文件广泛引用,以实现模块化编程和代码复用。如 include/mysql_com.h 定义了与 MySQL 协议相关的结构和常量。
  4. mysys 目录:包含一些底层的系统相关函数,如文件操作、内存管理等。这些函数为上层数据库功能提供了基础支持,具有较高的可移植性。例如 mysys/my_file.c 实现了跨平台的文件操作函数。
  5. client 目录:客户端相关代码,用于与数据库服务器进行交互。包含了各种语言接口的实现以及连接管理等功能。例如,C 语言客户端库代码就位于此目录下。

SQL 解析与语法分析

词法分析

MariaDB 使用 Flex 工具生成词法分析器,词法分析器负责将输入的 SQL 语句分割成一个个单词(token)。词法规则定义在 sql/lex.l 文件中。例如,以下是一个简单的词法规则示例:

%{
#include "my_global.h"
#include "my_sys.h"
#include "sql_parse.h"
%}

digit       [0-9]
letter      [a-zA-Z_]
identifier  {letter}({letter}|{digit})*

%%
"SELECT"    { return(SELECT); }
"FROM"      { return(FROM); }
{identifier} { yylval.str_value= strdup(yytext); return(ID); }
{digit}+    { yylval.num_value= atoi(yytext); return(NUMBER); }
.           { return(*yytext); }

上述代码定义了一些基本的词法规则,如识别 SELECTFROM 关键字,以及标识符和数字。词法分析器将输入的 SQL 文本按这些规则转化为对应的 token,传递给语法分析器。

语法分析

语法分析基于 Yacc 工具,在 sql/parser.yy 文件中定义语法规则。语法分析器根据词法分析器提供的 token 构建出一棵语法树。例如,简单的查询语句语法规则如下:

statement_list: statement
               | statement_list statement
               ;

statement: query_stmt
         | SET_SYSTEM_VAR
         | other_statement
         ;

query_stmt: SELECT_LEX
          ;

SELECT_LEX: SELECT select_expr_list INTO_CLAUSE_opt FROM clause_opt WHERE_clause_opt GROUP BY_clause_opt HAVING_clause_opt ORDER BY_clause_opt LIMIT_clause_opt
          ;

上述规则描述了 SQL 语句的层次结构,从语句列表开始,包含各种类型的语句,查询语句又由多个子句组成。语法分析器根据这些规则对 token 流进行匹配,构建语法树。语法树节点的具体操作由语义动作定义,例如在 SELECT_LEX 规则中,语义动作可能涉及初始化查询相关的数据结构等操作。

查询优化

逻辑优化

逻辑优化阶段主要对语法树进行转换和优化,以生成更高效的执行计划。这一过程涉及多个优化策略,如谓词下推(将过滤条件尽可能下推到存储引擎层)、消除冗余子查询等。

以谓词下推为例,在 sql/opt_prepare.cc 文件中,JOIN::optimize() 函数会对查询的语法树进行遍历。当发现有过滤条件(谓词)时,会尝试将其下推到合适的表连接操作之前。例如:

void JOIN::optimize()
{
    // 遍历语法树节点
    for (Item *item : where_items)
    {
        // 检查谓词是否可以下推
        if (item->is_simple_predicate() && can_push_down_predicate(item))
        {
            // 将谓词下推到合适的表连接操作
            push_down_predicate(item);
        }
    }
}

上述代码片段展示了简单的谓词下推逻辑,通过遍历 where_items 中的谓词,判断是否满足下推条件,若满足则执行下推操作。

物理优化

物理优化主要考虑选择合适的存储访问路径和连接算法。MariaDB 会根据统计信息(如表的行数、索引分布等)来评估不同执行计划的成本。

sql/optimizer.cc 文件中,JOIN::optimize_quick() 函数负责选择最优的物理执行计划。它会遍历所有可能的连接顺序和访问方法,并计算每个计划的成本。例如:

double JOIN::optimize_quick()
{
    double best_cost = HUGE_VAL;
    JOIN_PLAN best_plan;

    // 遍历所有可能的连接顺序
    for (const auto &order : generate_join_orders())
    {
        // 为每种连接顺序生成物理执行计划
        JOIN_PLAN plan = generate_physical_plan(order);

        // 计算计划成本
        double cost = calculate_cost(plan);

        // 更新最优计划
        if (cost < best_cost)
        {
            best_cost = cost;
            best_plan = plan;
        }
    }

    // 设置最优计划
    set_best_plan(best_plan);
    return best_cost;
}

上述代码通过生成不同的连接顺序,为每种顺序生成物理执行计划并计算成本,最终选择成本最低的计划作为最优执行计划。

存储引擎

InnoDB 存储引擎

InnoDB 是 MariaDB 中常用的事务性存储引擎。其源代码位于 storage/innobase 目录。

  1. 缓冲池管理:缓冲池是 InnoDB 性能的关键组件,用于缓存磁盘数据页。在 storage/innobase/buf/buf0buf.cc 文件中,buf_pool_create() 函数负责创建缓冲池。
buf_pool_t* buf_pool_create(ulint size, ulint n_pages_per_chunk)
{
    buf_pool_t *pool = static_cast<buf_pool_t*>(ut_malloc(sizeof(buf_pool_t)));
    // 初始化缓冲池参数
    pool->size = size;
    pool->n_pages_per_chunk = n_pages_per_chunk;
    // 创建缓冲池的内存结构
    pool->chunks = static_cast<buf_chunk_t**>(ut_malloc(n_chunks * sizeof(buf_chunk_t*)));
    for (ulint i = 0; i < n_chunks; i++)
    {
        pool->chunks[i] = buf_chunk_create(pool, i);
    }
    return pool;
}

上述代码展示了缓冲池创建的基本过程,包括分配内存、初始化参数以及创建缓冲池的各个数据块。

  1. 事务处理:InnoDB 的事务处理机制确保数据的一致性和完整性。在 storage/innobase/trx/trx0trx.cc 文件中,trx_start() 函数用于启动一个新事务。
trx_t* trx_start(ulint isolation_level)
{
    trx_t *trx = static_cast<trx_t*>(trx_sys_alloc(sizeof(trx_t)));
    // 初始化事务相关参数
    trx->isolation_level = isolation_level;
    trx->state = TRX_STATE_ACTIVE;
    // 记录事务开始日志
    trx_log_start(trx);
    return trx;
}

该函数为新事务分配内存,初始化事务状态和隔离级别,并记录事务开始日志。

MyISAM 存储引擎

MyISAM 是一种非事务性存储引擎,以其快速的读取性能而闻名。其源代码位于 storage/myisam 目录。

  1. 表结构存储:MyISAM 表结构信息存储在 .frm 文件中。在 storage/myisam/myisamdef.h 文件中定义了表结构相关的数据结构。
typedef struct st_myisam_table_def
{
    char name[FN_REFLEN];
    uint32_t flags;
    uint16_t field_count;
    MI_FIELD *fields;
    // 其他表结构相关信息
} MYISAM_TABLE_DEF;

上述结构定义了 MyISAM 表的名称、标志、字段数量以及字段数组等关键信息。

  1. 数据读取storage/myisam/myisamread.cc 文件中的 myisam_rrnd() 函数用于按行读取 MyISAM 表数据。
int myisam_rrnd(MYISAM_SHARE *share, my_off_t offset, uchar *buf, size_t length)
{
    // 定位到指定偏移位置
    if (my_seek(share->file, offset, MY_SEEK_SET) != 0)
    {
        return -1;
    }
    // 读取数据
    if (my_read(share->file, buf, length) != length)
    {
        return -1;
    }
    return 0;
}

该函数通过文件偏移定位到指定行位置,然后读取相应长度的数据。

日志系统

重做日志(Redolog)

重做日志用于崩溃恢复,确保在系统崩溃后能将未完成的事务回滚,已提交的事务重新应用。在 MariaDB 中,重做日志相关代码分布在多个文件,如 sql/log.ccstorage/innobase/log/ 目录下。

在 InnoDB 存储引擎中,storage/innobase/log/log0log.cc 文件中的 log_write_up_to() 函数负责将日志记录写入重做日志文件。

void log_write_up_to(log_t *log, lsn_t end_lsn)
{
    // 获取日志缓冲区
    byte *buf = log->buf;
    lsn_t start_lsn = log->buf_free;

    // 计算要写入的日志长度
    size_t len = (end_lsn - start_lsn) & (log->buf_size - 1);

    // 将日志缓冲区数据写入文件
    my_write(log->file, buf + start_lsn, len);

    // 更新日志缓冲区位置
    log->buf_free = end_lsn;
}

上述代码从日志缓冲区获取要写入的日志数据,计算长度后写入重做日志文件,并更新日志缓冲区的空闲位置。

二进制日志(Binlog)

二进制日志用于主从复制和数据备份。在 sql/binlog.cc 文件中,write_binlog() 函数负责将事件写入二进制日志。

int write_binlog(THD *thd, const char *buf, size_t len)
{
    // 获取二进制日志文件对象
    Binlog_event *event = create_binlog_event(thd, len);

    // 填充事件数据
    memcpy(event->data, buf, len);

    // 写入二进制日志文件
    if (my_write(thd->bin_log->file, event->data, event->header_len + len) != event->header_len + len)
    {
        return -1;
    }

    // 释放事件对象
    free_binlog_event(event);
    return 0;
}

该函数创建二进制日志事件,填充事件数据后写入二进制日志文件,并在写入完成后释放事件对象。

并发控制

锁机制

MariaDB 使用多种锁机制来实现并发控制。以表级锁为例,在 sql/lock.cc 文件中,mysql_lock_tables() 函数用于获取表级锁。

int mysql_lock_tables(THD *thd, TABLE_LIST *tables, enum_lock_type type)
{
    // 遍历表列表
    for (TABLE_LIST *table = tables; table; table = table->next_local)
    {
        // 获取表对象
        TABLE *tbl = table->table;

        // 根据锁类型获取锁
        if (type == LOCK_READ)
        {
            if (mysql_rlock_table(tbl) != 0)
            {
                return -1;
            }
        }
        else if (type == LOCK_WRITE)
        {
            if (mysql_wlock_table(tbl) != 0)
            {
                return -1;
            }
        }
    }
    return 0;
}

上述代码遍历要锁定的表列表,根据锁类型调用相应的函数获取表级读锁或写锁。

事务隔离级别实现

事务隔离级别通过锁机制和多版本并发控制(MVCC)实现。在 InnoDB 存储引擎中,不同隔离级别下的读操作处理方式不同。以可重复读(Repeatable Read)隔离级别为例,在 storage/innobase/row/row0mysql.cc 文件中,row_search_mvcc() 函数实现了该隔离级别下的读操作。

int row_search_mvcc(
    const buf_page_t *page,
    const dtuple_t *tuple,
    ulint prebuilt0,
    row_prebuilt_t *prebuilt,
    ulint flags)
{
    // 获取当前事务 ID
    trx_id_t trx_id = prebuilt->trx->id;

    // 遍历页面中的记录
    for (ulint i = 0; i < page->n_heap; i++)
    {
        const rec_t *rec = page_get_rec(page, i);

        // 获取记录的事务 ID
        trx_id_t rec_trx_id = rec_get_trx_id(rec);

        // 根据事务 ID 和隔离级别判断是否可见
        if (rec_trx_id <= trx_id)
        {
            // 记录可见,进行处理
            return 0;
        }
    }
    return -1;
}

上述代码在可重复读隔离级别下,通过比较记录的事务 ID 和当前事务 ID 来判断记录是否可见,从而实现事务隔离。

总结 MariaDB 源代码结构的重要性

通过对 MariaDB 源代码结构的深入剖析,从 SQL 解析、查询优化、存储引擎、日志系统到并发控制等各个方面,我们了解到其内部复杂而精妙的设计。这不仅有助于开发者更好地理解数据库的工作原理,还为定制开发、性能优化以及问题排查提供了坚实的基础。掌握 MariaDB 源代码结构,能够让开发者在面对实际应用中的各种需求和挑战时,更加游刃有余地进行操作和优化。无论是对现有功能的改进,还是开发新的特性,深入研究源代码结构都是不可或缺的一步。