快速,持续,稳定,傻瓜式
支持Mysql,Sqlserver数据同步

从头开始编写MySQL存储引擎

在线QQ客服:1922638

专业的SQL Server、MySQL数据库同步软件

描述如何编写MySQL存储引擎-一个负责持久化MySQL表数据的插件。

介绍

本文总结了我编写新的MySQL存储引擎(“ SE”)时学到的东西。MySQL的存储引擎是插件,负责将数据实际存储在磁盘上,并提供对数据的访问。SE通常通常作为键/值数据库实现,但不一定必须如此。Oracle的MySQL附带了各种SE,并且整个SE环境可能有些令人不知所措。默认值为InnoDB,一个键/值数据库库。其他SE可以读取和写入CSV文件(“ Tina ”),或专门为存档数据而编写的文件(“ archive ”)。MariaDB是一个分支,更具进步性,并包含用于访问Cassandra NoSQL数据库(Percona的的 TokuDB键/值库和xtradb,InnoDB的改进版本。

总体而言,MySQL附带13个SE,MariaDB大约有20个。这显然还不够。让我们再添加一个!

下载代码和第一印象

您可以从GitHub下载MySQL代码。我正在使用5.7.12版本。

git clone https://github.com/mysql/mysql-server.git
cd mysql-server
git checkout mysql-5.7-12

让我们简要看一下源代码结构。文件和目录的数量有点令人生畏,但其中大多数与我们无关。只有几个目录值得一看:

include           Stores global include files. Here you will find mysql.h, which has many important macros and declarations.
sql               Contains the actual sql related code: parser, query engine etc.
sql/field.h       The Field class describes a MySQL column
sql/item.h        The Item class is a node in the abstract syntax tree which is generated by the parser
sql/sql_*.cc      These files implement the actual SQL operations
sql/handler.h     The base class for a storage engine
storage           This directory has all the storage engines
storage/innobase  The InnoDB storage engine
storage/example   A storage engine example project

浏览完代码后,很明显,MySQL的代码库中的某些部分相对较旧。虽然大多数代码都是用C ++实现的,但实际上是一个“带有类的C”。不使用更高级的C ++功能,例如模板,异常或标准库。链接列表被实现为包装“ void *”对象(my_list.h)的结构。MySQL也有自己的字符串实现(string.c),而不是使用std :: string。也未使用RAII。相反,您会发现许多“ goto”指令,这些指令负责清理分配的资源。

函数可能非常长(超过1000行代码,带有许多嵌套的“ if”子句-mysql_prepare_create_table),并且某些类也很大。大多数方法存储在基类中,而不是派生类中,此外,这些类还存储许多状态。

MySQL使用几个内存分配器(memroot_allocator.h)。它们与STL不兼容。许多类会覆盖operator new和operator delete来使用自定义分配器。

该代码没有一致的编码风格。缩进方式有所不同,结构名称有时会大写(“ struct THD”),小写字母带有下划线(“ st_mem_root”),或者以大写字母开头,然后再带有下划线(“ Field_long”)。空格的使用也有所不同(foo = 3; foo = 3; foo = 3)。

我从未与在Oracle或MySQL AB工作的人交谈过,但是我们可以根据我们看到的代码尝试对他们的企业文化做出一些猜测。缺乏编码风格意味着每个工程师都可以选择他最熟悉的工程师。即使在同一个文件中,编码样式也有很大不同,这可能表明没有“代码所有权”,并且每个人都可以处理代码的每个部分。过去,我在类似的环境中使用过类似的代码,这是一个工作的好地方。

如果MySQL想要摆脱其技术负担并吸引新的开发人员,那么使用“带有类的C”,拒绝使用STL以及陈旧的冗长而复杂的功能将需要大量重构。

编译,安装,运行,调试

让我们继续安装。如果遵循上述步骤,则您已经将代码检出到“ mysql-server”目录中。在这里,我们将运行cmake,它将生成Makefile。我们将创建一个较慢的Debug版本,但可用于调试。

cd mysql-server
cmake -DCMAKE_BUILD_TYPE=Debug
make -j 5
sudo make install

您编译的MySQL文件现在安装在/ usr / local / mysql中。您可以在子目录中运行cmake,以具有用于Debug和Release的单独版本,并且可以选择其他安装目录。运行“ cmake –help”以获取选项列表。对于本文,我将尽可能简化所有内容。

现在,只有几件事要做,直到我们可以成功启动MySQL守护程序。我们需要创建一个新的数据目录(用于存储表)并对其进行初始化(请注意,您将必须在以下命令中更改路径名):

mkdir /home/chris/tmp/mysql-data # must be empty, or else...
cd /usr/local/mysql/bin
./mysqld --initialize --datadir=/home/chris/tmp/mysql-data

最后一条命令将生成一个root密码。写下来,稍后您将需要它。我已经使用空的root密码设置了开发环境-在测试时更加方便。您可以使用以下命令重置密码(将<PASSWORD>替换为上面生成的root密码):

# start the server
./mysqld --datadir=/home/chris/tmp/mysql-data

# in a separate terminal we can now change the password
./mysqladmin  password --user=root --password=<password>

现在我们可以创建一个新的数据库。

./mysqladmin create test --user=root --password

也可以启动客户端,然后您最终可以创建表,插入数据等。

./mysql --user=root test

引导新的存储引擎

存储引擎被实现为动态库(“ .so”文件),并且源存储在“ mysql-server / storage”目录中。如果您只是想玩转玩具,则可以修改现有的“示例” SE。我选择通过以下步骤创建自己的“ upscaledb”:

cd mysql-server/storage
# copy the "ha_example" directory to "ha_upscaledb"
cp -r ha_example ha_upscaledb
# rename the files
cd ha_upscaledb
mv ha_example.h ha_upscaledb.h
mv ha_example.cc ha_upscaledb.cc

最后一步,使用您喜欢的IDE(我使用vim)将“ example”替换为“ upscaledb”,将“ EXAMPLE”替换为“ UPSCALEDB”。不要忘记也更改CMakeLists.txt文件。然后转到mysql-server的根目录,再次运行“ cmake”和“ make”。现在已构建新的upscaledb存储引擎,文件名为mysql-server / storage / upscaledb / ha_upscaledb.so。

现在我们必须通知MySQL有关新的存储引擎。首先,我们创建一个从安装目录到新.so文件的符号链接。通过此链接,我们的服务器将始终使用.so文件的最新版本。每当我们进行更改时,我们只需编译存储引擎并重新启动MySQL服务器。

cd /usr/local/mysql/lib/plugin
sudo ln -s ~/prj/mysql-server/storage/upscaledb/ha_upscaledb.so ha_upscaledb.so

最后一步是更新MySQL的内部系统表。我们可以使用MySQL客户端来做到这一点。确保MySQL服务器仍在运行!

cd /usr/local/mysql/bin
./mysql --user=root test

mysql> INSTALL  PLUGIN upscaledb SONAME ‘ha_upscaledb.so’;

现在,您可以开始使用新的SE并尝试创建表(CREATE TABLE测试(值INTEGER)ENGINE = upscaledb;)。由于我们的SE只是一个没有任何实现的框架,因此会出现错误。在开始添加逻辑之前,我将向您展示如何调试MySQL服务器。以下命令在gdb中启动服务器,并让gdb捕获所有信号(即CTRL-C进入调试器):

cd /usr/local/mysql/bin
gdb --args ./mysqld --gdb --datadir=/home/chris/tmp/mysql-data

尝试在SE的“ create()”方法中设置断点,然后再次从上方运行CREATE TABLE命令!

增加功能

现在是时候填写Handler类的各种方法了。详细描述每种方法将需要太多时间。此外,其中一些我仍然不知道。但是,我将进行概述,并提供到实际实现的链接。

重要的是要意识到在同一张表上可以有多个Handler实例。因此,实际表数据需要存储在单独的对象(“共享”)中,然后处理程序获取共享。如果您的数据存储在纯文件中,则文件句柄将是Share的成员,而不是Handler的成员。

你可以看一下ExampleShare 这里和我UpscaledbShare 这里。您会看到UpscaledbShare存储了实际的数据库句柄以及有关数据库的其他元数据。

建立表格

每当创建表时,都会调用ExampleHandler :: create和UpscaledbHandler :: create。之后,MySQL将立即在该表上调用open()方法。因此,您的create()方法可以准备表,但实际上不必打开它。

UpscaledbHandler :: create()的实现很简单。它创建一个upscaledb Environment,然后为每个索引创建一个数据库。如果用户创建了没有主键的表,则将生成索引。数据库配置取决于列的实际类型和其他一些参数(即,它是否唯一)。

开张桌

每当打开表时,都会调用ExampleHandler :: open和UpscaledbHandler :: open。如上所述,对于同一张表,这种情况可能会(而且会)多次发生。因此,您必须将实际的表数据存储在“共享”中。

检索到Share对象的指针后,UpscaledbHandler :: open()方法检查upscaledb环境是否已打开。如果是,它将立即返回。如果不是,则继续打开文件,并将环境的句柄存储在“共享”中。

收盘台

该ExampleHandler ::密切和UpscaledbHandler ::接近方法都应该关闭表。如果表数据存储在共享中,则可以使用引用计数来确定何时销毁共享对象。在我的UpscaledbHandler中,我从不销毁共享。毕竟,迟早会再次需要共享。

插入行

每当您调用INSERT SQL语句时,都会调用处理程序的write_row()方法。它唯一的参数是新行,以字节数组序列化。该数组以与调用CREATE TABLE语句时指定的顺序不同的顺序存储实际的列。主键始终位于开头,之后是所有其他索引列,最后是未索引列。

该字节数组通常以(可选)位图开头,该位图描述当前列中的空值。它后面是固定长度的列或长度可变的列(TINYTEXT,MEDIUMTEXT,TEXT,LONGTEXT或相应的BLOB列之一)。可变长度列以存储列大小的一个或两个字节开始,然后是数据(可以将其存储在单独的存储块中;在这种情况下,字节数组包含指向实际数据的编码指针)。如果将行保留在文件中,则以更紧凑的格式“压缩”可变长度的行是有意义的,以便减少空间。

我的UpscaledbHandler缓存索引的Field对象以快速提取索引的列。然后,可以使用以下代码来提取索引列的键(“ index”是索引的数字ID;主索引始终为0)。

static inline ups_key_t
key_from_row(TABLE *table, const uchar *buf, int index)
{
  KEY_PART_INFO *key_part = table->key_info[index].key_part;
  uint16_t key_size = (uint16_t)key_part->length;
  uint32_t offset = key_part->offset;

  if (key_part->type == HA_KEYTYPE_VARTEXT1
          || key_part->type == HA_KEYTYPE_VARBINARY1) {
    key_size = buf[offset];
    offset += 1;
  }
  else if (key_part->type == HA_KEYTYPE_VARTEXT2
          || key_part->type == HA_KEYTYPE_VARBINARY2) {
    key_size = *(uint16_t *)&buf[offset];
    offset += 2;
  }

  ups_key_t key = ups_make_key((uchar *)buf + offset, key_size);
  return key;
}

提取索引键后,您便可以将行的数据存储在文件中。您将不得不处理以下三种情况:

  1. 用户未指定ANY索引或主键
  2. 用户指定了主键,但没有其他索引
  3. 用户指定了主键和其他索引

如果还不够复杂,那么请记住,索引可以是“虚拟的”,即它包含多个列(如以下语句所示)。

CREATE TABLE test (
    id         INT NOT NULL,
    last_name  CHAR(30) NOT NULL,
    first_name CHAR(30) NOT NULL,
    PRIMARY KEY (id),
    INDEX name (last_name, first_name)    -- creates a virtual index!
);

这是UpscaledbHandler :: write_row的实现方法的实现。

<h3deleteing rows =“”>DELETE SQL命令最终调用处理程序的delete_row()方法。您必须确保删除所有索引,而不仅仅是主要索引。但是,删除主键实际上非常简单,因为MySQL将使用数据库游标来定位它。这是UpscaledbHandler :: delete_row实现。

更新行

这是最复杂的-至少要快速进行。所述update_row()方法接收两个参数; 旧行的值和新行的值。天真的解决方案是为旧行调用delete_row(),然后为新行调用write_row()。这行得通,但实际上速度很慢,因为您最终将更新所有列,即使仅更新了一个列。仅更新那些已修改的列要快得多。

游标

对于许多任务,MySQL核心将只创建一个数据库游标,找到一个键(在主索引或辅助索引上),然后在处理实际数据时继续前进。您必须实现一些方法来支持游标。

  • index_init():为二级索引创建游标
  • index_end():可以关闭游标
  • index_read_map():将光标定位在一行上
  • index_next():将光标移至下一个键,检索行
  • index_prev():将光标移至上一个键,检索行
  • index_last():将光标移至最后一个键,检索行
  • index_first():将光标移至第一个键,检索行
  • index_next_same():将光标移动到当前键的下一个重复项
  • rnd_init():为主索引创建一个游标
  • rnd_end():关闭游标
  • rnd_next():将光标移至下一个键,检索行
  • rnd_pos():将光标移动到指定的行

其他方法

其他一些重要或有趣的方法值得一提。

named_table():重命名表(及其所有文件)。每当您的模式更改时,即因为您添加了列,都将使用此方法。MySQL将所有数据复制到临时表中,然后使用rename_table()方法将临时表重命名为原始表。

delete_table():删除表(及其所有文件)。

table_flags():返回一组描述您的处理程序功能的标志。其中许多文件没有得到很好的记录,许多文件是针对InnoDB的。我的猜测是只有InnoDB才能实现所有功能。

index_flags():返回一组描述您的处理程序功能的标志-即是否提供了index_prev()和index_next()方法等。

结论

编写自己的MySQL存储引擎听起来很复杂,但实际上并非如此。您不必实现我上面描述的所有方法。对于某些方法,MySQL将处理缺少的实现并提供自己的实现。对于其他用户,它只会简单地中止某些SQL命令并显示错误。ExampleHandler基本为空,不提供任何功能。但是,尽管如此,您仍然可以加载它,将断点放置在调试器中,然后开始逐段添加功能。如果您遇到困难,可以看看MySQL和MariaDB的现有SE。mysql-internals邮件列表上的开发人员也非常有帮助。

我想到了一些有趣的SE想法:

  • 仅附加数据库,将其数据保留在HDFS中;然后,用户可以使用Spark或Hadoop的Map / Reduce作业进一步处理数据
  • 基于std :: map或std :: multi_map的内存表
  • 由XML文件支持的SE

您还有什么其他想法?

相关推荐

咨询软件
 
QQ在线咨询
售前咨询热线
QQ1922638