(接 从草稿开始写一个 MySQL 存储引擎(一))
增加功能
现在是时候来填充我们的 Handler 类中的各种方法了。详细的描述每个方法将会花上很多时间。同时,其中的一部分我暂时也不太清楚。但是我会给出一个概览,并且提供具体实现的链接。
意识到同一个表中运行了多种 Handler 的实例是一个非常重要的过程。实际的表数据因此需要存储在不同的对象中(一个 'share'),然后 Handler 将会需要这个 Share。如果你的数据存储在一个原始文件中,那么这个文件的 Handle 将会成为 Share 的一个成员,而不是 Handler 中的成员。
你能在 这儿 看到一个样例的Share,以及我的 UpscaledbShare 。可以看到 UpscaledbShare 存储了实际的数据 handles,以及额外关于数据库的元数据。
创建表
ExampleHandler::create 和 UpscaledbHandler::create 每当创建表时都会被调用。之后,MySQL 将会在表上调用 open() 方法。你的 create() 方法因此可以准备表,但是并不一定需要打开它。
UpscaledbHandler::create() 的实现非常直接。首先它创建一个 upscaledb 环境,然后创建一个针对每个索引的数据库。如果用户没有为对应的表创立主键,那么将会生成一个索引。数据库的配置文件依赖于每列的实际类型以及其他的一下参数(例如,是否唯一)。
打开表
ExampleHandler::open 和 UpscaledbHandler::open 每当打开表时都会被调用。如上面提到的,它将会在同一个表中发生多次。因此你需要将你的实际数据存储到 'Share' 中。
当检索到一个 'Share' 对象的指针后,UpscaledbHandler::open() 方法将会检查 upscaledb 环境是否已经打开。如果打开,它将会立即返回。如果没有,它将会打开文件,并且将环境的 Handle 存储到 'Share' 中。
关闭表
ExampleHandler::close 和 UpscaledbHandler::close 方法被用来关闭表。如果你的表数据存储在 Share 中,那么你将会使用引用计数来确定何时销毁 Share 对象。在我的 UpscaledbHandler 中我不会销毁我的 Share,因为 Share 在之后的很长时间将会起作用。
插入行(INSERTing rows)
不论你何时调用 INSERT SQL 状态,你 handler 的 write_row() 方法都会被调用。它唯一的参数就是新的一行,字节阵列中的序列。当你调用 CREATE TABLE 状态时,阵列会用不同的顺序存储实际上的列。主键永远是初始的,后面跟随着其他索引过的列,最后跟着未索引的列。
这个字节阵列通常开始于一个(可选的)位映射,它描述了当前列中的空值。后面跟随着任何一个修改过长度的列,或者是变长的列(例如 TINYTEXT,MEDIUMTEXT, TEXT, LONGTEXT 或者是一个对应的BLOB 列)。变长的列通常以一到两个存储了列的大小的字节开始,后面跟着相关的数据。(这些数据能够被存储为分隔的块;因此这个字节序列包含了一个对应了实际数据的编码指针。)如果你将一行存进文件中,它将会压缩变量长度为一个更加紧凑的格式,从而节约空间。
我的 UpscaledbHandler 缓存了索引的域对象,从而可以快速解压索引的列。下面的代码可以用于解压一个索引行的 key (‘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;
}
当解压了检索 key 之后,你可以在你的文件中存储行数据。同时你将需要解决三个问题:
用户没有指定任何的索引或者主键。
用户指定了主键但是没有制定其他的索引。
用户制定了主键和额外的索引。
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!
);
在 Handler 中,DELETE SQL 命令以调用 delete_row()方法作为收尾。你需要确保 所有 的部分都被删除了,不仅仅是主要的。同时,删除主键是非常简单的,因为 MySQL 将会使用一个数据库游标来定位它。 可以参考 [UpscaledbHandler::delete_row()][https://github.com/cruppstahl/upscaledb-mysql/blob/d4c2744e612616efc2deeeaf4ccd3132959c0e14/storage/upscaledb/ha_upscaledb.cc#L1163-L1217] 的实现。
更新行(UPDATEing rows)
这是最复杂的一部分 - 至少如果你希望让它能够比较快的话。 updata_row() 方法接受两个参数:旧的行的值和新行的值。最原始的方法就是调用 delete_row() 来删除原始的行,然后调用 write_row() 来写入新行。这样会运行的非常慢,因为即使只更新一列,你实际上也更新了所有的列。只更新修改了的列会快的多。
游标
对于大部分的任务,MySQL 核心仅仅会创建一个数据库游标,定位一个 key(不论是在主键或者第二检索上),然后在实际数据上工作的时候向前移动。下面是一些为了支撑游标的功能你需要实现的一些方法。
index_init(): creates a cursor for a secondary index
index_end(): can close the cursor
index_read_map(): positions the cursor on a row
index_next(): moves cursor to the next key, retrieves the row
index_prev(): moves cursor to the previous key, retrieves the row
index_last(): moves cursor to the last key, retrieves the row
index_first(): moves cursor to the first key, retrieves the row
index_next_same(): moves cursor to the next duplicate of the current key
rnd_init(): creates a cursor for the primary index
rnd_end(): closes the cursor
rnd_next(): moves cursor to the next key, retrieves the row
rnd_pos(): moves cursor to a specified row
其他的方法
接下来会提到一些其他重要的或者有趣的方法。
rename_table():对表进行重命名(以及它所有的文件)。每当你的 schema 改变时都会调用这个方法,例如,当你增加一列时。MySQL 会将所有的数据复制到一个临时的表中,然后使用这个方法将你临时表中的数据重命名到你原始的表中。
delete_table(): 删除一个表(以及所有包含的文件)
table_flags(): 返回一个描述了你的 Handler 能力的 flag 集。这些 flag 很多都没有很好的文档,并且很多都是针对 InnoDB 生成的文档。我的猜测是只有 InnoDB 实现了所有的功能。
index_flags(): 返回一个描述了你的 Handler 能力的 flag 集。例如,你的 index_prev() 和 index_next() 提供的功能。
总结
编写自己的存储引擎听起来像一个很复杂的任务,但是实际上并不是。你不需要实现我上面说的所有方法。对于有些方法,MySQL将会找出缺失的实现并且提供自己的方法,但是对于其他的,将会有一些报错。ExampleHandler 本来是空的并且不提供任何功能。但是你能够加载并且为它加上断电,一个接一个地实现功能。如果你遇到了问题,那么你可以参考 MySQL 已有的存储引擎或者是 MariaDB 。mysql-internals 邮件列表里的开发者们也能提供一定的帮助。
一些关于有趣的存储引擎的想法浮现在我脑海中:
· 一个将数据存储在 HDFS 中的只增数据库;使用者能够使用 Spark 或者 Hadoop 的 map/reduce 方法来进行进一步的数据处理。
· 基于 std::map 或者 std::multi_map 的存储中的表。
· 使用 XML 文件后端的存储引擎。
你还有什么其他的想法呢?
License
This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)