MySQL-专家级教程-五-

163 阅读1小时+

MySQL 专家级教程(五)

原文:Expert MySQL, 2nd Edition

协议:CC BY-NC-SA 4.0

九、开发 MySQL 插件

在我们浏览 MySQL 源代码和架构的第三章中,我们提到了 MySQL 的一个特殊特性,叫做插件。MySQL 插件是专门设计的动态库,允许您在不使服务器离线的情况下向服务器添加新功能。目前支持多种形式的插件,但是随着服务器的不断发展,预计会看到更多使用这种架构的特性。

本章更详细地研究了 MySQL 插件架构。您将了解更多关于插件如何工作,如何构造,以及服务器支持哪些类型的插件。我还将演示如何通过创建一个独特的认证插件来创建一个插件。

MySQL 插件解释

MySQL 插件包含在名为库的动态可加载模块中。一个库可以包含一个或多个插件,可以单独安装(加载)或卸载(卸载)。插件以专门特性的形式为服务器提供扩展。除了特性本身,插件还可以包含自己的状态和系统变量。一个插件是使用被称为应用编程接口(API) 的标准化架构开发的。

插件架构(称为 MySQL 插件 API)使用一组特殊的结构,其中包含信息以及指向公共方法的函数指针。使用公共结构允许服务器调用特定的方法,由此函数指针将调用重新映射到该插件的方法的特定实现。我将在后面的章节中更详细地解释插件架构。

插件的类型

MySQL 服务器目前支持几种类型的插件。我们已经看到了一个非常早期的插件的例子——用户自定义函数 1 (UDF)。第 7 章详细介绍了这种形式的插件。有些人会说 UDF 不是真正的插件,尽管事实上它们是可动态加载的,并且使用相同的命令来安装和卸载。这是因为它们不使用标准的插件架构。

表 9-1 列出了使用该架构支持的插件类型,包括插件名称、类型名称、简短描述以及源代码中示例的位置(如果有的话)。

表 9-1 。MySQL 插件 API 支持的插件

类型描述例子
南非民主统一战线(United Democratic Front)SQL 命令中使用的特殊函数。/sql/udf_example.cc
存储引擎用于读写数据的存储引擎。/存储/*
全文分析器用于在表中搜索文本列的全文分析器。/插件/全文
守护进程允许将离散代码模块加载到服务器中,而无需与服务器本身进行交互,例如复制心跳和监控/插件/守护程序 _ 示例
信息图式允许创建新的 INFORMATION_SCHEMA 视图,以便向用户传达信息。
审计启用服务器审核。MySQL 商业版中有一个审计日志插件。/plug-in/audit _ null
复制专门的复制功能,如更改事件执行的同步方法。/plugin/semisync
证明更改登录服务器的验证方法。/plugin/auth
验证密码实施密码规则以获得更安全的密码。/plugin/密码验证

正如您所看到的,有许多类型的插件,它们提供了广泛的特性。随着更加强调模块化设计,我们很可能会在未来看到更多的插件类型。

使用 MySQL 插件

插件可以使用特殊的 SQL 命令作为启动选项来加载和卸载,也可以通过mysql_plugin客户端应用来加载和卸载。

要使用 SQL 命令加载插件,使用LOAD PLUGIN命令,如下所示。这里,我们正在加载一个名为something_cool 的插件,它包含在名为some_cool_feature.so的编译库模块中。这些库需要放在plugin_dir路径中,以便服务器可以找到它们。

mysql> SHOW VARIABLES LIKE 'plugin_dir';
+−−-------------+−−----------------------------+
| Variable_name | Value                        |
+−−-------------+−−----------------------------+
| plugin_dir    | /usr/local/mysql/lib/plugin/ |
+−−-------------+−−----------------------------+
1 row in set (0.00 sec)

image 注意MySQL 文档使用术语安装卸载来动态加载和卸载插件。文档使用术语 load 来指定通过启动选项使用的插件。

mysql> INSTALL PLUGIN something_cool SONAME some_cool_feature.so;

卸载插件更容易,如下所示。这里我们正在卸载刚刚安装的插件。

mysql> UNINSTALL PLUGIN something_cool;

插件也可以在启动时使用- plugin-load 选项安装。这个选项可以被多次列出——每个插件一次——或者可以接受一个分号分隔的列表(没有空格)。如何使用此选项的示例包括:

mysqld ... --plugin-load=something_cool ...
mysqld ... --plugin-load=something_cool;something_even_better ...

还可以使用mysql_plugin客户端应用加载和卸载插件。该应用要求服务器停止工作。它将以引导模式启动服务器,加载或卸载插件,然后关闭引导的服务器。该应用主要用于停机期间的服务器维护,或者作为一种诊断工具,用于通过消除插件(以简化诊断)来尝试重启故障服务器。

客户端应用使用一个配置文件来保存关于插件的相关数据,比如库的名称和其中包含的所有插件。一个插件库可以包含多个插件。下面是daemon_example插件的配置文件的一个例子。

#
# Plugin configuration file. Place on a separate line:
#
# library binary file name (without .so or .dll)
# component_name
# [component_name] - additional components in plugin
#
libdaemon_example
daemon_example

要使用mysql_plugin应用安装(启用)或卸载(禁用)插件,请至少指定插件的名称、ENABLEDISABLEbasedirdatadirplugin-dirplugin-ini选项。如果该应用不在您的路径上,您可能还需要指定my-print-defaults选项。应用以静默方式运行,但是您可以打开 verbosity 来查看应用的运行情况(vvv)。下面描述了一个使用客户端应用加载daemon_example插件的例子。

cbell$ sudo ./mysql_plugin --datadir=/mysql_path/data/ --basedir=/mysql_path/ --plugin-dir=../plugin/daemon_example/ --plugin-ini=../plugin/daemon_example/daemon_example.ini --my-print-defaults=../extra daemon_example ENABLE -vvv
# Found tool 'my_print_defaults' as '/mysql_path/bin/my_print_defaults'.
# Command: /mysql_path/bin/my_print_defaults mysqld > /var/tmp/txtdoaw2b
#    basedir = /mysql_path/
# plugin_dir = ../plugin/daemon_example/
#    datadir = /mysql_path/data/
# plugin_ini = ../plugin/daemon_example/daemon_example.ini
# Found tool 'mysqld' as '/mysql_path/bin/mysqld'.
# Found plugin 'daemon_example' as '../plugin/daemon_example/libdaemon_example.so'
# Enabling daemon_example...
# Query: REPLACE INTO mysql.plugin VALUES ('daemon_example','libdaemon_example.so');
# Command: /mysql_path/bin/mysqld --no-defaults --bootstrap --datadir=/mysql_path/data/ --basedir=/mysql_path/ < /var/tmp/sqlft1mF7
# Operation succeeded.

请注意,在输出中,我必须使用超级用户权限。如果您试图从安装在隔离对 mysql 文件夹的访问的平台(如 Linux 和 Mac OS X)上的服务器安装或卸载插件,您将需要使用此工具。还要注意,详细输出显示了此应用正在做什么。在这种情况下,它用我们指定的插件的信息替换了mysql.plugin表中的任何行。类似的删除查询将被发出以禁用插件。

您可以通过以下三种方式之一发现哪些插件已经加载或已经加载。您可以使用特殊的 SHOW 命令,从 mysql.plugin 表中选择信息,或者从 INFORMATION_SCHEMA.plugins 视图中选择信息。每一种显示的信息都略有不同。下面演示了这些命令。为了简洁起见,我使用了输出的摘录。

mysql> show plugins;
+−−--------------------------+−−--------+−−------------------+−−-------+−−-------+
| Name                       | Status   | Type               | Library | License |
+−−--------------------------+−−--------+−−------------------+−−-------+−−-------+
| binlog                     | ACTIVE   | STORAGE ENGINE     | NULL    | GPL     |
| mysql_native_password      | ACTIVE   | AUTHENTICATION     | NULL    | GPL     |
| mysql_old_password         | ACTIVE   | AUTHENTICATION     | NULL    | GPL     |
| sha256_password            | ACTIVE   | AUTHENTICATION     | NULL    | GPL     |
| CSV                        | ACTIVE   | STORAGE ENGINE     | NULL    | GPL     |
| MEMORY                     | ACTIVE   | STORAGE ENGINE     | NULL    | GPL     |
| MyISAM                     | ACTIVE   | STORAGE ENGINE     | NULL    | GPL     |
| MRG_MYISAM                 | ACTIVE   | STORAGE ENGINE     | NULL    | GPL     |
| ARCHIVE                    | ACTIVE   | STORAGE ENGINE     | NULL    | GPL     |
| BLACKHOLE                  | ACTIVE   | STORAGE ENGINE     | NULL    | GPL     |
| FEDERATED                  | DISABLED | STORAGE ENGINE     | NULL    | GPL     |
| InnoDB                     | ACTIVE   | STORAGE ENGINE     | NULL    | GPL     |
...
| PERFORMANCE_SCHEMA         | ACTIVE   | STORAGE ENGINE     | NULL    | GPL     |
| partition                  | ACTIVE   | STORAGE ENGINE     | NULL    | GPL     |
+−−--------------------------+−−--------+−−------------------+−−-------+−−-------+
41 rows in set (0.00 sec)

注意显示在SHOW PLUGINS命令输出中的信息。这个视图是所有已知插件的列表,其中一些插件是通过命令行选项或特殊的编译指令自动加载的。它显示插件类型以及许可证类型。现在,让我们看看mysql.plugin表的输出。

mysql> select * from mysql.plugin;
Empty set (0.00 sec)

但是等等,没有输出!这是因为mysql.plugin表只存储那些已经安装的动态插件——更确切地说,是那些用INSTALL PLUGIN命令安装的插件。由于我们没有安装任何插件,所以没有什么可显示的。下面显示了插件安装后的输出。

mysql> install plugin daemon_example soname 'libdaemon_example.so';
Query OK, 0 rows affected (0.00 sec)

mysql> select * from mysql.plugin;
+−−--------------+−−--------------------+
| name           | dl                   |
+−−--------------+−−--------------------+
| daemon_example | libdaemon_example.so |
+−−--------------+−−--------------------+
1 row in set (0.00 sec)

现在让我们看看 INFORMATION_SCHEMA.plugins 视图的输出。下面显示了视图的输出。

mysql> select * from information_schema.plugins \G
*************************** 1\. row ***************************
           PLUGIN_NAME: binlog
        PLUGIN_VERSION: 1.0
         PLUGIN_STATUS: ACTIVE
           PLUGIN_TYPE: STORAGE ENGINE
   PLUGIN_TYPE_VERSION: 50606.0
        PLUGIN_LIBRARY: NULL
PLUGIN_LIBRARY_VERSION: NULL
         PLUGIN_AUTHOR: MySQL AB
    PLUGIN_DESCRIPTION: This is a pseudo storage engine to represent the binlog in a transaction
        PLUGIN_LICENSE: GPL
           LOAD_OPTION: FORCE
*************************** 2\. row ***************************
           PLUGIN_NAME: mysql_native_password
        PLUGIN_VERSION: 1.0
         PLUGIN_STATUS: ACTIVE
           PLUGIN_TYPE: AUTHENTICATION
   PLUGIN_TYPE_VERSION: 1.0
        PLUGIN_LIBRARY: NULL
PLUGIN_LIBRARY_VERSION: NULL
         PLUGIN_AUTHOR: R.J.Silk, Sergei Golubchik
    PLUGIN_DESCRIPTION: Native MySQL authentication
        PLUGIN_LICENSE: GPL
           LOAD_OPTION: FORCE
*************************** 3\. row ***************************
           PLUGIN_NAME: mysql_old_password
        PLUGIN_VERSION: 1.0
         PLUGIN_STATUS: ACTIVE
           PLUGIN_TYPE: AUTHENTICATION
   PLUGIN_TYPE_VERSION: 1.0
        PLUGIN_LIBRARY: NULL
PLUGIN_LIBRARY_VERSION: NULL
         PLUGIN_AUTHOR: R.J.Silk, Sergei Golubchik
    PLUGIN_DESCRIPTION: Old MySQL-4.0 authentication
        PLUGIN_LICENSE: GPL
           LOAD_OPTION: FORCE
...

*************************** 41\. row ***************************
           PLUGIN_NAME: partition
        PLUGIN_VERSION: 1.0
         PLUGIN_STATUS: ACTIVE
           PLUGIN_TYPE: STORAGE ENGINE
   PLUGIN_TYPE_VERSION: 50606.0
        PLUGIN_LIBRARY: NULL
PLUGIN_LIBRARY_VERSION: NULL
         PLUGIN_AUTHOR: Mikael Ronstrom, MySQL AB
    PLUGIN_DESCRIPTION: Partition Storage Engine Helper
        PLUGIN_LICENSE: GPL
           LOAD_OPTION: ON
*************************** 42\. row ***************************
           PLUGIN_NAME: daemon_example
        PLUGIN_VERSION: 1.0
         PLUGIN_STATUS: ACTIVE
           PLUGIN_TYPE: DAEMON
   PLUGIN_TYPE_VERSION: 50606.0
        PLUGIN_LIBRARY: libdaemon_example.so
PLUGIN_LIBRARY_VERSION: 1.4
         PLUGIN_AUTHOR: Brian Aker
    PLUGIN_DESCRIPTION: Daemon example, creates a heartbeat beat file in mysql-heartbeat.log
        PLUGIN_LICENSE: GPL
           LOAD_OPTION: ON
42 rows in set (0.01 sec)

mysql>

我们看到了与SHOW PLUGINS命令相似的信息,但是有更多的信息。除了名称、类型和许可信息,我们还可以看到插件的作者、版本和描述。还要注意,动态加载的插件daemon_example也显示在视图中。

现在您已经知道了这些命令的作用,您可以使用适当的命令来处理插件了。例如,如果您想一目了然地看到哪些插件可用,请使用SHOW PLUGINS命令。如果您想查看加载了哪些插件,请查询mysql.plugin表。如果你想查看可用插件的元数据,查询INFORMATION_SCHEMA.plugins视图。

MySQL 插件 API

插件架构在/include/mysql/plugin.h 中定义,这个文件中有很多元素,包括一些插件类型的专门代码。对每一行代码的完整解释超出了本书的范围;相反,在这一节中,我们关注的是为了构建你自己的插件,你需要熟悉的关键元素。

在文件的顶部附近,您会找到创建插件时使用的符号和值的定义。清单 9-1 显示了最常用符号的定义。每个插件类型都有定义,许可证类型也有定义。

清单 9-1 定义来自 plugin.h

/*
  The allowable types of plugins
*/
#define MYSQL_UDF_PLUGIN             0  /* User-defined function        */
#define MYSQL_STORAGE_ENGINE_PLUGIN  1  /* Storage Engine               */
#define MYSQL_FTPARSER_PLUGIN        2  /* Full-text parser plugin      */
#define MYSQL_DAEMON_PLUGIN          3  /* The daemon/raw plugin type */
#define MYSQL_INFORMATION_SCHEMA_PLUGIN  4  /* The I_S plugin type */
#define MYSQL_AUDIT_PLUGIN           5  /* The Audit plugin type        */
#define MYSQL_REPLICATION_PLUGIN     6       /* The replication plugin type */
#define MYSQL_AUTHENTICATION_PLUGIN  7  /* The authentication plugin type */
#define MYSQL_VALIDATE_PASSWORD_PLUGIN  8   /* validate password plugin type */
#define MYSQL_MAX_PLUGIN_TYPE_NUM    9  /* The number of plugin types   */

/* We use the following strings to define licenses for plugins */
#define PLUGIN_LICENSE_PROPRIETARY 0
#define PLUGIN_LICENSE_GPL 1
#define PLUGIN_LICENSE_BSD 2

#define PLUGIN_LICENSE_PROPRIETARY_STRING "PROPRIETARY"
#define PLUGIN_LICENSE_GPL_STRING "GPL"
#define PLUGIN_LICENSE_BSD_STRING "BSD"

插件支持的许可类型有定义。对于大多数标准的 MySQL 插件,许可是 GPL。对于那些只有 MySQL 商业许可才可用的插件,许可设置为专有。如果您需要添加更多的许可证类型,将它们添加到文件中,增加值,并提供一个文本字符串以在插件视图中标识它。

MySQL 插件 API 用来与服务器通信的机制是 st_mysql_structure,也是在/include/mysql/plugin.h 文件中定义的。清单 9-2 显示了 st_mysql_structure 的定义。

清单 9-2?? plugin . h 中的 st_mysql_plugin 结构

/*
  Plugin description structure.
*/

struct st_mysql_plugin
{
  int type;             /* the plugin type (a MYSQL_XXX_PLUGIN value)   */
  void *info;           /* pointer to type-specific plugin descriptor   */
  const char *name;     /* plugin name                                  */
  const char *author;   /* plugin author (for I_S.PLUGINS)              */
  const char *descr;    /* general descriptive text (for I_S.PLUGINS)   */
  int license;          /* the plugin license (PLUGIN_LICENSE_XXX)      */
  int (*init)(MYSQL_PLUGIN);  /* the function to invoke when plugin is loaded */
  int (*deinit)(MYSQL_PLUGIN);/* the function to invoke when plugin is unloaded */
  unsigned int version; /* plugin version (for I_S.PLUGINS)             */
  struct st_mysql_show_var *status_vars;
  struct st_mysql_sys_var **system_vars;
  void * __reserved1;   /* reserved for dependency checking             */
  unsigned long flags;  /* flags for plugin */
};

结构中的前六个属性包含关于插件的元数据信息,包括类型、描述、名称、作者信息和许可证信息。接下来的两个属性是函数指针,指向加载和卸载插件的函数。接下来是包含为插件定义的状态和系统变量的结构。最后,有一个属性用于设置标志,以便将插件功能传达给服务器。

需要特别注意的是 info 属性。这是一个指向每种插件专用结构的指针。它们在/include/mysql 的头文件中定义,名为 plugin_其中代表插件类型。例如,plugin_auth.h 文件包含认证插件类型的结构定义。

定义的结构也以插件命名。每个结构包含每个插件类型的特定方法的属性和函数指针。通过这种方式,服务器可以成功地导航和调用每个插件类型的特定方法。下面显示了 plugin_auth.h 文件中的 st_mysql_auth 结构。

/**
  Server authentication plugin descriptor
*/
struct st_mysql_auth
{
  int interface_version;                        /** version plugin uses */
  /**
    A plugin that a client must use for authentication with this server
    plugin. Can be NULL to mean "any plugin".
  */
  const char *client_auth_plugin;
  /**
    Function provided by the plugin which should perform authentication (using
    the vio functions if necessary) and return 0 if successful. The plugin can
    also fill the info.authenticated_as field if a different username should be
    used for authorization.
  */
  int (*authenticate_user)(MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *info);
};

文件中的特殊版本号对于每种插件类型都是唯一的。它定义了插件类型的版本号,用于在安装时帮助识别和确认架构与服务器的兼容性。

#define MYSQL_AUTHENTICATION_INTERFACE_VERSION 0x0100

插件和版本号

st_mysql_plugin 结构包含一个版本属性。除了在插件视图中显示之外,服务器不会直接使用它。我们应该知道另外两个版本号。第一个是 PLUGIN_LIBRARY_VERSION,服务器设置的版本号,表示插件 API 的版本。这允许服务器知道一个插件是否有兼容的架构。第二个是 PLUGIN_VERSION_TYPE,它特定于每个插件类型。我们可以在/library/mysql/plugin.h 中看到这些:

#定义 MYSQL _ PLUGIN _ INTERFACE _ VERSION 0x 0104

5.6.6 服务器的值是 1.4。您可以在上面 INFORMATION_SCHEMA.plugins 视图的输出中看到这一点。

#定义 MYSQL _ DAEMON _ INTERFACE _ VERSION(MYSQL _ VERSION _ ID < < 8)

上面显示了 daemon_example 的特定插件类型。在这种情况下,服务器的版本被放在高位字节中,以帮助进一步识别插件。对于服务器版本 5.6.6,该值将被计算为 50606.0。您可以在上面 INFORMATION_SCHEMA.plugins 视图的输出中看到这一点。

image 注意正如我们上面看到的,大多数插件类型都有特定的值。作为早期的例子,daemon_example 的版本号为 0。

要创建一个插件,首先在/plugin 文件夹中创建一个新文件夹,命名为容易与你的插件关联的东西。在该文件夹中至少放置一个源文件,该文件包含 st_mysql_plugin 结构的实现,以及与插件类型相关的信息结构的具体实现。您应该用正确的元数据填充插件结构,实现初始化和取消初始化的方法,并实现插件类型的特定方法。

或者,您可以在源代码树之外创建一个文件夹,编译它,并将其与服务器库链接起来。如果高级开发者希望将插件代码从服务器源代码中分离出来,他们可能想探索这个选项。

您还将创建一个 ini 文件,其中包含关于插件的信息,如“使用 MySQL 插件”一节中所述如果你有具体的结构,变量,定义等。,对于您的插件,您可以创建适当的文件并将它们放在同一个文件夹中。

现在我们已经看到了创建插件的构建模块,我们将看到如何编译插件,然后开始创建我们自己的插件。

编译 MySQL 插件

你可能会想,有一些神秘的机制用来编译你的插件。我有好消息告诉你——没有。您唯一需要的是一个 CMakeList.txt 文件,其中包含编译插件的 cmake 指令。对于库中只有一个插件的简单插件,文件内容很短。清单 9-3 显示了示例认证插件的完整内容。

清单 9-3 CMakeLists.txt 为认证插件示例

# Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; version 2 of the
# License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 021101301  USA

MYSQL_ADD_PLUGIN(auth dialog.c
  MODULE_ONLY)

MYSQL_ADD_PLUGIN(auth_test_plugin test_plugin.c
  MODULE_ONLY)

MYSQL_ADD_PLUGIN(qa_auth_interface qa_auth_interface.c
  MODULE_ONLY)

MYSQL_ADD_PLUGIN(qa_auth_server qa_auth_server.c
  MODULE_ONLY)

MYSQL_ADD_PLUGIN(qa_auth_client qa_auth_client.c
  MODULE_ONLY)

CHECK_CXX_SOURCE_COMPILES(
"#define _GNU_SOURCE
#include <sys/socket.h>
int main() {
  struct ucred cred;
  getsockopt(0, SOL_SOCKET, SO_PEERCRED, &cred, 0);
}" HAVE_PEERCRED)

IF(HAVE_PEERCRED)
  MYSQL_ADD_PLUGIN(auth_socket auth_socket.c
    MODULE_ONLY)
ENDIF()

这个例子定义了五个插件。对于每个插件,MYSQL_ADD_PLUGIN 指令用于定义插件名称,并将其与源文件关联起来。在文件的顶部,定义了一个与插件无关的模块。如果您的插件代码要使用您在其他源文件中定义的特殊方法、函数或类,这就是您指定要编译的附加源文件的方式。

既然我们知道了构建插件的构件是什么,在哪里可以找到特定于插件类型的结构和定义,以及如何构建插件,我们就可以构建一个新的插件了。

在接下来的几节中,我将向您展示如何构建一个身份验证插件,该插件使用一个硬件设备来进一步保护您的服务器,方法是将登录限制为必须拥有特殊钥匙卡和个人识别码(PIN) 的用户。

RFID 认证插件

为了说明如何创建 MySQL 插件,我将向您展示如何创建身份验证插件,因为这可能是最有用的插件之一,也是寻求定制 MySQL 安装的开发者可能想要创建的领域之一。我选择通过引入一个硬件设备来使该解决方案比典型的用户名和密码对更安全,从而使项目更有趣。在这种情况下,硬件设备读取管理员给用户的特殊身份卡,该卡包含硬件设备只读的唯一号码。这比简单的密码提供了更高一级的安全性。 2

我选择使用的 keycard 设备是射频识别卡(RFID) 。3RFID 标签通常是信用卡大小的塑料卡、标签或类似的东西,它包含一个特殊的天线,通常是线圈、细线或箔层的形式,被“调谐”到特定的频率,以便当读取器发射无线电信号时,标签可以使用电磁能量来传输嵌入在嵌入线圈中的非易失性消息,然后将其转换为字母数字串。这种形式的 RFID 标签是一种无源设备,因为它不包含电池。需要更大范围或功率的 RFID 系统通常在 RFID 标签本身中包括电池。这些被称为活动标签。

标签和阅读器必须调谐到相同的频率。有许多 RFID 标签制造商和众多频率。因此,当选择将 RFID 系统整合到您的项目中时,请确保您购买的标签与阅读器的频率相同。我建议购买一个包括阅读器和几个标签的套件,以确保避免兼容性问题。

对于这个项目,我们将使用这个字符串作为认证机制的一部分。如果我们还包括提示用户记住 PIN,这将进一步加强解决方案的安全性,因为除了目标用户之外,没有人可以使用该卡登录(当然,除非他们分享了他们的 PIN,但在这种情况下,您会遇到更严重的问题)。

该解决方案更加安全,因为它不仅仅依赖于用户必须知道的东西,例如密码,用户还必须拥有他们必须出示的物理项目来完成身份验证。因此,该解决方案提供了被认为是非常安全的身份验证的三个要素中的两个。第三个要素是进一步识别用户的生物特征,如指纹或掌纹。

在下一节中,我将描述这种认证机制是如何工作的。正如您将看到的,它包括一个客户机软件组件、一个客户机硬件组件和一个服务器软件组件。软件组件是一个特殊的认证插件。

操作概念

该项目使用 RFID 阅读器和标签或钥匙卡来代替传统的用户密码。服务器端的设置包括使用 CREATE USER 命令的变体来创建用户,并将她的帐户与 RFID 认证插件相关联。用户还被分配或允许使用一个短的数字串(我像大多数银行和信用卡一样使用 4 位数字)选择一个特殊的个人识别码(PIN)。我们使用 RFID 代码和连接的 PIN 来形成该用户的密码。

当用户希望登录到服务器时,她启动她的 mysql 客户端,然后提示她刷卡,一旦读取正确,就要求输入她的 PIN。该信息然后被传输到服务器以验证组合的代码和 PIN。通过验证后,用户登录到服务器,客户端继续。如果代码不匹配,用户将收到相应的错误消息,指示其登录尝试失败。

如果这听起来对你们有些人来说很熟悉,这不是一个意外。我使用了几个类似的系统来访问分配给我的资源。这些系统不仅更安全,而且对用户来说也更容易,因为除非他们丢失了钥匙卡,否则她只需记住一个短的密码。

既然您已经熟悉了系统的运行方式,那么让我们来看看如何构建系统。

RFID 模块

您需要的第一个组件是 RFID 阅读器(模块)。我选择购买一个入门套件,其中包含一个可以读取 125kHz 标签和三个标签(钥匙卡)的 RFID 标签读取器。我是一名创客,所以我经常求助于迎合创客群体的电子产品供应商。一个是 SparkFun 电子(http://www.sparkfun.com)

image 注意如果您购买了不同的 RFID 系统,请遵循供应商的安装指南,但如果您的系统相似,请阅读以下内容。

SparkFun 的 RFID 入门套件(项目# RTL-09875 $49.95 美元)是一个很好的选择,因为它是基于 USB 的,因此可以在所有现代平台上工作。它相对便宜,并在模块上暴露引脚,允许您探索 RFID 模块的硬件功能,如果您想做出更复杂的解决方案。虽然它不包含 Shell(稍后将详细介绍),但它确实提供了一个无焊料解决方案,并配有一个声音读取蜂鸣器。最后,该套件包括三个带有独特 RFID 代码的钥匙卡。

图 9-1 显示了零售包装中的 RFID 入门套件。它包含一个 RFID 模块、模块板和三张钥匙卡。你必须提供自己的 USB 转迷你 USB 电缆(也可以从 SparkFun 获得)。图 9-2 显示了模块板本身的细节,以及为开发者准备的额外引脚(显示在右边)。图 9-3 显示了套件中包含的样本钥匙卡。

我建议通读这一章,并跟随代码走一遍。一旦您订购了自己的 RFID 套件,您可以返回到该章节并完成示例。

9781430246596_Fig09-01.jpg

图 9-1。spark fun 电子公司的 RFID 启动套件

9781430246596_Fig09-02.jpg

图 9-2。 RFID 模块板

9781430246596_Fig09-03.jpg

图 9-3。钥匙卡

我喜欢从迎合业余爱好者和专业人士的电子产品供应商那里购买产品的一个原因是,他们通常会提供大量的产品信息。他们的网站上什么都有,从数据手册(详细的规格,包括电子专业人员在项目中使用元件所需的一切)和原理图到示例项目的链接,在某些情况下,还有教程和快速入门指南。

在这方面,SparkFun 是一个优秀的供应商。例如,RFID 入门套件包含数据手册、原理图、用于创建您自己的模块板的 Eagle 文件的链接,以及使用套件的快速入门指南(http://www.sparkfun.com/tutorials/243)。

我不会复制本书中的快速入门指南,但我会在接下来的章节中向您介绍在这个示例项目中使用 RFID 模块需要做的事情。我建议你在通读完本章后,阅读快速入门指南,以查看设置的替代演示。

image 快速入门指南是为 Windows 编写的,所以如果你使用 Windows,你会发现该指南比使用其他平台更有用。

安装驱动程序

如果你想使用 RFID 模块通过串行接口 5 读取代码,就像调制解调器从 COM 端口读取一样,你需要一个叫做 FTDI 芯片驱动程序的特殊驱动程序。幸运的是,SparkFun 上的人也提供了一个链接。

大多数平台都需要这个驱动程序,这样现有的软件就可以通过那些标准化的 COM 端口从设备中读取数据。安装驱动程序并连接 RFID 模块后,驱动程序会将 USB 端口映射到 COM 端口(Windows)或 tty 设备(Linux 和 Mac OS X)。您可以通过如下所示列出/dev文件夹的内容来发现 Linux 和 Mac OS X 平台中的 USB 设备。

Chucks-iMac:∼ cbell$ ls /dev/tty.usb*
/dev/tty.usbserial-A501D94V

在上面的输出中,我们看到了一个名为的设备,这样它就为您提供了一个线索。FTDI 驱动程序将负责连接 USB 端口和标准通信端口协议。

在 Windows 上,您可以通过打开设备管理器,展开 COM 和 LPT 端口树,右键单击一个端口,然后选择高级设置对话框来找到指定的 COM 端口。您不仅可以看到分配的 COM 端口,如果需要,您还可以更改它。图 9-4 显示了该对话框的一个例子。

9781430246596_Fig09-04.jpg

图 9-4。Windows 上的 COM 端口高级设置

image 提示我发现有必要把我的 Windows 笔记本电脑的 COM 端口从 COM13 改成 COM1。在我更改映射之前,RFID 模块不能与 Windows 上的任何终端客户端一起工作。一旦改变,它可以在终端客户端和认证插件中完美地工作。

安装驱动程序并通过 USB 电缆连接模块后,打开终端客户端并更改设置以连接到 COM 端口。SparkFun RFID 入门套件使用 9600,8,N,1 的波特率、位、奇偶校验和停止位配置。这是大多数终端客户端的典型默认设置。如果您的客户端有连接按钮或开关,请立即点按它,然后尝试刷卡。如果一切正常,您应该会听到 RFID 模块发出一声响亮的嘟嘟声,表明它已经读取了钥匙卡,并且您应该会看到终端客户端中出现一个 12 个或更多字符的字符串。如果没有,请返回并诊断 COM 端口、USB 端口和终端客户端的设置。

发现卡的识别号码

如果你仔细看了钥匙卡(也许试着看天线),你可能会注意到上面没有写任何代码。那你怎么知道密码是什么呢?如果您按照上一节所述设置 RFID 模块,您将会看到该钥匙卡的代码。

因此,您需要阅读每张钥匙卡并记下返回的代码。现在花一点时间来发现所有的代码,并在每张卡上做一个标记,这样你就可以发现(回忆)与钥匙卡相关的代码。图 9-5 显示了一个终端客户端从连接的 RFID 模块读取代码的例子。

9781430246596_Fig09-05.jpg

图 9-5。终端客户端读取 RFID 码

根据您的终端客户端设置(有些有十六进制视图选项),您可能会看到几个额外的字符,这些字符可能会显示为点或其他奇怪的符号。这些是 RFID 模块发送的控制代码,对于我们的使用,可以安全地忽略它们。您要寻找的是代表 RFID 代码的 12 个字符。

现在,您已经知道了与钥匙卡相关的代码,让我们绕一小段路,谈谈如何使裸露的印刷电路板(RFID 模块板)更加安全、坚固和用户友好。

固定 RFID 模块

除了对 RFID 阅读器本身缺乏保护之外,对这种解决方案的批评可能是对 RFID 阅读器本身缺乏安全性。正如你所看到的,对于一个精明的读者来说,将阅读器从 USB 系绳中拔出并在另一台计算机上使用它来发现他或其他人的钥匙卡号码是非常容易的。

幸运的是,这只能让差事用户到此为止。他还必须发现另一个用户的 PIN。以这种方式,保护 RFID 读取器本身可以被认为是不太重要的。如果你担心保护读者,我提供一些可能的解决方案。

如果您对硬件开发或电子产品有经验,您可以将 RFID 模块集成到用户的电脑机箱中。一个可能的位置是在一个空的驱动器托架后面。许多 PC 制造商在主板上有额外的 USB 连接器,一些供应商不连接这些额外的端口。在某些情况下,主板可能有专用的内部 USB 连接。如果有一个这样的接口,你可以将 RFID 阅读器的 USB 电缆连接到内部端口。最后,您可以使用特殊的安全锁来防止箱子被篡改。

我选择使用 Radio Shack 的一个小项目案例(项目编号 270–1801,价格 3.99 美元)来安装 RFID 模块。图 9-6 为工程案例。我首先在 Shell 的一端钻了一个小孔,这样读音就不会被抑制(它不会那么大声),我在另一端切了一个槽,让 USB 电缆可以贴合。

9781430246596_Fig09-06.jpg

图 9-6。 RFID Shell 未经修改

然后,我用一片双面胶带将 RFID 模块固定到金属盖上。我调整了模块的方向,使读者面朝上,塑料盒倒置(盖子朝下)放在桌子上。然后,我用提供的螺钉合上 Shell,放置一个不打滑的自粘性支脚来隐藏每个螺钉。图 9-7 显示未组装的机箱,图 9-8 显示完成的解决方案。让我们把完成的单元简称为 RFID 阅读器。

9781430246596_Fig09-07.jpg

图 9-7。组装 RFID 阅读器单元

9781430246596_Fig09-08.jpg

图 9-8。组装好的 RFID 阅读器

现在我们有了一个可以工作的 RFID 阅读器,让我们深入研究使用 MySQL 实现这一功能的代码。在下一节中,我将解释身份验证插件是如何构建的,并向您展示构建使用 RFID 阅读器来验证用户登录的身份验证插件的细节。

认证插件的架构

既然我们已经看到了解决方案是如何工作的,以及如何配置 RFID 阅读器和发现钥匙卡标识字符串,那么让我们来看看身份验证插件的组成和架构。

认证插件包含两个组件:客户端插件和服务器端插件。为了方便起见,这两者可以驻留在同一个代码模块中。

与所有插件一样,您必须在服务器上配置插件,然后才能使用它。本章前面和第 7 章中描述的程序与认证插件相同。具体来说,你必须将编译好的库放在由plugin-dir变量指定的文件夹中,并使用INSTALL PLUGIN命令来安装插件。下面是一个在 Linux 上安装我们将要构建的插件的示例命令。

INSTALL PLUGIN rfid_auth SONAME 'rfid_auth.so';

要将用户与认证插件相关联,使用创建用户命令的 IDENTIFIED WITH 子句(见下文)。这告诉服务器用请求客户机启动指定插件的客户端组件来替换普通的 MySQL 身份验证。

CREATE USER 'test'@'localhost' IDENTIFIED WITH rfid_auth AS 'XXXXXXXPPPPP';

在上面的两个示例命令中,我将插件称为 rfid_auth,这是我为 rfid 身份验证插件选择的名称。您将需要为您可能希望创建的任何身份验证插件提供相同的一致性。

还要注意AS子句。该子句允许您指定一个短语,服务器端身份验证插件可以使用该短语来帮助识别用户。出于说明、简洁和易于开发的目的,我选择使用这个字符串来存储用户的 keycard 代码和她的 PIN。虽然它以明文形式存储在mysql.user表中,但它仍然是安全的,因为大多数用户没有读取该表的权限。在后面的小节中,我将提供一些更安全的方法来存储这个值。现在让我们把注意力转向认证插件是如何工作的。

认证插件是如何工作的?

当用户与身份验证插件相关联,并且用户试图连接到服务器时,服务器将向服务器请求包含响应的数据包,服务器将使用该数据包来完成验证。这种机制反映了传统 MySQL 服务器认证协议的挑战和响应序列。

在这种情况下,客户端被设计为尝试加载与服务器端插件同名的相应客户端插件。客户端知道这一点,因为插件的名称在从服务器发送的包的一个特殊区域中被返回。通过这种方式,我们确信认证插件段(服务器和客户端)只相互通信。

你可能想知道,“这怎么可能呢?难道客户端不需要知道如何加载插件吗?”第二个问题的答案是,“是的。”客户端必须能够加载客户端插件。因此,您不能使用旧版本的 mysql 客户端应用通过与服务器端身份验证插件关联的用户帐户登录到服务器。

MySQL 客户端应用将尝试从 MySQL 配置文件中指定的- plugin-dir 加载插件。您还可以通过提供- plugin-dir 选项来指定其位置,如下所示:

cbell@ubuntu $ ../client/mysql -utest -h 127.0.0.1 --port=13000 --plugin-dir=../lib/plugin

现在我们对认证插件的工作原理有了一个概念,让我们花点时间看看每个插件是如何构造的。

创建身份验证插件

要创建身份验证插件,您需要创建以下三个文件。

  • cmakelists . txt–cmake 配置文件
  • RFID _ auth . cc–源文件
  • RFID _ auth . ini–插件 ini 文件(如上所述,由mysql_plugin客户端应用使用)

就这样!简单,嗯?现在开始创建文件夹。

构建 RFID 身份验证插件

让我们从 CMakeLists.txt 文件开始我们的编码工作。打开一个你选择的文本编辑器,输入如清单 9-4 所示的指令。第一行调用一个宏,该宏设置了正确编译 MySQL 插件(因此得名)所需的一切。该宏将插件名、源文件名和任何特殊指令作为参数。在这种情况下,我们使用MODULE_ONLY 6 来构建模块,但不将其链接到服务器,并使用MODULE_OUTPUT_NAME来设置已编译插件的名称。

***清单 9-4 。*cmakelists . txt 文件

# cmake configuration file for the RFID Authentication Plugin

MYSQL_ADD_PLUGIN(rfid_auth rfid_auth.cc
  MODULE_ONLY MODULE_OUTPUT_NAME "rfid_auth")

INSTALL(FILES rfid_auth.ini DESTINATION ${INSTALL_PLUGINDIR})

这是 Oracle MySQL 工程师不懈努力的又一个例子,他们努力使服务器代码更加模块化,更易于通过插件接口进行扩展。

现在我们准备开始编码解决方案。打开您最喜欢的代码编辑器,创建一个新文件,并将其命名为rfid_auth.cc。将其放在/plugin/rfid_auth文件夹中。我不会列出 rfid_auth.cc 文件的全部内容,而是一次遍历代码的一部分。我从包含文件部分开始,然后描述并列出客户端插件代码,稍后描述并列出服务器端插件代码。清单 9-5清单 9-10 中的所有代码都应该放在同一个源文件中。

包括文件和定义

首先,列出所有包含文件和您想为代码做的任何定义。清单 9-5 显示了 RFID 认证插件所需的包含文件。这是两个插件都需要的包含文件。

清单 9-5 包含和定义代码

#include <my_global.h>
#include <mysql/plugin_auth.h>
#include <mysql/client_plugin.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <mysql.h>

#ifndef __WIN__

#include <unistd.h>
#include <pwd.h>

#else

#include <windows.h>
#include <conio.h>
#include <iostream>
#include <string>
using namespace std;

/*
 Get password with no echo.
*/
char *getpass(const char *prompt)
{
  string pass ="";
  char ch;
  cout << prompt;
  ch = _getch();
  while(ch != 13) //character 13 is enter key
  {
    pass.push_back(ch);
    ch = _getch();
  }
  return (char *)pass.c_str();
}

#endif

#define MAX_RFID_CODE  12
#define MAX_BUFFER    255
#define MAX_PIN        16

请注意,这里有一个条件编译语句。这是因为 Windows 平台具有不同的从串行端口读取的机制。此外,Windows 平台没有原生的getpass()方法。

既然源文件的序言已经完成,让我们看看客户端插件的代码是怎样的。

客户端插件

使用类似于上述的st_mysql_plugin结构的特定结构来构建客户端认证插件。幸运的是,有一些宏可以用来简化创建过程。

客户端插件负责将用户凭证发送到服务器进行验证。对于 RFID 认证插件,这意味着提示用户刷卡,读取钥匙卡,要求用户输入 PIN,然后将连接的 RFID 代码和 PIN 发送到服务器。

让我们从看代码开始阅读键码。我们再次需要使用条件编译,因为在 Windows 上读取 COM 端口的代码与在 Linux 和 Mac OS X 上的代码有很大的不同。

清单 9-6 读取 RFID 码

/*
 * Read a RFID code from the serial port.
 */
#ifndef __WIN__
unsigned char *get_rfid_code(char *port)
{
  int fd;
  unsigned char *rfid_code= NULL;
  int nbytes;
  unsigned char raw_buff[MAX_BUFFER];
  unsigned char *bufptr = NULL;

  fd = open(port, O_RDWR | O_NOCTTY | O_NDELAY);
  if (fd ==1)
  {
    printf("Unable to open port: %s.\n", port);
    return NULL;
  }
  else
    fcntl(fd, F_SETFL, 0);

  bufptr = raw_buff;
  while ((nbytes = read(fd, bufptr, raw_buff + sizeof(raw_buff) - bufptr - 1)) > 0)
  {
    bufptr += nbytes;
    if (bufptr[−1] == '\n' || bufptr[−2] == '\n' || bufptr[−3] == '\n' ||
        bufptr[−1] == '\r' || bufptr[−2] == '\r' || bufptr[−3] == '\r' ||
        bufptr[−1] == 0x03 || bufptr[−2] == 0x03 || bufptr[−3] == 0x03)
    break;
  }
  *bufptr = '\0';

  rfid_code = (unsigned char *)strdup((char *)raw_buff);
  return rfid_code;
}

#else

unsigned char *get_rfid_code(char *port)
{
  HANDLE com_port;
  DWORD nbytes;
  unsigned char raw_buff[MAX_BUFFER];
  unsigned char *rfid_code= NULL;

    /* Open the port specified. */
    com_port = CreateFile(port, GENERIC_READ, 0, 0, OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL, 0);
    if (com_port == INVALID_HANDLE_VALUE)
    {
      int error = GetLastError();
      if (error == ERROR_FILE_NOT_FOUND)
      {
        printf("Unable to open port: %s.\n", port);
        return NULL;
      }
      printf("Error opening port: %s:%d.\n", port, error);
      return NULL;
    }

    /* Configure the port. */
    DCB com_config = {0};
    com_config.DCBlength = sizeof(com_config);
    if (!GetCommState(com_port, &com_config))
    {
      printf("Unable to get port state.\n");
      return NULL;
    }
    com_config.BaudRate = CBR_9600;
    com_config.ByteSize = 8;
    com_config.Parity = NOPARITY;
    com_config.StopBits = ONESTOPBIT;
    if (!SetCommState(com_port, &com_config))
    {
      printf("Unable to set port state.\n");
      return NULL;
    }

    /* Set timeouts. */
    COMMTIMEOUTS timeouts = {0};
    timeouts.ReadIntervalTimeout=50;
    timeouts.ReadTotalTimeoutConstant=50;
    timeouts.ReadTotalTimeoutMultiplier=10;
    if (!SetCommTimeouts(com_port, &timeouts))
    {
      printf("Cannot set timeouts for port.\n");
      return NULL;
    }

    /* Read from the port. */
    if (!ReadFile(com_port, raw_buff, MAX_BUFFER, &nbytes, NULL))
    {
      printf("Unable to read from the port.\n");
      return NULL;
    }

    /* Close the port. */
    CloseHandle(com_port);

  rfid_code = (unsigned char *)strdup((char *)raw_buff);
    return rfid_code;
}

#endif /* __WIN__ */

我把 Windows 部分的细节留给那些熟悉 Windows 编程的人,因为这段代码是经过充分验证的、经常重复的代码部分。

image 非 Windows 代码很容易写,但是它有一个特别有趣的小窍门。在几个平台上测试 RFID 阅读器时,我发现从阅读器返回的 RFID 代码可以包含许多不同模式的控制代码。因此,我必须编写代码来考虑我遇到的所有排列。您自己对平台的体验可能会导致类似的观察结果。

现在,让我们把注意力转向这个插件的核心方法。清单 9-7 显示了用来控制客户端插件的代码。这个方法被映射到客户端插件结构(如下所述),当客户端检测到服务器正在从 rfid_auth 插件请求数据时,就会调用这个方法。

清单 9-7 向服务器发送 RFID 代码

static int rfid_send(MYSQL_PLUGIN_VIO *vio, st_mysql *mysql)
{
  char *port= 0;
  char pass[MAX_PIN];
  int len, res;
  unsigned char buffer[MAX_BUFFER];
  unsigned char *raw_buff= NULL;
  int start= 0;

  /* Get the port to open. */
  port= getenv("MYSQL_RFID_PORT");
  if (!port)
  {
    printf("Environment variable not set.\n");
    return CR_ERROR;
  }

  printf("Please swipe your card now...\n");

  raw_buff = get_rfid_code(port);
  if (raw_buff == NULL)
  {
    printf("Cannot read RFID code.\n");
    return CR_ERROR;
  }
  len = strlen((char *)raw_buff);

  // Strip off leading extra bytes.
  for (int j= 0; j < 2; j++)
     if (raw_buff[j] == 0x02 || raw_buff[j] == 0x03)
       start++;

  strncpy((char *)buffer, (char *)raw_buff+start, len-start);
  len = strlen((char *)buffer);
  /* Check for valid read. */
  if (len >= MAX_RFID_CODE)
  {
    // Strip off extra bytes at end (CR, LF, etc.)
    buffer[MAX_RFID_CODE] = '\0';
        len = MAX_RFID_CODE;
  }
  else
  {
    printf("RFID code length error. Please try again.\n");
    return CR_ERROR;
  }

  strncpy(pass, getpass("Please enter your PIN: "), sizeof(pass));
  strcat((char *)buffer, pass);
  len = strlen((char *)buffer);

  res= vio->write_packet(vio, buffer, len);

  return res ? CR_ERROR : CR_OK;
}

上述方法从检查使用哪个端口从 RFID 读取器读取开始。我使用一个名为MYSQL_RFID_PORT 的环境变量,让用户指定要打开的端口的文本字符串。例子有 COM1,COM2,/dev/ttyUSB0等。可能还有其他更好的方式来指定端口,但这是最容易编码和部署的方式(只需将其添加到用户的登录脚本中)。

在包含端口的环境变量被读取后,该方法提示用户刷她的钥匙卡,并调用上面定义的 get_rfid_code()方法来读取 rfid 代码。包括一些简单的错误处理,以确保代码被正确读取(所有 12 个字符都可用)。一旦代码被读取,该方法提示用户输入她的 PIN,然后从标准输入(键盘)读取 PIN。

然后使用 vio 类方法write_packet() 将这些信息连接起来并发送给服务器。就这样!客户端身份验证插件已将控制权移交给服务器端插件来验证字符串。如果有效,write_packet()返回CR_OK,否则返回CR_ERROR。如果服务器端验证成功,客户端应用将接管与服务器的握手,并完成登录。

最后要讨论和编码的是客户端插件定义结构。有一些宏使这个定义变得更容易。清单 9-8 显示了前缀宏mysql_declare_client_pluginmysql_end_client_plugin 后缀宏的使用。该结构的内容以插件名、作者、插件描述、版本数组和许可证开始。接下来是指向 MySQL API(仅供内部使用)、初始化、反初始化和选项处理帮助器方法的函数指针。由于 RFID 认证插件相当简单,我们不使用这些方法中的任何一个,因此,我们将它们设置为 NULL。最后一项是函数指针,指向服务器从客户机请求验证数据时调用的方法。正如你所看到的,这就是上面描述的rfid_send()方法。

清单 9-8 定义 客户端插件

mysql_declare_client_plugin(AUTHENTICATION)
  "rfid_auth",
  "Chuck Bell",
  "RFID Authentication Plugin - Client",
  {0, 0, 1},
  "GPL",
  NULL,
  NULL,
  NULL,
  NULL,
  rfid_send
mysql_end_client_plugin;

image 注意你分配给客户端插件(结构中的第一个条目)的名字必须匹配服务器端插件的名字(见下文)。如果不匹配,您将会遇到一些来自客户端或服务器的异常错误消息。

如您所见,客户端插件并不复杂(除了从串口读取)。现在让我们看看服务器端插件代码。

服务器端插件

服务器端插件代码更简单。这是因为它只需验证从客户端收到的 RFID 代码。清单 9-9 显示了验证代码。

清单 9-9 验证 RFID 代码

/*
 * Server-side plugin
 */
static int rfid_auth_validate(MYSQL_PLUGIN_VIO *vio,
                     MYSQL_SERVER_AUTH_INFO *info)
{
  unsigned char *pkt;
  int pkt_len, err= CR_OK;

  if ((pkt_len= vio->read_packet(vio, &pkt)) < 0)
    return CR_ERROR;

  info->password_used= PASSWORD_USED_YES;

  if (strcmp((const char *) pkt, info->auth_string))
    return CR_ERROR;

  return err;
}

上面的代码只需要通过 vio->read_packet()类方法从客户端读取一个包,并将其与 mysql.user 表中存储的代码进行匹配。

服务器端定义也使用宏来定义结构。然而,它需要定义两种结构。我们有一个类似的结构来定义插件,但也有一个特殊的结构,插件处理程序结构,用来存储版本、文本字符串描述符和指向服务器端验证代码的函数指针(在一些文档中也称为身份验证方法)。处理程序结构也用于通过插件实用程序命令呈现关于插件的信息。清单 9-10 显示了用于定义服务器端插件的两种结构。

清单 9-10 定义服务器端插件

static struct st_mysql_auth rfid_auth_handler=
{
  MYSQL_AUTHENTICATION_INTERFACE_VERSION,
  "rfid_auth",
  rfid_auth_validate
};

mysql_declare_plugin(rfid_auth_plugin)
{
  MYSQL_AUTHENTICATION_PLUGIN,
  &rfid_auth_handler,
  "rfid_auth",
  "Chuck Bell",
  "RFID Authentication Plugin - Server",
  PLUGIN_LICENSE_GPL,
  NULL,
  NULL,
  0x0100,
  NULL,
  NULL,
  NULL,
  0,
}
mysql_declare_plugin_end;

第二个结构与用于定义任何服务器插件的结构相同。宏mysql_declare_pluginmysql_declare_plugin_end有助于简化代码。如您所见,它包含插件类型、处理程序结构的地址、插件名称、作者、描述字符串、许可证类型、指向初始化和取消初始化的函数指针、版本(十六进制)、指向状态变量的函数指针、系统变量、内部专用位置,最后是一组用于进一步描述插件功能的标志。有关此结构的更多详细信息,请参见在线参考手册。

image 注意你分配给服务器端插件(结构中的第一个条目)的名字必须匹配客户端插件的名字(见上)。如果没有,您将会遇到一些来自客户端或服务器的异常错误消息。

现在我们有了 RFID 认证插件的所有代码,我们可以编译插件并测试它。首先,我们来看最后一个文件——rfid_auth.ini文件。

rfid_auth.ini 文件

为了完成我们的插件,我们还需要创建用于 mysql_plugin 客户端应用的初始化文件。如果您不打算将插件与服务器的特殊版本捆绑在一起,或者从源代码树中启动 make install 命令,那么您不需要创建这个文件。清单 9-11 显示了文件的内容。

清单 9-11?? 文件 rfid_auth.ini

#
# Plugin configuration file. Place the following on a separate line:
#
# library binary file name (without .so or .dll)
# component_name
# [component_name] - additional components in plugin
#
librfid_auth
rfid_auth

既然源文件已经完成,让我们编译插件。

编译插件

编译插件甚至更容易。服务器代码的基本 cmake 文件包含所有需要的宏,以确保当从源代码树的根发出以下命令时,任何放置在/plugin 文件夹中的具有正确格式的 CMakeLists.txt 文件的插件都将被自动配置。

cmake .
make

没错。不需要特殊的、复杂的或令人费解的命令。只需创建一个文件夹,比如/plugin/rfid_auth,并将文件放入其中。当准备好编译时,导航到树的根并输入上面的命令。

继续编译插件,然后将其复制到服务器的插件目录中。如果您遇到错误,请返回并修复这些错误,直到插件代码编译时没有错误或警告。

RFID 认证在行动

在您匆忙购买 RFID 阅读器并开始编码之前,让我们来看看这个在真实服务器上执行的例子。回想一下,编译后,我们需要将插件(例如 rfid_auth.so 或 rfid_auth.dll)复制到服务器上与- plugin-dir 设置对应的位置。

服务器不需要重新启动,但是如果您试图复制一个现有的、已安装的插件,您可能会遇到服务器的一些不寻常的和潜在的破坏性行为。例如,在 Windows 上,服务器可能会崩溃,但在 Ubuntu 上,服务器不会受到影响。

对于认证插件,插件还必须放在客户端可以找到的位置(或者通过客户端应用的- plugin-dir 选项指定)。

一旦插件在正确的位置,我们必须去服务器安装插件,如下所示。

INSTALL PLUGIN rfid_auth SONAME 'rfid_auth.so';

该命令应该没有错误地返回。您可以通过对 INFORMATION_SCHEMA.plugins 视图发出一个查询来验证插件是否被加载,如清单 9-12 所示。注意插件的名称、类型、描述、作者和版本。将这些与上面代码中定义的进行比较。

清单 9-12 验证插件已安装

mysql> select * from information_schema.plugins where plugin_name like 'rfid%' \G
*************************** 1\. row ***************************
           PLUGIN_NAME: rfid_auth
        PLUGIN_VERSION: 1.0
         PLUGIN_STATUS: ACTIVE
           PLUGIN_TYPE: AUTHENTICATION
   PLUGIN_TYPE_VERSION: 1.0
        PLUGIN_LIBRARY: rfid_auth.so
PLUGIN_LIBRARY_VERSION: 1.4
         PLUGIN_AUTHOR: Chuck Bell
    PLUGIN_DESCRIPTION: RFID Authentication Plugin - Server
        PLUGIN_LICENSE: GPL
           LOAD_OPTION: ON
1 row in set (0.00 sec)

一旦我们知道插件已经安装,我们就可以创建与插件相关联的用户。下面显示了创建用户以使用 RFID 身份验证插件进行身份验证的示例。

CREATE USER 'test'@'localhost' IDENTIFIED WITH rfid_auth AS '51007BB754C91234';

现在,我们可以转到客户端,尝试使用插件登录。清单 9-13 显示用户试图登录服务器。注意使用- plugin-dir 选项来指定 RFID 身份验证插件的位置。还要注意客户端插件提示刷卡和输入 PIN。当提示我刷卡时,我只是将卡放在 RFID 阅读器 Shell 的顶部,直到我听到它发出一声正确读取的信号。这个过程用了不到两秒钟(我把我的卡放在桌子上,随时可以使用),输入 PIN 只是简单的输入一个四位数。

清单 9-13 登录 用 RFID 认证插件

cbell@ubuntu:$ ../client/mysql -utest -h 127.0.0.1 --port=13000 --plugin-dir=../lib/plugin
Please swipe your card now...
Please enter your PIN:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.6.6-m9 Source distribution

Copyright (c) 2000, 2012, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

您可能想知道是否只有 mysql 客户端应用启用了客户端插件。好消息是,所有提供的客户端应用都支持插件,可以与身份验证插件一起使用。清单 9-14 展示了一个使用认证插件的 mysqladmin 客户端应用的例子。

清单 9-14 使用 mysqladmin 客户端应用登录

cbell@ubuntu:$ mysqladmin processlist -utest -h 127.0.0.1 --port=13000 --plugin-dir=../lib/plugin
Please swipe your card now...
Please enter your PIN:
+−−--+−−----+−−---------------+−−--+−−-------+−−----+−−-----+−−----------------+
| Id | User | Host            | db | Command | Time | State | Info             |
+−−--+−−----+−−---------------+−−--+−−-------+−−----+−−-----+−−----------------+
| 3  | test | localhost:46374 |    | Query   | 0    | init  | show processlist |
+−−--+−−----+−−---------------+−−--+−−-------+−−----+−−-----+−−----------------+

image 注意使用认证插件不会违反或绕过 MySQL 服务器中的用户安全层。

如您所见,使用身份验证插件改变了用户登录服务器的方式。在这个例子中,我创建了一种登录服务器的独特方法,它要求使用特殊的钥匙卡,并且不使用密码(PIN 码除外)。

在下一节中,我将提出一些改进和强化插件的建议,以便在生产环境中使用,或者创建您自己的基于专业 RFID 阅读器的认证机制。

进一步的工作

如果您发现 RFID 身份验证插件是构建您自己的更安全的用户登录机制的一个有价值的模型,您可以在许多方面改进这个解决方案。您可以更改 PIN 的大小,使用 validate_password 插件 来帮助创建更安全的 PIN,或者添加额外的硬件元素,例如客户端工作站的 MAC 地址。在这种情况下,MAC 地址将进一步限制用户使用特定的钥匙卡和从特定机器读取的匹配 PIN 进行登录。

也许最好也是最安全的选择是使用 SHA1 或 MD%算法对 mysql.plugin 表 中存储的 keycode 进行加密。客户端插件必须使用相同的种子来形成加密字符串,所以使用 MySQL 方法可能会有问题。然而,一个简单的散列或者甚至是一个加扰应该足以保护代码不被意外发现。即使这样,代码的使用也是有限的,因为入侵者必须构建一个客户端插件的复制品,我希望你同意,这超出了一般用户的技能。

另一种选择是使用 RFID 代码本身作为种子,并加密一个已知的短语。我建议使用随机字节流,这样有人读取代码时——或者如果一个极端持久的窥探者试图读取插件的二进制代码——就不会发现明文形式的密码。无论哪种方式,对存储在 mysql.user 表中的内容进行加密都应该被认为是生产使用的一项重要要求。

除了保护 RFID 阅读器本身之外,您还可以结合生物特征元素,例如指纹阅读器。实现这样一个设备可能需要更多的编程,但如果你正在寻找一个高度安全的解决方案,生物识别设备将完成你的追求。

摘要

你可能会想,“哇,这太难理解了。”这可能是真的,但是一旦您使用了代码并看到了它的运行,我希望您将会看到这是一个如何构建认证插件的好例子,您可以使用它作为您自己的认证插件的样板。

在这一章中,我讨论了 MySQL 服务器最重要的特性——在不停止或重新配置服务器的情况下添加新特性的能力。向您介绍了可用的插件类型,以及它们如何构成服务器功能未来扩展的基础。然后,我研究了 MySQL 插件的架构以及它们是如何安装和卸载的,并演示了如何通过使用 RFID 阅读器创建认证插件来创建插件。

下一章将探索最复杂的插件类型,MySQL 存储引擎。您将看到如何创建自己的存储引擎。您应该对扩展 MySQL 系统以满足您的需求的容易程度印象深刻。仅嵌入式服务器库一项就开启了广阔的可能性领域。再加上在 MySQL 中创建自己的存储引擎甚至(后来)自己的函数的能力,很容易理解为什么 MySQL 是“世界上最受欢迎的开源数据库”

1UDF 先于插件架构,最早出现在 3.21.24 版本。它们没有被改变以使用新的体系结构。

我将描述一些使这个例子更加安全的修改。

3

虽然仍有可能丢失钥匙卡,但拥有一张可以避免更频繁的忘记密码事件。

5 虽然 USB 端口被定义为串行连接,但大多数人将带有九个或更多引脚的老式端口称为“串行端口”我指的就是这种类型的港口。

6 这些指令在 cmake/plugin.cmake 文件中定义。*

十、构建您自己的存储引擎

MySQL 可插拔架构支持使用多个存储引擎,这是 MySQL 系统最重要的特性之一。许多数据库专业人员已经改进了调整关系数据库系统的逻辑结构的高级技能,以满足数据及其应用的需要。使用 MySQL,数据库专业人员还可以通过选择最佳存储方法来优化数据库的访问方法,从而优化数据库系统的物理层。与只使用单一存储机制的关系数据库系统相比,这是一个巨大的优势。 1

本章将指导您完成创建自己的存储引擎的过程。我首先详细解释构建存储引擎插件的细节,然后带您浏览构建示例存储引擎的教程。如果你一直渴望得到 MySQL 源代码,并让它做一些真正酷的事情,现在是时候卷起袖子重新灌满饮料了。如果你对做这种修改有点担心,请通读这一章并按照例子来做,直到你对这个过程感到满意为止。

MySQL 存储引擎概述

存储引擎插件是 MySQL 服务器架构中的一个软件层。它负责将物理数据层从服务器的逻辑层中抽象出来,并为服务器提供底层输入/输出(I/O)操作。当一个系统在分层架构中开发时,它提供了一种机制来简化和标准化各层之间的接口(??)。这个质量衡量了分层架构的成功。分层体系结构的一个强大特性是能够修改一个层,并且如果接口没有改变,也不会改变相邻的层。

Oracle 重新设计了 MySQL 的架构(从版本 5.0 开始),以纳入这种分层架构方法。插件架构是在版本 5.1 中添加的,可插拔存储引擎是这一努力最明显的形式。存储引擎插件使系统集成商和开发者能够在数据读写需要特殊处理的环境中使用 MySQL。此外,插件架构允许您创建自己的存储引擎。

这样做而不是将数据转换成 MySQL 可以接受的格式的一个原因是转换的成本。例如,假设您有一个您的组织已经使用了很长时间的遗留应用。应用使用的数据对您的组织来说非常有价值,不可复制。此外,您可能需要使用旧的应用。您可以创建一个能够以旧格式读写数据的存储引擎,而不是将数据转换为新格式。其他示例包括数据及其访问方法需要特殊的数据处理以确保最有效地读写数据的情况。

此外,也许是最重要的,存储引擎插件可以连接通常不与数据库系统连接的数据。也就是说,您可以创建存储引擎来读取流数据(例如 RSS)或其他非传统的、非磁盘存储的数据。无论您需要什么,MySQL 都可以满足您的需求,它允许您创建自己的存储引擎,使您能够为自己的环境创建高效、专用的关系数据库系统。

您可以使用 MySQL 服务器作为您的关系数据库处理引擎,并通过提供一个直接插入服务器的特殊存储引擎,将其直接连接到您的遗留数据。这听起来不像是一件容易的事情,但它确实是。

最重要的架构元素是使用单个对象的数组来访问存储引擎(每个存储引擎一个对象)。这些单个对象的控制是以一种叫做 handlerton 的复杂结构的形式出现的(就像在 singleton 中一样——参见关于 singletons 的侧栏)。称为 handler 的特殊类是一个基类,它使用 handlerton 来完成接口,并提供基本的连接来启用存储引擎。我将在本章后面的图 10-1 中演示这一点。

9781430246596_Fig10-01.jpg

图 10-1。可插拔存储引擎类派生

所有存储引擎都派生自 base-handler 类,该类充当警察,将常见的访问方法和函数调用封送到存储引擎,以及从存储引擎封送到服务器。换句话说,处理程序和 handlerton 结构充当存储引擎和服务器之间的中介(或黑盒)。只要您的存储引擎符合处理程序接口和可插拔架构,您就可以将其插入服务器。所有的连接、认证、解析和优化仍然由服务器以通常的方式执行。不同的存储引擎只是以一种通用的格式将数据传入和传出服务器,并在专用存储介质之间进行转换。

Oracle 很好地记录了创建新存储引擎的过程。在撰写本文时,《MySQL 参考手册》的第 14 章包含了对存储引擎以及处理程序接口所支持和要求的所有功能的完整解释。我建议您在阅读完本章并构建了示例存储引擎之后,再阅读 MySQL 参考手册。在这种情况下,MySQL 参考手册最好用作参考。

什么是独生子女?

在面向对象编程的某些情况下,您可能需要限制对象创建,以便对于给定的类只进行一次对象实例化。原因之一可能是该类保护一组共享的操作或数据。例如,如果有一个 manager 类被设计为访问特定资源或数据的看门人,您可能会尝试创建一个对该对象的静态或全局引用,因此在整个应用中只允许一个实例。然而,使用全局实例和常量结构或访问函数违背了面向对象的原则。您可以不这样做,而是创建一个特殊形式的对象,将创建限制为仅创建一个实例,以便它可以由应用中的所有区域(对象)共享。这些特殊的、一次性创建的对象被称为单件。(有关单件的更多信息,请参见http://www.codeproject.com/Articles/1921/Singleton-Pattern-its-implementation-with-C)。创建单件有多种方法:

  • 静态变量
  • 堆注册
  • 运行时类型信息(RTTI)
  • 自动注册
  • 智能单件(像智能指针)

现在你知道什么是单身了,你可能在想你在整个职业生涯中一直在创造这些,但却不知道!

存储引擎开发流程

开发新存储引擎的过程可以描述为一系列阶段。毕竟,存储引擎不仅仅由几行代码组成;因此,开发这种规模和复杂性的东西的最自然的方式是通过迭代过程,在转移到另一个更复杂的部分之前,开发和测试系统的一小部分。在接下来的教程中,我将从最基本的功能开始,逐步添加功能,直到出现一个功能完整的存储引擎。

前几个阶段创建并添加基本的数据读写机制。后期阶段添加索引和事务支持。根据您要向自己的存储引擎添加的功能,您可能不需要完成所有阶段。一个正常运行的存储引擎至少应该支持前四个阶段中定义的功能。 2 这些阶段是:

  1. 停止引擎—该过程的第一步是创建可插入服务器的基本存储引擎。创建基本的源代码文件,将存储引擎建立为处理程序基类的派生,并将存储引擎本身插入服务器源代码。
  2. 使用表格—如果存储引擎没有创建、打开、关闭和删除文件的方法,它就不会很有趣。在此阶段,您将设置基本的文件处理例程,并确定引擎正在正确处理文件。
  3. 读写数据—要完成最基本的存储引擎,您必须实现读写方法,以便从存储介质中读写数据。 3 这个阶段是您添加这些方法来读取存储介质格式的数据并将其转换为 MySQL 内部数据格式的阶段。同样,将数据从 MySQL 内部数据格式写到存储介质中。
  4. 更新和删除数据—要使存储引擎能够在应用中使用,您还必须实现那些允许更改存储引擎中数据的方法。这个阶段是实现数据更新和删除的解决方案的阶段。
  5. 索引数据—一个全功能的存储引擎还应该包括允许快速随机读取和范围查询的能力。在这一阶段,您将实现文件访问方法中第二复杂的操作—索引。我已经提供了一个索引类,可以让您更容易地自己探索这一步。
  6. 添加事务支持—该过程的最后一个阶段是向存储引擎添加事务支持。在这个阶段,存储引擎变成了一个真正的关系数据库存储机制,适合在事务环境中使用。这是文件访问方法中最复杂的操作。

在整个过程中,您应该在每个阶段进行测试和调试。在接下来的小节中,我将向您展示调试存储引擎和编写测试来测试各个阶段的例子。所有正常的调试和跟踪机制都可以在存储引擎中使用。您还可以使用交互式调试器来查看运行中的代码!

需要源文件

您将使用的源文件通常被创建为一个单独的代码(或类)文件和一个头文件。这些文件分别被命名为ha_<engine name>.ccha_<engine name>.h4 存储引擎源代码位于主源代码树的storage目录下。该文件夹中是各种存储引擎的源代码文件。除了这两个文件,这就是你需要开始!

意外的帮助

MySQL 参考手册提到了几个源代码文件,它们对学习存储引擎很有帮助。事实上,我在这里所包含的大部分内容都来自于对这些资源的研究。Oracle 提供了一个示例存储引擎(名为example),它为在第 1 阶段创建存储引擎提供了一个很好的起点。事实上,我用它来帮助你开始学习教程。

归档引擎是第三阶段引擎的一个例子,它提供了读写数据的好例子。如果您想查看更多关于如何读取、写入和更新文件的示例,CSV 引擎是一个不错的选择。CSV 引擎是第 4 阶段引擎的一个例子(CSV 可以读写数据,也可以更新和删除数据)。CSV 引擎不同于命名约定,因为它是最先实现的引擎之一。源文件被命名为ha_tina.ccha_tina.h。最后,要查看 Stage 5 和 Stage 6 存储引擎的示例,请查看 MyISAM 和 InnoDB 存储引擎。

在开始创建自己的存储引擎之前,请花点时间专门研究一下这些存储引擎。因为源代码中嵌入了一些关于存储引擎应该如何工作的宝贵建议和指导。有时学习、扩展或模拟一个系统的最好方法是检查它的内部工作方式。

手柄按钮

正如我前面提到的,所有存储引擎的标准接口是 handlerton 结构。它是在sql目录下的handler.cchandler.h文件中实现的,它使用许多其他结构来组织支持插件接口和抽象接口所需的所有元素。

您可能想知道在这种机制中如何确保并发性。答案是——另一种结构!每个存储引擎负责创建一个共享结构,该结构从所有线程中处理程序的每个实例引用。自然,这意味着一些代码必须受到保护。好消息是,不仅有互斥(mutex)保护方法可用,而且 handlerton 源代码已经被设计为最小化对这些保护的需求。

handlerton 结构是一个很大的结构,有许多数据项和方法。数据项被表示为它们在结构中定义的普通数据类型,但是方法是使用函数指针实现的。函数指针的使用是高级开发者用来允许运行时多态性的那些巧妙构造的机制之一。可以使用函数指针将执行重定向到不同的(但等效的接口)函数。这是 handlerton 如此成功的技术之一。

清单 10-1 是 handlerton 结构定义的简略清单,表 10-1 包括了对更重要元素的描述。

清单 10-1。MySQL 的 Handlerton 结构

struct handlerton
{
  SHOW_COMP_OPTION state;
  enum legacy_db_type db_type;
   uint slot;
   uint savepoint_offset;
   int  (*close_connection)(handlerton *hton, THD *thd);
   int  (*savepoint_set)(handlerton *hton, THD *thd, void *sv);
   int  (*savepoint_rollback)(handlerton *hton, THD *thd, void *sv);
   int  (*savepoint_release)(handlerton *hton, THD *thd, void *sv);
   int  (*commit)(handlerton *hton, THD *thd, bool all);
   int  (*rollback)(handlerton *hton, THD *thd, bool all);
   int  (*prepare)(handlerton *hton, THD *thd, bool all);
   int  (*recover)(handlerton *hton, XID *xid_list, uint len);
   int  (*commit_by_xid)(handlerton *hton, XID *xid);
   int  (*rollback_by_xid)(handlerton *hton, XID *xid);
   void *(*create_cursor_read_view)(handlerton *hton, THD *thd);
   void (*set_cursor_read_view)(handlerton *hton, THD *thd, void *read_view);
   void (*close_cursor_read_view)(handlerton *hton, THD *thd, void *read_view);
   handler *(*create)(handlerton *hton, TABLE_SHARE *table, MEM_ROOT *mem_root);
   void (*drop_database)(handlerton *hton, char* path);
   int (*panic)(handlerton *hton, enum ha_panic_function flag);
   int (*start_consistent_snapshot)(handlerton *hton, THD *thd);
   bool (*flush_logs)(handlerton *hton);
   bool (*show_status)(handlerton *hton, THD *thd, stat_print_fn *print, enum ha_stat_type stat);
   uint (*partition_flags)();
   uint (*alter_table_flags)(uint flags);
   int (*alter_tablespace)(handlerton *hton, THD *thd, st_alter_tablespace *ts_info);
   int (*fill_is_table)(handlerton *hton, THD *thd, TABLE_LIST *tables,
                        class Item *cond,
                        enum enum_schema_tables);
   uint32 flags;                                /* global handler flags */
   int (*binlog_func)(handlerton *hton, THD *thd, enum_binlog_func fn, void *arg);
   void (*binlog_log_query)(handlerton *hton, THD *thd,
                            enum_binlog_command binlog_command,
                            const char *query, uint query_length,
                            const char *db, const char *table_name);
   int (*release_temporary_latches)(handlerton *hton, THD *thd);
   enum log_status (*get_log_status)(handlerton *hton, char *log);
   enum handler_create_iterator_result
     (*create_iterator)(handlerton *hton, enum handler_iterator_type type,
                        struct handler_iterator *fill_this_in);
   int (*discover)(handlerton *hton, THD* thd, const char *db,
                   const char *name,
                   uchar **frmblob,
                   size_t *frmlen);
   int (*find_files)(handlerton *hton, THD *thd,
                     const char *db,
                     const char *path,
                     const char *wild, bool dir, List<LEX_STRING> *files);
   int (*table_exists_in_engine)(handlerton *hton, THD* thd, const char *db,
                                 const char *name);
   int (*make_pushed_join)(handlerton *hton, THD* thd,
                           const AQP::Join_plan* plan);
  const char* (*system_database)();
  bool (*is_supported_system_table)(const char *db,
                                    const char *table_name,
                                    bool is_sql_layer_system_table);

   uint32 license; /* Flag for Engine License */
   void *data; /* Location for engines to keep personal structures */
};

表 10-1 。handler ton-结构定义

元素描述
显示 _ 组件 _ 选项状态确定存储引擎是否可用。
const char *comment描述存储引擎的注释,也由 SHOW 命令返回。
枚举传统数据库类型数据库类型保存在中的枚举值。指示哪个存储引擎创建了该文件的 frm 文件。该值用于确定与表相关联的处理程序类。
uint 插槽处理程序数组中引用此句柄的位置。
uint savepoint_offset为存储引擎创建保存点所需的内存大小。
int (*close_connection)(。。。)用于关闭连接的方法。
int (*savepoint_set)(。。。)将保存点设置为 savepoint_offset 元素中指定的保存点偏移量的方法。
int (*savepoint_rollback)(。。。)回滚(撤消)保存点的方法。
int(*savepoint_release)(。。。)释放(忽略)保存点的方法。
int(*commit)(。。。)提交挂起事务的提交方法。
int(*rollback)(。。。)回滚挂起事务的回滚方法。
int(*prepare)(。。。)为提交准备事务准备方法。
int(*recover)(。。。)返回正在准备的事务列表的方法。
int(*commit_by_xid)(。。。)通过事务 ID 提交事务的方法。
int(*rollback_by_xid)(。。。)按事务 ID 回滚事务的方法。
void *(*create_cursor_read_view)()用于创建光标的方法。
void(* set _ 游标 _read_view)(void *)用于切换到特定光标视图的方法。
void(* close _ cursor _ read _ view)(void *)用于关闭特定光标视图的方法。
处理程序*(创建)(表共享*表)用于创建此存储引擎的处理程序实例的方法。
int (*panic)(枚举 ha_panic_function 标志)在服务器关闭和崩溃期间调用的方法。
int(*启动一致快照)(…)为开始一致读取(并发)而调用的方法。
bool (*flush_logs)()用于将日志刷新到磁盘的方法。
布尔 (*show_status)(. ..返回存储引擎状态信息的方法。
uint (*partition_flags)()用于返回分区标志的方法。
uint (*alter_table_flags)(。。。)用于返回 ALTER TABLE 命令的标志集的方法。
int (*alter_tablespace)()。.)用于返回 ALTER TABLESPACE 命令的标志集的方法。
int (*fill_is_table)(。。。)服务器机制用来填充信息模式视图(表)的方法。
uint32 标志指示处理程序支持哪些功能的标志。
int (*binlog_func)()。。。)回调二进制对数函数的方法。
void (*binlog_log_query)(。。。)用于查询二进制日志的方法。
int (*release_temporary_latches)(。。。)InnoDB 特定用途(参见 InnoDB 引擎的文档)。

image 注意为了节省空间,我省略了代码中的注释。为了简洁,我还跳过了结构中不太重要的项目。有关 handlerton 结构的更多信息,请参见handler.h文件。

处理程序类

理解存储引擎插件接口的另一部分是handler类。handler类源自Sql_alloc ,这意味着所有的内存分配例程都是通过继承提供的。handler类被设计成存储处理程序的实现。它通过 handlerton 结构提供了一组与服务器接口的一致方法。handlerton 和 handler 实例作为一个单元工作,以实现存储引擎体系结构的抽象层。图 10-1 描述了这些类以及它们是如何被派生出来形成一个新的存储引擎的。该图将 handlerton 结构显示为处理程序和新存储引擎之间的接口。

handler类的完整详细的研究超出了本书的范围。相反,我展示了实现样本存储引擎的最重要和最常用的方法。我将在本章的后面以更加叙述性的格式解释每一个实现和调用的方法。

作为对handler类的介绍,我提供了清单 10-2handler类定义的摘录。现在花点时间浏览一下课程。请注意,有许多方法可用于各种各样的任务,例如创建、删除、修改表,以及操作字段和索引的方法。甚至还有崩溃保护、恢复和备份的方法。

尽管handler类令人印象深刻,并且涵盖了存储引擎的所有可能情况,但是大多数存储引擎并不使用完整的方法列表。如果您想实现一个具有一些高级特性的存储引擎,请花些时间探索 MySQL 参考手册中对handler类的精彩介绍。一旦您习惯了创建存储引擎,您就可以使用参考手册将您的存储引擎提升到更高的水平。

清单 10-2。Handler-class 定义

class handler :public Sql_alloc
{
...
  const handlerton *ht;                 /* storage engine of this handler */
  uchar *ref;                            /* Pointer to current row */
  uchar *dupp_ref;                       /* Pointer to dupp row */
...

  handler(const handlerton *ht_arg, TABLE_SHARE *share_arg)
    :table_share(share_arg), ht(ht_arg),
    ref(0), data_file_length(0), max_data_file_length(0), index_file_length(0),
    delete_length(0), auto_increment_value(0),
    records(0), deleted(0), mean_rec_length(0),
    create_time(0), check_time(0), update_time(0),
    key_used_on_scan(MAX_KEY), active_index(MAX_KEY),
    ref_length(sizeof(my_off_t)), block_size(0),
    ft_handler(0), inited(NONE), implicit_emptied(0),
    pushed_cond(NULL)
    {}
...
  int ha_index_init(uint idx, bool sorted)
...
  int ha_index_end()
...
  int ha_rnd_init(bool scan)
...
  int ha_rnd_end()
...
  int ha_reset()
...
...
  virtual int exec_bulk_update(uint *dup_key_found)
...
  virtual void end_bulk_update() { return; }
...
  virtual int end_bulk_delete()
...
  virtual int index_read(uchar * buf, const uchar * key,
       uint key_len, enum ha_rkey_function find_flag)
...
  virtual int index_read_idx(uchar * buf, uint index, const uchar * key,
           uint key_len, enum ha_rkey_function find_flag);
  virtual int index_next(uchar * buf)
   { return  HA_ERR_WRONG_COMMAND; }
  virtual int index_prev(uchar * buf)
   { return  HA_ERR_WRONG_COMMAND; }
  virtual int index_first(uchar * buf)
   { return  HA_ERR_WRONG_COMMAND; }
  virtual int index_last(uchar * buf)
   { return  HA_ERR_WRONG_COMMAND; }
  virtual int index_next_same(uchar *buf, const uchar *key, uint keylen);
  virtual int index_read_last(uchar * buf, const uchar * key, uint key_len)
...

virtual int read_range_first(const key_range *start_key,
                               const key_range *end_key,
                               bool eq_range, bool sorted);
  virtual int read_range_next();
  int compare_key(key_range *range);
  virtual int ft_init() { return HA_ERR_WRONG_COMMAND; }
  void ft_end() { ft_handler=NULL; }
  virtual FT_INFO *ft_init_ext(uint flags, uint inx,String *key)
    { return NULL; }
  virtual int ft_read(uchar *buf) { return HA_ERR_WRONG_COMMAND; }
  virtual int rnd_next(uchar *buf)=0;
  virtual int rnd_pos(uchar * buf, uchar *pos)=0;
  virtual int read_first_row(uchar *buf, uint primary_key);
...
  virtual int restart_rnd_next(uchar *buf, uchar *pos)
    { return HA_ERR_WRONG_COMMAND; }
  virtual int rnd_same(uchar *buf, uint inx)
    { return HA_ERR_WRONG_COMMAND; }
  virtual ha_rows records_in_range(uint inx, key_range *min_key,
                                   key_range *max_key);
    { return (ha_rows) 10; }
  virtual void position(const uchar *record)=0;
  virtual void info(uint)=0; // see my_base.h for full description
  virtual void get_dynamic_partition_info(PARTITION_INFO *stat_info,
                                          uint part_id);
  virtual int extra(enum ha_extra_function operation)
  { return 0; }
  virtual int extra_opt(enum ha_extra_function operation, ulong cache_size)
  { return extra(operation); }
...
  virtual int delete_all_rows()
...
  virtual ulonglong get_auto_increment();
  virtual void restore_auto_increment();
...
  virtual int reset_auto_increment(ulonglong value)
...
  virtual void update_create_info(HA_CREATE_INFO *create_info) {}
...
  int ha_repair(THD* thd, HA_CHECK_OPT* check_opt);
...
  virtual bool check_and_repair(THD *thd) { return TRUE; }
  virtual int dump(THD* thd, int fd =1) { return HA_ERR_WRONG_COMMAND; }
  virtual int disable_indexes(uint mode) { return HA_ERR_WRONG_COMMAND; }
  virtual int enable_indexes(uint mode) { return HA_ERR_WRONG_COMMAND; }
  virtual int indexes_are_disabled(void) {return 0;}
  virtual void start_bulk_insert(ha_rows rows) {}
  virtual int end_bulk_insert() {return 0; }
  virtual int discard_or_import_tablespace(my_bool discard)
...
  virtual uint referenced_by_foreign_key() { return 0;}
  virtual void init_table_handle_for_HANDLER()
...
  virtual void free_foreign_key_create_info(char* str) {}
...
  virtual const char *table_type() const =0;
  virtual const char **bas_ext() const =0;
...
  virtual uint max_supported_record_length() const { return HA_MAX_REC_LENGTH; }
  virtual uint max_supported_keys() const { return 0; }
  virtual uint max_supported_key_parts() const { return MAX_REF_PARTS; }
  virtual uint max_supported_key_length() const { return MAX_KEY_LENGTH; }
  virtual uint max_supported_key_part_length() const { return 255; }
  virtual uint min_record_length(uint options) const { return 1; }
...
  virtual bool is_crashed() const  { return 0; }
...
  virtual int rename_table(const char *from, const char *to);
  virtual int delete_table(const char *name);
  virtual void drop_table(const char *name);

  virtual int create(const char *name, TABLE *form, HA_CREATE_INFO *info)=0;
...
  virtual int external_lock(THD *thd __attribute__((unused)),
                            int lock_type __attribute__((unused)))
...
  virtual int write_row(uchar *buf __attribute__((unused)))
...
  virtual int update_row(const uchar *old_data __attribute__((unused)),
                         uchar *new_data __attribute__((unused)))
...
  virtual int delete_row(const uchar *buf __attribute__((unused)))
...
};

MySQL 存储引擎简介

观看处理程序工作的最佳方式是观看它的运行。因此,在我们开始构建一个存储引擎之前,让我们检查一个正在使用的真实存储引擎。如果您还没有编译您的服务器,请继续使用 debug 进行编译。继续启动您的服务器和调试器,然后将您的调试工具连接到正在运行的服务器,如第 5 章中的所述。

我想向您展示一个运行中的简单存储引擎。在这种情况下,我使用归档存储引擎。在调试器打开且服务器运行的情况下,打开ha_archive.cc文件,并在方法的第一个可执行行上放置一个断点:

int ha_archive::create(...)
static ARCHIVE_SHARE *ha_archive::get_share(...)
int ha_archive::write_row(...)int ha_tina::rnd_next(...)
int ha_archive::rnd_next(...)

一旦设置了断点,启动命令行 MySQL 客户端,切换到测试数据库,并发出以下命令:

CREATE TABLE testarc (a int, b varchar(20), c int) ENGINE=ARCHIVE;

您应该立即看到在create()方法中调试器停止。这个方法是创建基本数据表的地方。事实上,这是首先要执行的事情之一。调用my_create()方法来创建文件。注意,代码正在寻找一个设置了(在方法的顶部)的AUTO_INCREMENT_FLAG字段;如果找到该字段,代码将设置一个错误并退出。这是因为归档存储引擎不支持自动递增字段。您还可以看到,该方法正在创建一个元文件,并检查压缩例程是否正常工作。

单步执行代码并观察迭代器。您可以在任何时候继续执行,或者,如果您真的很好奇,继续一步一步地返回到调用函数。

现在,让我们看看当我们插入数据时会发生什么。回到您的 MySQL 客户端,输入以下命令:

INSERT INTO testarc VALUES (10, "test", -1);

这一次,代码在get_share()方法中停止。此方法负责创建共享结构(存储为。frm 文件)用于存档处理程序的所有实例。当您逐步执行此方法时,您可以看到代码在何处设置全局变量和其他初始化类型的任务。继续,让调试器继续执行。

代码下一个停止的地方是在write_row()方法中。这个方法是将通过buf参数传递的数据写入磁盘的地方。记录缓冲区(uchar *buf)是 MySQL 用来在系统中传递行的机制。它是一个包含行数据和其他元数据的二进制缓冲区。这就是 MySQL 文档中提到的“内部格式”当您一步一步地阅读这段代码时,您会看到引擎设置了一些统计数据,做了一些错误检查,并最终使用方法末尾的方法real_write_row()写入数据。继续并逐步完成该方法。

real_write_row()方法中,可以看到另一个字段迭代器。这个迭代器遍历二进制大对象(BLOB)字段,并使用压缩方法将它们写入磁盘。如果您需要支持 BLOB 字段,这是一个很好的例子——只需用低级 IO 调用代替压缩方法。继续,让代码继续;然后返回 MySQL 客户端,输入命令:

SELECT * FROM testarc;

代码下一个停止的地方是在rnd_next()方法中。这是处理程序读取数据文件并将数据返回到记录缓冲区(uchar *buf)的地方。再次注意,代码设置了一些统计数据,进行错误检查,然后使用get_row()方法读取数据。单步执行这段代码,然后让它继续。

真是个惊喜!代码在rnd_next()方法处再次停止。这是因为rnd_next()方法是对表扫描的一系列调用之一。该方法不仅负责读取数据,还负责检测文件的结尾。因此,在您正在处理的示例中,应该有对该方法的两次调用。第一个检索第一行数据,第二个检测文件的结尾(您只插入了一行)。下面列出了表格扫描的典型调用序列,使用了您一直在研究的例子:

ha_spartan::info
ha_spartan::rnd_init
ha_spartan::extra
ha_spartan::rnd_next
ha_spartan::rnd_next
ha_spartan::extra

+−−----+−−----+−−----+
| a    | b    | c    |
+−−----+−−----+−−----+
| 10   | test | -1   |
+−−----+−−----+−−----+
1 row in set (26.25 sec)

image 注意查询返回的时间是服务器记录的实际运行时间,而不是执行时间。因此,花费在调试上的时间也很重要。

花些时间在你可能感兴趣的其他方法上设置断点。您还可以花一些时间阅读这个存储引擎中的注释,因为它们为如何使用一些处理程序方法提供了很好的线索。

斯巴达存储引擎

我为存储引擎教程选择了基本存储引擎的概念,它具有普通存储引擎的所有特性。这包括在索引支持下读写数据。也就是说是 5 级发动机。我将这个示例存储引擎称为 Spartan 存储引擎,因为在许多方面,它只实现了可行的数据库存储机制的基本需求。

我将指导您使用示例(ha_example ) MySQL 存储引擎构建 Spartan 存储的过程。在本教程中,我建议您参考其他存储引擎来了解更多信息。虽然你可能会发现你认为可以改进的地方(确实有几个),但在你成功实现第五阶段之前,不要对 Spartan 引擎进行任何改进。

让我们从检查 Spartan 存储引擎的支持类文件开始。

低级输入/输出类

存储引擎旨在使用一种专门的机制来读写数据,这种机制为用户提供了一些独特的好处。这意味着存储引擎本质上不支持相同的功能。

大多数存储引擎要么使用其他源文件中定义的 C 函数,要么使用类头文件和源文件中定义的 C++类。对于 Spartan 引擎,我选择使用后一种方法。我创建了一个数据文件类和一个索引文件类。忠于本章和 Spartan-engine 项目的意图,这两个类都没有针对性能进行优化。相反,它们提供了一种创建工作存储引擎的方法,并演示了创建自己的存储引擎需要做的大部分事情。

本节在概述中描述了每个类。您可以跟随代码,看看这些类是如何工作的。尽管低级类只是基础,可能需要一些微调,但我认为您会发现这些类非常有用,甚至可能会将它们作为您自己的存储引擎 I/O 的基础。

斯巴达 _ 数据类

Spartan 存储引擎的主要低级 I/O 类是Spartan_data类。这个类负责封装 Spartan 存储引擎的数据。清单 10-3 包含了这个类的完整头文件。正如您在标题中看到的,这个类的方法非常简单。我只实现了基本的打开、关闭、读取和写入操作。

清单 10-3。 斯巴达 _ 数据类头

/*
  Spartan_data.h

  This header defines a simple data file class for writing and reading raw
  data to and from disk. The data written is in uchar format so it can be
  anything you want it to be. The write_row and read_row accept the
  length of the data item to be written/read.
*/
#include "my_global.h"
#include "my_sys.h"

class Spartan_data
{
public:
  Spartan_data(void);
  ∼Spartan_data(void);
  int create_table(char *path);
  int open_table(char *path);
  long long write_row(uchar *buf, int length);
  long long update_row(uchar *old_rec, uchar *new_rec,
                       int length, long long position);
  int read_row(uchar *buf, int length, long long position);
  int delete_row(uchar *old_rec, int length, long long position);
  int close_table();
  long long cur_position();
  int records();
  int del_records();
  int trunc_table();
  int row_size(int length);
private:
  File data_file;
  int header_size;
  int record_header_size;
  bool crashed;
  int number_records;
  int number_del_records;
  int read_header();
  int write_header();
};

清单 10-4 包含了 Spartan 存储引擎数据类的完整源代码。请注意,在代码中,我包含了适当的DBUG调用,以确保我的源代码可以写入跟踪文件,如果我希望使用--with-debug开关调试系统的话。还要注意,使用的读写方法是 Oracle 提供的my_xxx平台安全实用方法。

清单 10-4。 斯巴达 _ 数据类源代码

/*
  Spartan_data.cc

  This class implements a simple data file reader/writer. It
  is designed to allow the caller to specify the size of the
  data to read or write. This allows for variable length records
  and the inclusion of extra fields (like blobs). The data are
  stored in an uncompressed, unoptimized fashion.
*/
#include "spartan_data.h"
#include <my_dir.h>
#include <string.h>

Spartan_data::Spartan_data(void)
{
  data_file =1;
  number_records =1;
  number_del_records =1;
  header_size = sizeof(bool) + sizeof(int) + sizeof(int);
  record_header_size = sizeof(uchar) + sizeof(int);
}

Spartan_data::∼Spartan_data(void)
{
}

/* create the data file */
int Spartan_data::create_table(char *path)
{
  DBUG_ENTER("SpartanIndex::create_table");
  open_table(path);
  number_records = 0;
  number_del_records = 0;
  crashed = false;
  write_header();
  DBUG_RETURN(0);
}

/* open table at location "path" = path + filename */
int Spartan_data::open_table(char *path)
{
  DBUG_ENTER("Spartan_data::open_table");
  /*
    Open the file with read/write mode,
    create the file if not found,
    treat file as binary, and use default flags.
  */
  data_file = my_open(path, O_RDWR | O_CREAT | O_BINARY | O_SHARE, MYF(0));
  if(data_file ==1)
    DBUG_RETURN(errno);
  read_header();
  DBUG_RETURN(0);
}

/* write a row of length uchars to file and return position */
long long Spartan_data::write_row(uchar *buf, int length)
{
  long long pos;
  int i;
  int len;
  uchar deleted = 0;

  DBUG_ENTER("Spartan_data::write_row");
  /*
    Write the deleted status uchar and the length of the record.
    Note: my_write() returns the uchars written or −1 on error
  */
  pos = my_seek(data_file, 0L, MY_SEEK_END, MYF(0));
  /*
    Note: my_malloc takes a size of memory to be allocated,
    MySQL flags (set to zero fill and with extra error checking).
    Returns number of uchars allocated -- <= 0 indicates an error.
  */
  i = my_write(data_file, &deleted, sizeof(uchar), MYF(0));
  memcpy(&len, &length, sizeof(int));
  i = my_write(data_file, (uchar *)&len, sizeof(int), MYF(0));
  /*
    Write the row data to the file. Return new file pointer or
    return −1 if error from my_write().
  */
  i = my_write(data_file, buf, length, MYF(0));
  if (i ==1)
    pos = i;
  else
    number_records++;
  DBUG_RETURN(pos);
}

/* update a record in place */
long long Spartan_data::update_row(uchar *old_rec, uchar *new_rec,
                                   int length, long long position)
{
  long long pos;
  long long cur_pos;
  uchar *cmp_rec;
  int len;
  uchar deleted = 0;
  int i =1;

  DBUG_ENTER("Spartan_data::update_row");
  if (position == 0)
    position = header_size; //move past header
  pos = position;
  /*
    If position unknown, scan for the record by reading a row
    at a time until found.
  */
  if (position ==1) //don't know where it is...scan for it
  {
    cmp_rec = (uchar *)my_malloc(length, MYF(MY_ZEROFILL | MY_WME));
    pos = 0;
    /*
      Note: my_seek() returns pos if no errors or −1 if error.
    */
    cur_pos = my_seek(data_file, header_size, MY_SEEK_SET, MYF(0));
    /*
      Note: read_row() returns current file pointer if no error or
      -1 if error.
    */
    while ((cur_pos != −1) && (pos != −1))
    {
      pos = read_row(cmp_rec, length, cur_pos);
      if (memcmp(old_rec, cmp_rec, length) == 0)
      {
        pos = cur_pos;      //found it!
        cur_pos = −1;       //stop loop gracefully
      }
      else if (pos != −1)   //move ahead to next rec
        cur_pos = cur_pos + length + record_header_size;
    }
    my_free(cmp_rec);
  }
  /*
    If position found or provided, write the row.
  */
  if (pos != −1)
  {
    /*
      Write the deleted uchar, the length of the row, and the data
      at the current file pointer.
      Note: my_write() returns the uchars written or −1 on error
    */
    my_seek(data_file, pos, MY_SEEK_SET, MYF(0));
    i = my_write(data_file, &deleted, sizeof(uchar), MYF(0));
    memcpy(&len, &length, sizeof(int));
    i = my_write(data_file, (uchar *)&len, sizeof(int), MYF(0));
    pos = i;
    i = my_write(data_file, new_rec, length, MYF(0));
  }
  DBUG_RETURN(pos);
}

/* delete a record in place */
int Spartan_data::delete_row(uchar *old_rec, int length,
                             long long position)
{
  int i = −1;
  long long pos;
  long long cur_pos;
  uchar *cmp_rec;
  uchar deleted = 1;

  DBUG_ENTER("Spartan_data::delete_row");
  if (position == 0)
    position = header_size; //move past header
  pos = position;
  /*
    If position unknown, scan for the record by reading a row
    at a time until found.
  */
  if (position == −1) //don't know where it is...scan for it
  {
    cmp_rec = (uchar *)my_malloc(length, MYF(MY_ZEROFILL | MY_WME));
    pos = 0;
    /*
      Note: my_seek() returns pos if no errors or −1 if error.
    */
    cur_pos = my_seek(data_file, header_size, MY_SEEK_SET, MYF(0));
    /*
      Note: read_row() returns current file pointer if no error or
      -1 if error.
    */
    while ((cur_pos !=1) && (pos !=1))
    {
      pos = read_row(cmp_rec, length, cur_pos);
      if (memcmp(old_rec, cmp_rec, length) == 0)
      {
        number_records--;
        number_del_records++;
        pos = cur_pos;
        cur_pos =1;
      }
      else if (pos !=1)   //move ahead to next rec
        cur_pos = cur_pos + length + record_header_size;
    }
    my_free(cmp_rec);
  }
  /*
    If position found or provided, write the row.
  */
  if (pos !=1)            //mark as deleted
  {
    /*
      Write the deleted uchar set to 1 which marks row as deleted
      at the current file pointer.
      Note: my_write() returns the uchars written or −1 on error
    */
    pos = my_seek(data_file, pos, MY_SEEK_SET, MYF(0));
    i = my_write(data_file, &deleted, sizeof(uchar), MYF(0));
    i = (i > 1) ? 0 : i;
  }
  DBUG_RETURN(i);
}

/* read a row of length uchars from file at position */
int Spartan_data::read_row(uchar *buf, int length, long long position)
{
  int i;
  int rec_len;
  long long pos;
  uchar deleted = 2;

  DBUG_ENTER("Spartan_data::read_row");
  if (position <= 0)
    position = header_size; //move past header
  pos = my_seek(data_file, position, MY_SEEK_SET, MYF(0));
  /*
    If my_seek found the position, read the deleted uchar.
    Note: my_read() returns uchars read or −1 on error
  */
  if (pos !=1L)
  {
    i = my_read(data_file, &deleted, sizeof(uchar), MYF(0));
    /*
      If not deleted (deleted == 0), read the record length then
      read the row.
    */
    if (deleted == 0) /* 0 = not deleted, 1 = deleted */
    {
      i = my_read(data_file, (uchar *)&rec_len, sizeof(int), MYF(0));
      i = my_read(data_file, buf,
                 (length < rec_len) ? length : rec_len, MYF(0));
    }
    else if (i == 0)
      DBUG_RETURN(−1);
    else
      DBUG_RETURN(read_row(buf, length, cur_position() +
                           length + (record_header_size - sizeof(uchar))));
  }
  else
    DBUG_RETURN(−1);
  DBUG_RETURN(0);
}

/* close file */
int Spartan_data::close_table()
{
  DBUG_ENTER("Spartan_data::close_table");
  if (data_file !=1)
  {
    my_close(data_file, MYF(0));
    data_file =1;
  }
  DBUG_RETURN(0);
}

/* return number of records */
int Spartan_data::records()
{
  DBUG_ENTER("Spartan_data::num_records");
  DBUG_RETURN(number_records);
}

/* return number of deleted records */
int Spartan_data::del_records()
{
  DBUG_ENTER("Spartan_data::num_records");
  DBUG_RETURN(number_del_records);
}

/* read header from file */
int Spartan_data::read_header()
{
  int i;
  int len;

  DBUG_ENTER("Spartan_data::read_header");
  if (number_records ==1)
  {
    my_seek(data_file, 0l, MY_SEEK_SET, MYF(0));
    i = my_read(data_file, (uchar *)&crashed, sizeof(bool), MYF(0));
    i = my_read(data_file, (uchar *)&len, sizeof(int), MYF(0));
    memcpy(&number_records, &len, sizeof(int));
    i = my_read(data_file, (uchar *)&len, sizeof(int), MYF(0));
    memcpy(&number_del_records, &len, sizeof(int));
  }
  else
    my_seek(data_file, header_size, MY_SEEK_SET, MYF(0));
  DBUG_RETURN(0);
}

/* write header to file */
int Spartan_data::write_header()
{
  DBUG_ENTER("Spartan_data::write_header");
  if (number_records !=1)
  {
    my_seek(data_file, 0l, MY_SEEK_SET, MYF(0));
    i = my_write(data_file, (uchar *)&crashed, sizeof(bool), MYF(0));
    i = my_write(data_file, (uchar *)&number_records, sizeof(int), MYF(0));
    i = my_write(data_file, (uchar *)&number_del_records, sizeof(int), MYF(0));
  }
  DBUG_RETURN(0);
}

/* get position of the data file */
long long Spartan_data::cur_position()
{
  long long pos;

  DBUG_ENTER("Spartan_data::cur_position");
  pos = my_seek(data_file, 0L, MY_SEEK_CUR, MYF(0));
  if (pos == 0)
    DBUG_RETURN(header_size);
  DBUG_RETURN(pos);
}

/* truncate the data file */
int Spartan_data::trunc_table()
{
  DBUG_ENTER("Spartan_data::trunc_table");
  if (data_file !=1 )
  {
    my_chsize(data_file, 0, 0, MYF(MY_WME));
    write_header();
  }
  DBUG_RETURN(0);
}

/* determine the row size of the data file */
int Spartan_data::row_size(int length)
{
  DBUG_ENTER("Spartan_data::row_size");
  DBUG_RETURN(length + record_header_size);
}

注意用于存储数据的格式。该类旨在支持从磁盘读取数据以及将内存中的数据写入磁盘。我使用 uchar 指针分配一块内存来存储这些行。这非常有用,因为它提供了使用内部 MySQL 行格式将表中的行写入磁盘的能力。同样,我可以从磁盘读取数据,将它们写入内存缓冲区,并简单地将handler类指向要返回给优化器的内存块。

但是,我可能无法预测存储一行所需的确切内存量。存储引擎的一些用途可能是拥有可变字段或者甚至二进制大对象(blob)的表。为了解决这个问题,我选择在每行的开头存储一个整数长度的字段。这允许我通过首先读取 length 字段,然后读取内存缓冲区中指定的 uchars 数量来扫描文件和读取可变长度的行。

image 提示每当为 MySQL 服务器编写扩展时,总是使用my_xxx实用程序方法。my_xxx实用方法是许多基本操作系统功能的封装,提供了更好的跨平台支持。

数据类相当简单,可用于实现存储引擎所需的基本读写操作。但是,我想让存储引擎更加高效。为了让我的数据文件获得良好的性能,我需要添加一个索引机制。这就是事情变得更加复杂的地方。

image 注意虽然我们不会在前四个阶段使用 index 类,但提前理解这段代码是有好处的。

斯巴达 _ 索引类

为了解决索引数据文件的问题,我实现了一个名为Spartan_index的独立索引类。index 类负责允许执行点查询(通过特定记录的索引进行查询)和范围查询(一系列升序或降序的键),以及缓存索引以进行快速搜索的能力。清单 10-5 包含了Spartan_index类的完整头文件。

清单 10-5。 斯巴达 _ 索引类表头

/*
  Spartan_index.h

  This header file defines a simple index class that can
  be used to store file pointer indexes (long long). The
  class keeps the entire index in memory for fast access.
  The internal-memory structure is a linked list. While
  not as efficient as a btree, it should be usable for
  most testing environments. The constructor accepts the
  max key length. This is used for all nodes in the index.

  File Layout:
    SOF                              max_key_len (int)
    SOF + sizeof(int)                crashed (bool)
    SOF + sizeof(int) + sizeof(bool) DATA BEGINS HERE
*/
#include "my_global.h"
#include "my_sys.h"

const long METADATA_SIZE = sizeof(int) + sizeof(bool);
/*
  This is the node that stores the key and the file
  position for the data row.
*/
struct SDE_INDEX
{
  uchar key[128];
  long long pos;
  int length;
};

/* defines (doubly) linked list for internal list */
struct SDE_NDX_NODE
{
  SDE_INDEX key_ndx;
  SDE_NDX_NODE *next;
  SDE_NDX_NODE *prev;
};

class Spartan_index
{
public:
  Spartan_index(int keylen);
  Spartan_index();
  ∼Spartan_index(void);
  int open_index(char *path);
  int create_index(char *path, int keylen);
  int insert_key(SDE_INDEX *ndx, bool allow_dupes);
  int delete_key(uchar *buf, long long pos, int key_len);
  int update_key(uchar *buf, long long pos, int key_len);
  long long get_index_pos(uchar *buf, int key_len);
  long long get_first_pos();
  uchar *get_first_key();
  uchar *get_last_key();
  uchar *get_next_key();
  uchar *get_prev_key();
  int close_index();
  int load_index();
  int destroy_index();
  SDE_INDEX *seek_index(uchar *key, int key_len);
  SDE_NDX_NODE *seek_index_pos(uchar *key, int key_len);
  int save_index();
  int trunc_index();
private:
  File index_file;
  int max_key_len;
  SDE_NDX_NODE *root;
  SDE_NDX_NODE *range_ptr;
  int block_size;
  bool crashed;
  int read_header();
  int write_header();
  long long write_row(SDE_INDEX *ndx);
  SDE_INDEX *read_row(long long Position);
  long long curfpos();
};

请注意,该类实现了创建、打开、关闭、读取和写入方法的预期形式。load_index()方法将整个索引文件读入内存,将索引存储为双向链表。所有的索引扫描和引用方法都是访问内存中的链表,而不是访问磁盘。这节省了大量时间,并提供了一种将整个索引保存在内存中以便快速插入和删除的方法。一个相应的方法save_index() ,允许你将索引从内存写回磁盘。这些方法的使用方式应该是在表打开时调用load_index(),然后在表关闭时调用save_index()

您可能想知道这种方法是否有大小限制。根据索引的大小、创建的索引数量以及条目数量,这种实现可能会有一些限制。然而,对于本教程和 Spartan 存储引擎的可预见使用来说,这不是问题。

您可能关心的另一个领域是双向链表的使用。这种实现不太可能成为高速索引存储的首选。您更可能使用 B 树或 B 树的某种变体来创建有效的索引访问方法。然而,链表很容易使用,它使得大量源代码的实现更容易管理。这个例子演示了如何将一个索引类合并到您的引擎中——而不是如何编码一个 B 树结构。这使得代码更简单,因为链表更容易编码。出于本教程的目的,链表结构将表现得非常好。事实上,您甚至可能想用它来组成自己的存储引擎,直到您让存储引擎的其余部分工作起来,然后将注意力转向更好的索引类。

清单 10-6 展示了Spartan_index类实现的完整源代码。代码相当长,所以要么花点时间研究这些方法,要么把代码留到以后阅读,直接跳到如何开始构建 Spartan 存储引擎的描述。

清单 10-6。 斯巴达 _index 类源代码

/*
  Spartan_index.cc

  This class reads and writes an index file for use with the Spartan data
  class. The file format is a simple binary storage of the
  Spartan_index::SDE_INDEX structure. The size of the key can be set via
  the constructor.
*/
#include "spartan_index.h"
#include <my_dir.h>
#include <string.h>

/* constuctor takes the maximum key length for the keys */
Spartan_index::Spartan_index(int keylen)
{
  root = NULL;
  crashed = false;
  max_key_len = keylen;
  index_file =1;
  block_size = max_key_len + sizeof(long long) + sizeof(int);
}

/* constuctor (overloaded) assumes existing file */
Spartan_index::Spartan_index()
{
  root = NULL;
  crashed = false;
  max_key_len =1;
  index_file =1;
  block_size =1;
}

/* destructor */
Spartan_index::∼Spartan_index(void)
{
}

/* create the index file */
int Spartan_index::create_index(char *path, int keylen)
{
  DBUG_ENTER("Spartan_index::create_index");
  DBUG_PRINT("info", ("path: %s", path));
  open_index(path);
  max_key_len = keylen;
  /*
    Block size is the key length plus the size of the index
    length variable.
  */
  block_size = max_key_len + sizeof(long long);
  write_header();
DBUG_RETURN(0);
 }

/* open index specified as path (pat+filename) */
int Spartan_index::open_index(char *path)
{
  DBUG_ENTER("Spartan_index::open_index");
  /*
    Open the file with read/write mode,
    create the file if not found,
    treat file as binary, and use default flags.
  */
  index_file = my_open(path, O_RDWR | O_CREAT | O_BINARY | O_SHARE, MYF(0));
  if(index_file ==1)
    DBUG_RETURN(errno);
  read_header();
  DBUG_RETURN(0);
}

/* read header from file */
int Spartan_index::read_header()
{
  DBUG_ENTER("Spartan_index::read_header");
  if (block_size ==1)
  {
    /*
      Seek the start of the file.
      Read the maximum key length value.
    */
    my_seek(index_file, 0l, MY_SEEK_SET, MYF(0));
    i = my_read(index_file, (uchar *)&max_key_len, sizeof(int), MYF(0));
    /*
      Calculate block size as maximum key length plus
      the size of the key plus the crashed status byte.
    */
    block_size = max_key_len + sizeof(long long) + sizeof(int);
    i = my_read(index_file, (uchar *)&crashed, sizeof(bool), MYF(0));
  }
  else
  {
    i = (int)my_seek(index_file, sizeof(int) + sizeof(bool), MY_SEEK_SET, MYF(0));
  }
  DBUG_RETURN(0);
}

/* write header to file */
int Spartan_index::write_header()
{
  int i;

  DBUG_ENTER("Spartan_index::write_header");
  if (block_size !=1)
  {
    /*
      Seek the start of the file and write the maximum key length
      then write the crashed status byte.
    */
    my_seek(index_file, 0l, MY_SEEK_SET, MYF(0));
    i = my_write(index_file, (uchar *)&max_key_len, sizeof(int), MYF(0));
    i = my_write(index_file, (uchar *)&crashed, sizeof(bool), MYF(0));
  }
  DBUG_RETURN(0);
}

/* write a row (SDE_INDEX struct) to the index file */
long long Spartan_index::write_row(SDE_INDEX *ndx)
{
  long long pos;
  int i;
  int len;

  DBUG_ENTER("Spartan_index::write_row");
  /*
     Seek the end of the file (always append)
  */
  pos = my_seek(index_file, 0l, MY_SEEK_END, MYF(0));
  /*
    Write the key value.
  */
  i = my_write(index_file, ndx->key, max_key_len, MYF(0));
  memcpy(&pos, &ndx->pos, sizeof(long long));
  /*
    Write the file position for the key value.
  */
  i = i + my_write(index_file, (uchar *)&pos, sizeof(long long), MYF(0));
  memcpy(&len, &ndx->length, sizeof(int));
  /*
    Write the length of the key.
  */
  i = i + my_write(index_file, (uchar *)&len, sizeof(int), MYF(0));
  if (i ==1)
    pos = i;
  DBUG_RETURN(pos);
}

/* read a row (SDE_INDEX struct) from the index file */
SDE_INDEX *Spartan_index::read_row(long long Position)
{
  int i;
  long long pos;
  SDE_INDEX *ndx = NULL;

  DBUG_ENTER("Spartan_index::read_row");
  /*
    Seek the position in the file (Position).
  */
  pos = my_seek(index_file,(ulong) Position, MY_SEEK_SET, MYF(0));
  if (pos !=1L)
  {
    ndx = new SDE_INDEX();
    /*
      Read the key value.
    */
    i = my_read(index_file, ndx->key, max_key_len, MYF(0));
    /*
      Read the key value. If error, return NULL.
    */
    i = my_read(index_file, (uchar *)&ndx->pos, sizeof(long long), MYF(0));
    if (i ==1)
    {
        delete ndx;
        ndx = NULL;
    }
  }
  DBUG_RETURN(ndx);
}

/* insert a key into the index in memory */
int Spartan_index::insert_key(SDE_INDEX *ndx, bool allow_dupes)
{
  SDE_NDX_NODE *p = NULL;
  SDE_NDX_NODE *n = NULL;
  SDE_NDX_NODE *o = NULL;
  int i =1;
  int icmp;
  bool dupe = false;
  bool done = false;

  DBUG_ENTER("Spartan_index::insert_key");
  /*
    If this is a new index, insert first key as the root node.
  */
  if (root == NULL)
  {
    root = new SDE_NDX_NODE();
    root->next = NULL;
    root->prev = NULL;
    memcpy(root->key_ndx.key, ndx->key, max_key_len);
    root->key_ndx.pos = ndx->pos;
    root->key_ndx.length = ndx->length;
  }
  else //set pointer to root
    p = root;
  /*
    Loop through the linked list until a value greater than the
    key to be inserted, then insert new key before that one.
  */
  while ((p != NULL) && !done)
  {
    icmp = memcmp(ndx->key, p->key_ndx.key,
                 (ndx->length > p->key_ndx.length) ?
                  ndx->length : p->key_ndx.length);
    if (icmp > 0) // key is greater than current key in list
    {
      n = p;
      p = p->next;
    }
    /*
      If dupes not allowed, stop and return NULL
    */
    else if (!allow_dupes && (icmp == 0))
    {
      p = NULL;
      dupe = true;
    }
    else
    {
      n = p->prev; //stop, insert at n->prev
      done = true;
    }
  }
  /*
    If position found (n != NULL) and dupes permitted,
    insert key. If p is NULL insert at end else insert in middle
    of list.
  */
  if ((n != NULL) && !dupe)
  {
    if (p == NULL) //insert at end
    {
      p = new SDE_NDX_NODE();
      n->next = p;
      p->prev = n;
      memcpy(p->key_ndx.key, ndx->key, max_key_len);
      p->key_ndx.pos = ndx->pos;
      p->key_ndx.length = ndx->length;
    }
    else
    {
      o = new SDE_NDX_NODE();
      memcpy(o->key_ndx.key, ndx->key, max_key_len);
      o->key_ndx.pos = ndx->pos;
      o->key_ndx.length = ndx->length;
      o->next = p;
      o->prev = n;
      n->next = o;
      p->prev = o;
    }
    i = 1;
  }
  DBUG_RETURN(i);
}

/* delete a key from the index in memory. Note:
   position is included for indexes that allow dupes */
int Spartan_index::delete_key(uchar *buf, long long pos, int key_len)
{
  SDE_NDX_NODE *p;
  int icmp;
  int buf_len;
  bool done = false;

  DBUG_ENTER("Spartan_index::delete_key");
  p = root;
  /*
    Search for the key in the list. If found, delete it!
  */
  while ((p != NULL) && !done)
  {
    buf_len = p->key_ndx.length;
    icmp = memcmp(buf, p->key_ndx.key,
                 (buf_len > key_len) ? buf_len : key_len);
    if (icmp == 0)
    {
      if (pos !=1)
      {
        if (pos == p->key_ndx.pos)
          done = true;
      }
      else
        done = true;
    }
    else
      p = p->next;
  }
  if (p != NULL)
  {
    /*
      Reset pointers for deleted node in list.
    */
    if (p->next != NULL)
      p->next->prev = p->prev;
    if (p->prev != NULL)
      p->prev->next = p->next;
    else
      root = p->next;
    delete p;
  }
  DBUG_RETURN(0);
}

/* update key in place (so if key changes!) */
int Spartan_index::update_key(uchar *buf, long long pos, int key_len)
{
  SDE_NDX_NODE *p;
  bool done = false;

  DBUG_ENTER("Spartan_index::update_key");
  p = root;
  /*
    Search for the key.
  */
  while ((p != NULL) && !done)
  {
    if (p->key_ndx.pos == pos)
      done = true;
    else
      p = p->next;
  }
  /*
    If key found, overwrite key value in node.
  */
  if (p != NULL)
  {
    memcpy(p->key_ndx.key, buf, key_len);
  }
  DBUG_RETURN(0);
}

/* get the current position of the key in the index file */
long long Spartan_index::get_index_pos(uchar *buf, int key_len)
{
  long long pos =1;

  DBUG_ENTER("Spartan_index::get_index_pos");
  SDE_INDEX *ndx;
  ndx = seek_index(buf, key_len);
  if (ndx != NULL)
    pos = ndx->pos;
  DBUG_RETURN(pos);
}

/* get next key in list */
uchar *Spartan_index::get_next_key()
{
  uchar *key = 0;

  DBUG_ENTER("Spartan_index::get_next_key");
  if (range_ptr != NULL)
  {
    key = (uchar *)my_malloc(max_key_len, MYF(MY_ZEROFILL | MY_WME));
    memcpy(key, range_ptr->key_ndx.key, range_ptr->key_ndx.length);
    range_ptr = range_ptr->next;
  }
  DBUG_RETURN(key);
}

/* get prev key in list */
uchar *Spartan_index::get_prev_key()
{
  uchar *key = 0;

  DBUG_ENTER("Spartan_index::get_prev_key");
  if (range_ptr != NULL)
  {
    key = (uchar *)my_malloc(max_key_len, MYF(MY_ZEROFILL | MY_WME));
    memcpy(key, range_ptr->key_ndx.key, range_ptr->key_ndx.length);
    range_ptr = range_ptr->prev;
  }
  DBUG_RETURN(key);
}

/* get first key in list */
uchar *Spartan_index::get_first_key()
{
  SDE_NDX_NODE *n = root;
  uchar *key = 0;

  DBUG_ENTER("Spartan_index::get_first_key");
  if (root != NULL)
  {
    key = (uchar *)my_malloc(max_key_len, MYF(MY_ZEROFILL | MY_WME));
    memcpy(key, n->key_ndx.key, n->key_ndx.length);
  }
  DBUG_RETURN(key);
}

/* get last key in list */
uchar *Spartan_index::get_last_key()
{
  SDE_NDX_NODE *n = root;
  uchar *key = 0;

  DBUG_ENTER("Spartan_index::get_last_key");
  while (n->next != NULL)
    n = n->next;
  if (n != NULL)
  {
    key = (uchar *)my_malloc(max_key_len, MYF(MY_ZEROFILL | MY_WME));
    memcpy(key, n->key_ndx.key, n->key_ndx.length);
  }
  DBUG_RETURN(key);
}

/* just close the index */
int Spartan_index::close_index()
{
  SDE_NDX_NODE *p;

  DBUG_ENTER("Spartan_index::close_index");
  if (index_file !=1)
  {
    my_close(index_file, MYF(0));
    index_file =1;
  }
  while (root != NULL)
  {
    p = root;
    root = root->next;
    delete p;
  }
  DBUG_RETURN(0);
}

/* find a key in the index */
SDE_INDEX *Spartan_index::seek_index(uchar *key, int key_len)
{
  SDE_INDEX *ndx = NULL;
  SDE_NDX_NODE *n = root;
  int buf_len;
  bool done = false;

  DBUG_ENTER("Spartan_index::seek_index");
  if (n != NULL)
  {
    while((n != NULL) && !done)
    {
      buf_len = n->key_ndx.length;
      if (memcmp(n->key_ndx.key, key,
          (buf_len > key_len) ? buf_len : key_len) == 0)
        done = true;
      else
        n = n->next;
    }
  }
  if (n != NULL)
  {
    ndx = &n->key_ndx;
    range_ptr = n;
  }
  DBUG_RETURN(ndx);
}

/* find a key in the index and return position too */
SDE_NDX_NODE *Spartan_index::seek_index_pos(uchar *key, int key_len)
{
  SDE_NDX_NODE *n = root;
  int buf_len;
  bool done = false;

  DBUG_ENTER("Spartan_index::seek_index_pos");
  if (n != NULL)
  {
    while((n->next != NULL) && !done)
    {
      buf_len = n->key_ndx.length;
      if (memcmp(n->key_ndx.key, key,
          (buf_len > key_len) ? buf_len : key_len) == 0)
        done = true;
      else if (n->next != NULL)
        n = n->next;
    }
  }
  DBUG_RETURN(n);
}

/* read the index file from disk and store in memory */
int Spartan_index::load_index()
{
  SDE_INDEX *ndx;
  int i = 1;

  DBUG_ENTER("Spartan_index::load_index");
  if (root != NULL)
    destroy_index();
  /*
    First, read the metadata at the front of the index.
  */
  read_header();
  while(i != 0)
  {
    ndx = new SDE_INDEX();
    i = my_read(index_file, (uchar *)&ndx->key, max_key_len, MYF(0));
    i = my_read(index_file, (uchar *)&ndx->pos, sizeof(long long), MYF(0));
    i = my_read(index_file, (uchar *)&ndx->length, sizeof(int), MYF(0));
    if (i != 0)
      insert_key(ndx, false);
  }
  DBUG_RETURN(0);
}

/* get current position of index file */
long long Spartan_index::curfpos()
{
  long long pos = 0;

  DBUG_ENTER("Spartan_index::curfpos");
  pos = my_seek(index_file, 0l, MY_SEEK_CUR, MYF(0));
  DBUG_RETURN(pos);
}

/* write the index back to disk */
int Spartan_index::save_index()
{
  SDE_NDX_NODE *n = NULL;
  int i;
  DBUG_ENTER("Spartan_index::save_index");
  i = my_chsize(index_file, 0L, '\n', MYF(MY_WME));
  write_header();
  n = root;
  while (n != NULL)
  {
    write_row(&n->key_ndx);
    n = n->next;
  }
  DBUG_RETURN(0);
}

int Spartan_index::destroy_index()
{
  SDE_NDX_NODE *n = root;
  DBUG_ENTER("Spartan_index::destroy_index");
  while (root != NULL)
  {
    n = root;
    root = n->next;
    delete n;
  }
  root = NULL;
  DBUG_RETURN(0);
}

/* Get the file position of the first key in index */
long long Spartan_index::get_first_pos()
{
  long long pos =1;

  DBUG_ENTER("Spartan_index::get_first_pos");
  if (root != NULL)
    pos = root->key_ndx.pos;
  DBUG_RETURN(pos);
}

/* truncate the index file */
int Spartan_index::trunc_index()
{
  DBUG_ENTER("Spartan_data::trunc_table");
  if (index_file !=1)
  {
    my_chsize(index_file, 0, 0, MYF(MY_WME));
    write_header();
  }
  DBUG_RETURN(0);
}

注意,和Spartan_data类一样,我使用DBUG例程来设置用于调试的跟踪元素。我还使用了my_xxx平台安全的实用方法。

image 提示这些方法可以在源码树根下的mysys目录中找到。它们通常被实现为存储在同名文件中的 C 函数(例如,my_write.c文件包含了my_write()方法)。

索引的工作方式是使用指向内存块的 uchar 指针存储一个键,一个位置值(long long)存储磁盘上用于定位文件指针的偏移位置,一个长度字段存储键的长度。在内存比较方法中使用了length变量来设置比较长度。这些数据项存储在一个名为SDE_INDEX的结构中。双向链表节点是另一个包含一个SDE_INDEX结构的结构。名为SDE_NDX_NODE的列表节点结构也为列表提供了nextprev指针。

当使用索引存储数据在Spartan_data类文件中的位置时,可以调用insert_index()方法,传入文件中数据项的键和偏移量。这个偏移量在对my_write()方法的调用中返回。这种技术允许您将指向数据的索引指针存储在磁盘上,并重用该信息,而无需转换它来将文件指针定位到磁盘上的正确位置。

索引存储在磁盘上的连续数据块中,这些数据块对应于SDE_INDEX结构的大小。该文件有一个头,用于存储崩溃状态变量和存储最大密钥长度的变量。崩溃状态变量有助于识别文件已损坏或在读取或写入期间发生了危及文件或其元数据完整性的错误的罕见情况。我没有使用可变长度的字段,比如 data 类,而是使用固定长度的内存块来简化磁盘访问的读写方法。在这种情况下,我有意识地决定为了简洁而牺牲空间。

现在,您已经了解了构建存储引擎(低级 I/O 功能)的肮脏工作,让我们看看如何构建一个基本的存储引擎。我将在后面的章节中返回到Spartan_dataSpartan_index类,分别讨论阶段 1 和阶段 5。

入门指南

下面的教程假设您已经配置了开发环境,并且已经编译了打开调试开关的服务器(见第 5 章)。我研究了构建 Spartan 存储引擎的每个阶段。在开始之前,您需要做一个非常重要的步骤:创建一个测试文件来测试存储引擎,这样我们就可以朝着一个特定的目标驱动开发。第 4 章研究了 MySQL 测试套件以及如何创建和运行测试。请参阅该章了解更多详细信息或复习。

image 提示如果你使用的是 Windows,你可能无法使用 MySQL 测试套件(mysql-test-run.pl)。您可以使用 Cygwin ( http://cygwin.com/)来建立一个类似 Unix 的环境,并在那里运行测试套件。如果您不想设置 Cygwin 环境,您仍然可以创建测试文件,将语句复制并粘贴到 MySQL 客户端程序中,并以这种方式运行测试。

您应该做的第一件事是创建一个新的测试来测试 Spartan 存储引擎。即使引擎还不存在,本着测试驱动开发的精神,您应该在编写代码之前创建测试。让我们现在做那件事。

测试文件应该从一个简单的测试开始,创建表并从中检索行。您可以创建一个完整的测试文件,其中包含我将向您展示的所有操作,但是最好从一个简单的测试开始,并在构建 Spartan 存储引擎的过程中扩展它。这有一个额外的好处,即您的测试将只测试当前阶段,而不会为尚未实现的操作生成错误。清单 10-7 显示了一个测试 Stage 1 Spartan 存储引擎的基本测试示例。

在阅读本教程的过程中,您将向该测试添加语句,从而有效地为完整的 Spartan 存储引擎构建完整的测试。

清单 10-7。 斯巴达-存储-引擎测试文件(Ch10s1.test)

#
# Simple test for the Spartan storage engine
#
--disable_warnings
drop table if exists t1;
--enable_warnings

CREATE TABLE t1 (
  col_a int,
  col_b varchar(20),
  col_c int
) ENGINE=SPARTAN;

SELECT * FROM t1;

RENAME TABLE t1 TO t2;

DROP TABLE t2;

您可以在源代码树的根目录下的/mysql-test/t目录中创建这个文件。当你第一次执行的时候,出错是正常的。事实上,您应该在开始阶段 1 之前执行测试。这样,你就知道测试是有效的(它不会失败)。如果您回忆起第 4 章的,您可以使用/mysql-test目录中的命令来执行测试:

%> touch r/Ch10s1.result
%> ./mysql-test-run.pl Ch10s1
%> cp r/cab.reject r/Ch10s1.result
%> ./mysql-test-run.pl Ch10s1

你试过吗?它产生错误了吗?测试套件返回了[failed],但是如果您检查生成的日志文件,您不会看到任何错误,尽管您会看到警告。为什么没有失败?事实证明,如果您在 create 语句中指定的存储引擎不存在,MySQL 将使用默认的存储引擎。在这种情况下,我的 MySQL 服务器安装发出了系统正在使用默认的 MyISAM 存储引擎的错误,因为没有找到 Spartan 存储引擎。清单 10-8 显示了一个/mysql-test/r/Ch10s1.log文件的例子。

清单 10-8。 来自测试运行的示例日志文件

mysql> drop table if exists t1;
mysql> CREATE TABLE t1 (
    ->   col_a int,
    ->   col_b varchar(20),
    ->   col_c int
    -> ) ENGINE=SPARTAN;
ERROR 1286 (42000): Unknown storage engine 'SPARTAN'
mysql>
mysql> SELECT * FROM t1;
ERROR 1146 (42S02): Table 'test.t1' doesn't exist
mysql>
mysql> DROP TABLE t1;
ERROR 1051 (42S02): Unknown table 'test.t1'

第一阶段:踩熄发动机

这个阶段的目标是生产一个存根存储引擎插件。存根引擎将具有最基本的操作,能够在CREATE语句上选择引擎并创建基表元文件(.frm)。我知道这听起来并不多,虽然它实际上并不存储任何东西, 5 创建一个阶段 1 引擎可以让您确保您拥有向服务器注册存储引擎所需的所有初始代码更改。我之前提到过,在 MySQL 系统的未来版本中,其中一些变化可能是不必要的。在使用 MySQL 源代码之前,最好查看一下在线参考手册中的最新变化。

创建斯巴达插件源文件

首先,在主源代码树的/storage目录下创建一个名为spartan的目录。我使用示例存储引擎让我们开始。MySQL 参考手册建议使用示例存储引擎的源文件作为基础。示例存储引擎包含用正确的代码语句实现的所有必要方法。这使得为 Spartan 存储引擎创建基础源文件变得很容易。

*.cc*.h文件从/storage/example目录复制到/storage/spartan目录。现在在spartan目录中应该有两个文件:ha_example.ccha_example.h。前缀ha_表示这些文件是从处理程序类派生的,代表一个表处理程序。重命名文件ha_spartan.ccha_spartan.h

image 注意短语表处理器 已经被更新的短语存储引擎所取代。您可能会遇到一些关于表处理程序的文档。它们与存储引擎同义,因此适用。

创建源文件的下一步是将所有出现的单词exampleEXAMPLE分别改为spartanSPARTAN。您可以使用您喜欢的代码编辑器或文本处理器来实现这些更改。生成的文件应该将所有的示例标识符都更改为spartan(例如st_example_share应该变成st_spartan_share)。用例敏感性。如果您没有正确地做到这一点,您的存储引擎将无法工作。

最后,编辑ha_spartan.h文件并添加 include 指令以包含spartan_data.h文件,如下所示:

#include "spartan_data.h"

添加 CMakeLists.txt 文件

因为我们正在创建一个新的插件和一个新的项目,所以我们需要创建一个 CMakeLists.txt 文件,以便 cmake 工具可以为项目创建适当的 make 文件。在/storage/spartan目录中打开一个新文件,并将其命名为 CMakeLists.txt。添加到文件中:

# Spartan storage engine plugin

SET(SPARTAN_PLUGIN_STATIC "spartan")
SET(SPARTAN_PLUGIN_DYNAMIC "spartan")

SET(SPARTAN_SOURCES ha_spartan.cc ha_spartan.h spartan_data.cc spartan_data.h)
MYSQL_ADD_PLUGIN(spartan ${SPARTAN_SOURCES} STORAGE_ENGINE MODULE_ONLY)

请注意,我们使用宏来定义插件的源代码,并在 c make 操作期间使用另一个宏来添加特定于存储引擎的 make 文件行。

最终修改

你需要做另外一个改变。在ha_spartan.cc文件的底部,您应该会看到一个mysq_declare_plugin部分。这是插件接口用来安装存储引擎的代码。关于这个结构的更多细节见第 9 章

请随意修改这一部分,以表明它是 Spartan 存储引擎。您可以在代码中添加自己的名字和注释。这个部分还没有使用,但是当存储引擎插件架构完成时,您将需要这个部分来启用插件接口。


mysql_declare_plugin(spartan)
{
  MYSQL_STORAGE_ENGINE_PLUGIN,
  &spartan_storage_engine,
  "Spartan",
  "Chuck Bell",
  "Spartan Storage Engine Plugin",
  PLUGIN_LICENSE_GPL,
  spartan_init_func,                            /* Plugin Init */
  NULL,                                      /* Plugin Deinit */
  0x0100 /* 1.0 */,
  func_status,                                /* status variables */
  spartan_system_variables,                          /* system variables */
  NULL,                                      /* config options */
  0,                                      /* flags */
}
mysql_declare_plugin_end;

如果对于一个存储引擎插件来说,这似乎是一个很大的工作量,那么事实就是如此。幸运的是,这种情况将在 MySQL 系统的未来版本中得到改善。

编译斯巴达引擎

现在所有这些改变都已经完成,是时候编译服务器并测试新的 Spartan 存储引擎了。该过程与其他编译过程相同。从源树的根目录,运行命令:

cmake .
make

在调试模式下编译服务器,以便可以在服务器运行时生成跟踪文件并使用交互式调试器浏览源代码。

测试斯巴达发动机的第一阶段

一旦服务器编译完毕,您就可以启动并运行它了。首先,安装新的插件。正如我们在第 9 章中看到的,我们可以将编译后的库(ha_spartan.so)复制到插件目录(plugin-dir)并执行命令:

INSTALL PLUGIN spartan SONAME 'ha_spartan.so';

或者,对于 Windows,此命令:

INSTALL PLUGIN spartan SONAME 'ha_spartan.dll';

您可能会尝试使用交互式 MySQL 客户端来测试服务器。没关系,我就是这么做的。清单 10-9 显示了运行大量 SQL 命令后 MySQL 客户端的结果。在这个例子中,我运行了SHOW STORAGE ENGINESCREATE TABLESHOW CREATE TABLEDROP TABLE命令。结果表明,这些命令是有效的,当我运行它时,测试应该会通过。

清单 10-9。 示例第一阶段斯巴达存储引擎手动测试

mysql> SHOW PLUGINS \G
*************************** 1\. row ***************************
   Name: binlog
 Status: ACTIVE
   Type: STORAGE ENGINE
Library: NULL
License: GPL
*************************** 2\. row ***************************
   Name: mysql_native_password
 Status: ACTIVE
   Type: AUTHENTICATION
Library: NULL
License: GPL
*************************** 3\. row ***************************
   Name: mysql_old_password
 Status: ACTIVE
   Type: AUTHENTICATION
Library: NULL
License: GPL
*************************** 4\. row ***************************
   Name: sha256_password
 Status: ACTIVE
   Type: AUTHENTICATION
Library: NULL
License: GPL
*************************** 5\. row ***************************
   Name: CSV
 Status: ACTIVE
   Type: STORAGE ENGINE
Library: NULL
License: GPL
*************************** 6\. row ***************************
   Name: MRG_MYISAM
 Status: ACTIVE
   Type: STORAGE ENGINE
Library: NULL
License: GPL
*************************** 7\. row ***************************
   Name: MEMORY
 Status: ACTIVE
   Type: STORAGE ENGINE
Library: NULL
License: GPL
*************************** 8\. row ***************************
   Name: MyISAM
 Status: ACTIVE
   Type: STORAGE ENGINE
Library: NULL
License: GPL
*************************** 9\. row ***************************
   Name: BLACKHOLE
 Status: ACTIVE
   Type: STORAGE ENGINE
Library: NULL
License: GPL
*************************** 10\. row ***************************
   Name: InnoDB
 Status: ACTIVE
   Type: STORAGE ENGINE
Library: NULL
License: GPL

...

*************************** 43\. row ***************************
   Name: partition
 Status: ACTIVE
   Type: STORAGE ENGINE
Library: NULL
License: GPL
43 rows in set (0.00 sec)

mysql> INSTALL PLUGIN spartan SONAME 'ha_spartan.so';
Query OK, 0 rows affected (0.00 sec)

mysql> SHOW PLUGINS \G
*************************** 1\. row ***************************
   Name: binlog
 Status: ACTIVE
   Type: STORAGE ENGINE
Library: NULL
License: GPL
*************************** 2\. row ***************************
   Name: mysql_native_password
 Status: ACTIVE
   Type: AUTHENTICATION
Library: NULL
License: GPL
*************************** 3\. row ***************************
   Name: mysql_old_password
 Status: ACTIVE
   Type: AUTHENTICATION
Library: NULL
License: GPL
*************************** 4\. row ***************************
   Name: sha256_password
 Status: ACTIVE
   Type: AUTHENTICATION
Library: NULL
License: GPL
*************************** 5\. row ***************************
   Name: CSV
 Status: ACTIVE
   Type: STORAGE ENGINE
Library: NULL
License: GPL
*************************** 6\. row ***************************
   Name: MRG_MYISAM
 Status: ACTIVE
   Type: STORAGE ENGINE
Library: NULL
License: GPL
*************************** 7\. row ***************************
   Name: MEMORY
 Status: ACTIVE
   Type: STORAGE ENGINE
Library: NULL
License: GPL
*************************** 8\. row ***************************
   Name: MyISAM
 Status: ACTIVE
   Type: STORAGE ENGINE
Library: NULL
License: GPL
*************************** 9\. row ***************************
   Name: BLACKHOLE
 Status: ACTIVE
   Type: STORAGE ENGINE
Library: NULL
License: GPL
*************************** 10\. row ***************************
   Name: InnoDB
 Status: ACTIVE
   Type: STORAGE ENGINE
Library: NULL
License: GPL

...

*************************** 43\. row ***************************
   Name: partition
 Status: ACTIVE
   Type: STORAGE ENGINE
Library: NULL
License: GPL
*************************** 44\. row ***************************
   Name: Spartan
 Status: ACTIVE
   Type: STORAGE ENGINE
Library: ha_spartan.so
License: GPL
44 rows in set (0.00 sec)

mysql> use test;
Database changed
mysql> CREATE TABLE t1 (col_a int, col_b varchar(20), col_c int) ENGINE=SPARTAN;
Query OK, 0 rows affected (0.02 sec)

mysql> SHOW CREATE TABLE t1 \G
*************************** 1\. row ***************************
       Table: t1
Create Table: CREATE TABLE 't1' (
  'col_a' int(11) DEFAULT NULL,
  'col_b' varchar(20) DEFAULT NULL,
  'col_c' int(11) DEFAULT NULL
) ENGINE=SPARTAN DEFAULT CHARSET=latin1
1 row in set (0.00 sec)

mysql> DROP TABLE t1;
Query OK, 0 rows affected (0.00 sec)

mysql>

我知道存储引擎正在工作,因为它列在了SHOW PLUGINS 命令和SHOW CREATE TABLE语句中。如果引擎连接失败,它可能会也可能不会显示在SHOW PLUGINS命令中,但是CREATE TABLE命令会指定 MyISAM 存储引擎而不是 Spartan 存储引擎。

您还应该运行您之前创建的测试(如果您运行的是 Linux)。当您这次运行测试时,测试通过。这是因为存储引擎现在是服务器的一部分,它可以被识别。让我们输入SELECT命令并重新运行测试。它应该再次通过。此时,您可以将测试结果添加到/r目录中,用于自动化测试报告。清单 10-10 显示了更新后的测试。

image 我们会为每个阶段做新版本的测试,命名为 Ch10sX(如 Ch10s1、Ch10s2 等。).

清单 10-10。 更新了斯巴达存储引擎测试文件(Ch10s1.test)

#
# Simple test for the Spartan storage engine
#
--disable_warnings
drop table if exists t1;
--enable_warnings

CREATE TABLE t1 (
  col_a int,
  col_b varchar(20),
  col_c int
) ENGINE=SPARTAN;

SELECT * FROM t1;

DROP TABLE t1;

这是第一阶段的引擎。它已经插好,可以添加Spartan_dataspartan_index类了。在下一阶段,我们将添加创建、打开、关闭和删除文件的功能。这听起来可能不多,但本着增量开发的精神,您可以添加这一点,然后测试和调试,直到一切正常,然后再继续进行更具挑战性的操作。

阶段 2:使用表格

此阶段的目标是生成一个存根存储引擎,它可以创建、打开、关闭和删除数据文件。在此阶段,您将设置基本的文件处理例程,并确定引擎正在正确处理文件。MySQL 已经为您提供了许多文件 I/O 例程,它们封装了底层函数,使它们平台安全。以下是一些可用函数的示例。详见/mysys目录中的文件。

  • my_create(...):创建文件
  • my_open(...):打开文件
  • my_read(...):从文件中读取数据
  • my_write(...):将数据写入文件
  • my_delete(...):删除文件
  • fn_format(...):创建一个平台安全的路径语句

在这一阶段,我将向您展示如何为低级 I/O 合并Spartan_data类。我将引导您完成每个更改,并包含每个更改的完整方法源代码。

更新斯巴达源文件

首先,要么从 Apress 网站的这本书的目录页面下载压缩的源文件,并将它们复制到您的/storage/spartan目录中。或者使用您之前创建的spartan_data.ccspartan_data.h文件。

因为我使用了Spartan_data类来处理低级 I/O,所以我需要创建一个对象指针来保存该类的一个实例。我需要把它放在一个可以共享的地方,这样就不会有两个或更多的类实例试图读取同一个文件。虽然这可能没问题,但它更复杂,需要更多的工作。相反,我在 Spartan 处理程序的共享结构中放置了一个对象引用。

image 提示在你做出每一个改变之后,编译spartan项目以确保没有错误。在进行下一个更改之前,请更正任何错误。

更新头文件

打开 ha_spartan.h 文件,将对象引用添加到 st_spartan_share 结构中。清单 10-11 显示了完整的代码变更(为简洁起见,省略了注释)。一旦你做了这个改变,重新编译spartan源文件以确保没有任何错误。

清单 10-11。 更改 ha_spartan.h 中的共享结构

/*
  Spartan Storage Engine Plugin
*/

#include "my_global.h"                   /* ulonglong */
#include "thr_lock.h"                    /* THR_LOCK, THR_LOCK_DATA */
#include "handler.h"                     /* handler */
#include "spartan_data.h"

class Spartan_share : public Handler_share {
public:
  mysql_mutex_t mutex;
  THR_LOCK lock;
  Spartan_data *data_class;
  Spartan_share();
  ∼Spartan_share()
  {
    thr_lock_delete(&lock);
    mysql_mutex_destroy(&mutex);
    if (data_class != NULL)
      delete data_class;
    data_class = NULL;
  }
};

...

更新类文件

接下来的一系列修改是在ha_spartan.cc文件中完成的。打开文件并找到constructor for the new Spartan_share class。由于现在在共享结构中有一个对象引用,我们需要在创建共享时实例化它。将Spartan_data类的实例化添加到方法中。将对象引用命名为data_class清单 10-12 显示了经过修改的方法摘录。

image 提示如果你使用的是 Windows,而 Visual Studio 中的 IntelliSense 无法识别新的Spartan_data类,你需要修复.ncb文件。退出 Visual Studio,从源根目录删除.ncb文件,然后重新构建mysqld。这可能需要一段时间,但完成后,IntelliSense 将再次工作。

清单 10-12。 更改 ha_spartan.cc 中的 Spartan_data 类构造函数

Spartan_share::Spartan_share()
{
  thr_lock_init(&lock);
  mysql_mutex_init(ex_key_mutex_Spartan_share_mutex,
                   &mutex, MY_MUTEX_INIT_FAST);
  data_class = new Spartan_data();
}

自然,当共享结构被销毁时,您也需要销毁对象引用。找到destructor方法并添加代码来销毁数据类对象引用。清单 10-13 显示了修改后的方法摘录。

清单 10-13。 更改 ha_spartan.h 中的 Spartan_data 析构函数

class Spartan_share : public Handler_share {
public:
  mysql_mutex_t mutex;
  THR_LOCK lock;
  Spartan_data *data_class;
  Spartan_share();
  ∼Spartan_share()
  {
    thr_lock_delete(&lock);
    mysql_mutex_destroy(&mutex);
    if (data_class != NULL)
      delete data_class;
    data_class = NULL;
  }
};

Spartan 存储引擎的处理程序实例也必须为数据文件提供文件扩展名。因为既有数据文件又有索引文件,所以需要创建两个文件扩展名。定义文件扩展名并将它们添加到ha_spartan_exts数组。对数据文件使用.sdesdi为索引文件。MySQL 使用这些扩展来删除文件和其他维护操作。找到ha_spartan_exts数组,在它上面添加#define,并将这些定义添加到数组中。清单 10-14 显示了数组结构的变化。

清单 10-14。 对 ha_spartan.cc 中 ha_spartan_exts 数组的修改

#define SDE_EXT ".sde"
#define SDI_EXT ".sdi"

static const char *ha_spartan_exts[] = {
  SDE_EXT,
  SDI_EXT,
  NullS
};

您需要添加的第一个操作是创建文件操作。这将创建一个空文件来包含表的数据。找到create()方法并添加代码以获得共享结构的副本,然后调用数据类create_table()方法并关闭表。清单 10-15 显示了更新后的创建方法。我将向您展示如何在稍后的阶段添加索引类。

清单 10-15。 更改 ha_spartan.cc 中的 create()方法

int ha_spartan::create(const char *name, TABLE *table_arg,
                       HA_CREATE_INFO *create_info)
{
  DBUG_ENTER("ha_spartan::create");
  char name_buff[FN_REFLEN];

  if (!(share = get_share()))
    DBUG_RETURN(1);
  /*
    Call the data class create table method.
    Note: the fn_format() method correctly creates a file name from the
    name passed into the method.
  */
  if (share->data_class->create_table(fn_format(name_buff, name, "", SDE_EXT,
                                      MY_REPLACE_EXT|MY_UNPACK_FILENAME)))
    DBUG_RETURN(−1);
  share->data_class->close_table();
  DBUG_RETURN(0);
}

您需要添加的下一个操作是打开文件操作。这将打开包含表格数据的文件。找到open()方法并添加代码以获得共享结构的副本并打开表。清单 10-16 显示了更新后的打开方法。

清单 10-16。 更改 ha_spartan.cc 中的 open()方法

int ha_spartan::open(const char *name, int mode, uint test_if_locked)
{
  DBUG_ENTER("ha_spartan::open");
  char name_buff[FN_REFLEN];

  if (!(share = get_share()))
    DBUG_RETURN(1);
  /*
    Call the data class open table method.
    Note: the fn_format() method correctly creates a file name from the
    name passed into the method.
  */
  share->data_class->open_table(fn_format(name_buff, name, "", SDE_EXT,
                                MY_REPLACE_EXT|MY_UNPACK_FILENAME));
  thr_lock_data_init(&share->lock,&lock,NULL);
  DBUG_RETURN(0);

}

您需要添加的下一个操作是删除文件操作。这将删除包含该表数据的文件。找到delete_table()方法,添加关闭表的代码,并调用my_delete()函数删除表。清单 10-17 显示了更新后的删除方法。我将在稍后阶段向您展示如何添加索引类。

清单 10-17。 修改 ha_spartan.cc 中的 delete_table()方法

int ha_spartan::delete_table(const char *name)
{
  DBUG_ENTER("ha_spartan::delete_table");
  char name_buff[FN_REFLEN];

  /*
    Call the mysql delete file method.
    Note: the fn_format() method correctly creates a file name from the
    name passed into the method.
  */
  my_delete(fn_format(name_buff, name, "", SDE_EXT,
            MY_REPLACE_EXT|MY_UNPACK_FILENAME), MYF(0));
  DBUG_RETURN(0);
}

还有最后一个操作,许多开发者都忘记了。RENAME TABLE命令允许用户重命名表格。您的存储处理程序还必须能够将文件复制到新名称,然后删除旧名称。当 MySQL 服务器处理.frm文件的重命名时,您需要执行数据文件的复制。找到rename_table()方法并添加代码来调用my_copy()函数来复制表的数据文件。清单 10-18 显示了更新后的重命名表方法。稍后,我将向您展示如何添加索引类。

清单 10-18。 修改 ha_spartan.cc 中的 rename_table()方法

int ha_spartan::rename_table(const char * from, const char * to)
{
  DBUG_ENTER("ha_spartan::rename_table ");
  char data_from[FN_REFLEN];
  char data_to[FN_REFLEN];

  my_copy(fn_format(data_from, from, "", SDE_EXT,
          MY_REPLACE_EXT|MY_UNPACK_FILENAME),
          fn_format(data_to, to, "", SDE_EXT,
          MY_REPLACE_EXT|MY_UNPACK_FILENAME), MYF(0));
  /*
    Delete the file using MySQL's delete file method.
  */
  my_delete(data_from, MYF(0));
  DBUG_RETURN(0);
}

好了,现在您已经完成了第 2 阶段存储引擎。剩下要做的就是编译服务器并运行测试。

image 注意一定要把更新后的 ha_spartan.so(或者 ha_spartan.dll)复制到你的插件目录下。如果你忘记了这一步,你可能会花很多时间去寻找为什么你的第二阶段引擎不能正常工作。

测试斯巴达发动机的第二阶段

当您再次运行测试时,您应该看到所有语句都成功完成。然而,有两件事测试不会验证你。首先,您需要确保.sde文件已经创建并删除。其次,您需要确保 rename 命令有效。

测试创建和删除表的命令很容易。启动服务器,然后启动 MySQL 客户端。从测试中发出CREATE语句,然后使用您的文件浏览器导航到/data/test文件夹。在那里你应该看到两个文件:t1.frmt1.sde。回到你的 MySQL 客户端,发布DROP声明。然后返回到/data/test文件夹,验证文件确实被删除了。

测试重命名表的命令也很容易。重复CREATE语句测试,然后发出命令:

RENAME TABLE t1 TO t2;

运行 RENAME 命令后,您应该能够发出 SELECT 语句,甚至是 DROP 语句来操作重命名的表。这将产生如下输出:

mysql> RENAME TABLE t1 to t2;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM t2;
Empty set (0.00 sec)

mysql> DROP TABLE t2;
Query OK, 0 rows affected (0.00 sec)

使用文件浏览器导航至/data/test文件夹。在那里你应该看到两个文件:t2.frmt2.sde。返回 MySQL 客户端,发出DROP语句。然后返回到/data/test文件夹,验证文件确实被删除了。

既然您已经验证了RENAME语句可以工作,那么将它添加到测试文件中并重新运行测试。测试应该没有错误地完成。清单 10-19 显示了更新后的Ch10s2.test文件。

清单 10-19。 更新了斯巴达存储引擎测试文件(Ch10s2.test)

#
# Simple test for the Spartan storage engine
#
--disable_warnings
drop table if exists t1;
--enable_warnings

CREATE TABLE t1 (
  col_a int,
  col_b varchar(20),
  col_c int
) ENGINE=SPARTAN;

SELECT * FROM t1;

RENAME TABLE t1 TO t2;

DROP TABLE t2;

好了,这是第二阶段的引擎。它被插入并创建、删除和重命名文件。在下一阶段,我们将添加读写数据的能力。

第三阶段:读写数据

此阶段的目标是生产一个可以读写数据的工作存储引擎。在这个阶段,我将向您展示如何结合Spartan_data类来读取和写入数据。我将带您了解每个变更,并包括每个变更的完整方法源代码。

更新斯巴达源文件

制造阶段 3 引擎需要更新基本读取过程(如前所述)。为了实现读操作,您将对ha_spartan.cc文件中的rnd_init()rnd_next()position()rnd_pos()方法进行修改。position()rnd_pos()方法在大型排序操作中使用,并使用内部缓冲区来存储行。写操作只需要改变write_row()方法。

更新头文件

定位方法要求您存储一个指针—记录偏移位置或排序操作中使用的键值。Oracle 提供了一种很好的方式来做到这一点,稍后您将在 position 方法中看到。打开ha_spartan.h文件并将current_position变量添加到ha_spartan类中。清单 10-20 显示了修改后的摘录。

清单 10-20。 修改为 ha_spartan.h 中的 ha_spartan 职业

class ha_spartan: public handler
{
  THR_LOCK_DATA lock;      /* MySQL lock */
  Spartan_share *share;    ///< Shared lock info
  Spartan_share *get_share(); ///< Get the share
  off_t current_position;  /* Current position in the file during a file scan */

public:
  ha_spartan(handlerton *hton, TABLE_SHARE *table_ar);
  ∼ha_spartan()
  {
  }
...

更新源文件

返回到ha_spartan.cc文件,因为那是需要进行其余修改的地方。你需要改变的第一个方法是rnd_init()。这里是您需要为表扫描设置初始条件的地方。在这种情况下,将当前位置设置为 0(文件开始),记录数设置为 0,并指定要用于排序方法的项目的长度。使用long long,,因为这是文件中当前位置的数据类型。清单 10-21 显示了经过修改的更新方法。

清单 10-21。 更改 ha_spartan.cc 中的 rnd_init()方法

int ha_spartan::rnd_init(bool scan)
{
  DBUG_ENTER("ha_spartan::rnd_init");
  current_position = 0;
  stats.records = 0;
  ref_length = sizeof(long long);
  DBUG_RETURN(0);
}

image 注意这是我们开始添加示例引擎之外的功能的地方。请确保正确指定您的返回代码。示例引擎通过发出 return 语句DBUG_RETURN(HA_ERR_WRONG_COMMAND);告诉优化器某个函数不受支持。请确保将这些更改为错误命令返回代码以外的内容(例如,0)。

下一个需要改变的方法是rnd_next(),它负责从文件中获取下一条记录,并检测文件的结尾。在这个方法中,您可以调用数据类read_row()方法,传入记录缓冲区、缓冲区的长度以及文件中的当前位置。注意文件结尾的返回和更多统计信息的设置。方法还记录当前位置,以便下一次调用方法时将文件推进到下一条记录。清单 10-22 显示了修改后的更新方法。

清单 10-22。 更改 ha_spartan.cc 中的 rnd_next()方法

int ha_spartan::rnd_next(uchar *buf)
{
  int rc;
  DBUG_ENTER("ha_spartan::rnd_next");
  MYSQL_READ_ROW_START(table_share->db.str, table_share->table_name.str,
                       TRUE);
  /*
    Read the row from the data file.
  */
  rc = share->data_class->read_row(buf, table->s->rec_buff_length,
                                   current_position);
  if (rc !=1)
    current_position = (off_t)share->data_class->cur_position();
  else
    DBUG_RETURN(HA_ERR_END_OF_FILE);
  stats.records++;
  MYSQL_READ_ROW_DONE(rc);
  DBUG_RETURN(rc);
}

Spartan_data类很好,因为它以与 MySQL 内部缓冲区相同的格式存储记录。事实上,它只是为每个记录写了几个 uchars 的头,存储了一个删除标志和记录长度(用于扫描和修复)。如果您正在使用一个以不同格式存储数据的存储引擎,那么此时您需要执行转换。在ha_tina.cc文件中可以找到如何完成翻译的示例。这个过程看起来像这样:

for (Field **field=table->field ; *field ; field++)
{
  /* copy field data to your own storage type */
  my_value = (*field)->val_str();
  my_store_field(my_value);
}

在这个例子中,您正在遍历field数组,以您自己的格式写出数据。寻找ha_tina::find_current_row()方法 的例子。

您需要更改的下一个方法是position(),它记录了文件在 MySQL 指针存储机制中的当前位置。它在每次调用rnd_next()后被调用。存储和检索这些指针的方法是my_store_ptr()my_get_ptr()。store-pointer 方法将一个引用变量(你想存储东西的地方)、你想存储的东西的长度和你想存储的东西作为参数。get-pointer 方法接受一个引用变量和所检索内容的长度,并返回存储的项。这些方法用于需要对数据进行排序的 order by 行的情况。看看清单 10-23 中的position()方法的变化,看看如何调用存储指针方法。

清单 10-23。 对 ha_spartan.cc 中 position()方法的修改

void ha_spartan::position(const uchar *record)
{
  DBUG_ENTER("ha_spartan::position");
  my_store_ptr(ref, ref_length, current_position);
  DBUG_VOID_RETURN;
}

您需要更改的下一个方法是rnd_pos() ,在这里您将检索存储的当前位置,然后从该位置读入行。注意,在这个方法中,我们还增加了读取统计数据ha_read_rnd_next_count。这为优化器提供了关于表中有多少行的信息,并且有助于优化后面的查询。清单 10-24 显示了经过修改的更新方法。

清单 10-24。 修改 ha_spartan.cc 中的 rnd_pos()方法

int ha_spartan::rnd_pos(uchar *buf, uchar *pos)
{
  int rc;
  DBUG_ENTER("ha_spartan::rnd_pos");
  MYSQL_READ_ROW_START(table_share->db.str, table_share->table_name.str,
                       TRUE);
  ha_statistic_increment(&SSV::ha_read_rnd_next_count);
  current_position = (off_t)my_get_ptr(pos,ref_length);
  rc = share->data_class->read_row(buf, current_position, -1);
  MYSQL_READ_ROW_DONE(rc);
  DBUG_RETURN(rc);
}

您需要更改的下一个方法是info() ,它将信息返回给优化器,以帮助选择最佳的执行路径。这是一个实现起来很有趣的方法,当你阅读源代码中的注释时,它会显得很幽默。在这个方法中你需要做的是返回记录的数量。Oracle 声明您应该总是返回 2 或更大的值。这将使优化器中浪费一行记录集的部分脱离。清单 10-25 显示了更新后的info()方法。

清单 10-25。 对 ha_spartan.cc 中 info()方法的修改

int ha_spartan::info(uint flag)
{
  DBUG_ENTER("ha_spartan::info");
  /* This is a lie, but you don't want the optimizer to see zero or 1 */
  if (stats.records < 2)
    stats.records= 2;
  DBUG_RETURN(0);
}

你需要改变的最后一个方法是write_row();您将再次使用Spartan_data类将数据写入数据文件。像读一样,Spartan_data类只需要把记录缓冲区写到磁盘,前面有一个删除状态标志和记录长度。清单 10-26 显示了修改后的更新方法。

清单 10-26。 对 ha_spartan.cc 中 write_row()方法的修改

int ha_spartan::write_row(uchar *buf)
{
  DBUG_ENTER("ha_spartan::write_row");
  long long pos;
  SDE_INDEX ndx;

  ha_statistic_increment(&SSV::ha_write_count);
  /*
    Begin critical section by locking the spartan mutex variable.
  */
  mysql_mutex_lock(&share->mutex);
  pos = share->data_class->write_row(buf, table->s->rec_buff_length);
/*
    End section by unlocking the spartan mutex variable.
  */
  mysql_mutex_unlock(&share->mutex);
  DBUG_RETURN(0);
}

请注意,我再次在写入操作周围放置了一个互斥体(例如,临界区),这样就不会有两个线程同时写入。现在是编译服务器和调试任何错误的好时机。完成后,您将拥有一个完整的阶段 3 存储引擎。剩下要做的就是编译服务器并运行测试。

测试斯巴达发动机的第三阶段

当您再次运行测试时,您应该看到所有语句都成功完成。如果你想知道为什么我总是从最后一个增量开始运行测试,那是因为你想确保没有新代码破坏旧代码正在做的任何事情。在这种情况下,您可以看到您仍然可以创建、重命名和删除表。现在,让我们继续测试读写操作。

测试这些功能很容易。启动服务器,然后启动 MySQL 客户端。如果您删除了测试表,请重新创建它,然后发出命令:

INSERT INTO t1 VALUES(1, "first test", 24);
INSERT INTO t1 VALUES(4, "second test", 43);
INSERT INTO t1 VALUES(3, "third test", -2);

在每条语句之后,您应该看到成功插入了记录。如果遇到错误(这是不应该的),启动调试器,在ha_spartan.cc文件的所有读写方法中设置断点,然后调试问题。除了ha_spartan.cc文件之外,您不应该再查看其他文件,因为这是唯一可能包含错误来源的文件。 6

现在您可以发出一个SELECT语句,看看服务器向您发回什么。输入命令:

SELECT * FROM t1;

您应该会看到返回的所有三行。清单 10-27 显示了运行查询的结果。

清单 10-27。 运行插入/选择语句的结果


mysql> INSERT INTO t1 VALUES(1, "first test", 24);
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO t1 VALUES(4, "second test", 43);
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO t1 VALUES(3, "third test", -2);
Query OK, 1 row affected (0.00 sec)

mysql> SELECT * FROM t1;
+−−-----+−−-----------+−−-----+
| col_a | col_b       | col_c |
+−−-----+−−-----------+−−-----+
|     1 | first test  |    24 |
|     4 | second test |    43 |
|     3 | third test  |    -2 |
+−−-----+−−-----------+−−-----+
3 rows in set (0.00 sec)

mysql>

现在,您已经验证了读取和写入工作正常,请将这些操作的测试添加到测试文件中,然后重新运行测试。测试应该没有错误地完成。清单 10-28 显示了更新后的Ch10s3.test文件。

清单 10-28。 更新了斯巴达-存储-引擎测试文件(Ch10s3.test)

#
# Simple test for the Spartan storage engine
#
--disable_warnings
drop table if exists t1;
--enable_warnings

CREATE TABLE t1 (
  col_a int,
  col_b varchar(20),
  col_c int
) ENGINE=SPARTAN;

SELECT * FROM t1;
INSERT INTO t1 VALUES(1, ìfirst testî, 24);
INSERT INTO t1 VALUES(4, ìsecond testî, 43);
INSERT INTO t1 VALUES(3, ìthird testî, -2);
SELECT * FROM t1;
RENAME TABLE t1 TO t2;
SELECT * FROM t2;
DROP TABLE t2;

这是第三阶段的引擎。它现在是一个基本的读/写存储引擎,可以完成读写数据的所有基本需求。在下一阶段,我们将添加更新和删除数据的功能。

阶段 4:更新和删除数据

这个阶段的目标是产生一个可以更新和删除数据的工作存储引擎。在这个阶段,我将向您展示如何整合用于更新和删除数据的Spartan_data类。我将带您经历每一个变更,并包括每一个变更的完整方法源代码。

Spartan_data类就地执行更新。也就是说,旧数据被新数据覆盖。删除是通过将数据标记为已删除并在读取时跳过已删除的记录来执行的。Spartan_data类中的read_row()方法跳过被删除的行。这似乎会浪费很多空间,如果存储引擎用于有大量删除和插入的情况,这可能是真的。为了减少这种可能性,您总是可以转储然后删除表,并从转储中重新加载数据。这将删除空记录。取决于您计划如何构建自己的存储引擎,这可能是您需要重新考虑的事情。

更新斯巴达源文件

这个阶段要求您更新update_row()delete_row()delete_all_rows()方法。delete_all_rows()方法是一种节省时间的方法,用于一次清空一个表,而不是一次清空一行。对于截断操作以及检测到批量删除查询时,优化器可能会调用此方法。

更新头文件

对于阶段 4 存储引擎,不需要对ha_spartan.h文件进行任何更改。

更新源文件

打开ha_spartan.cc文件并找到update_row()方法。该方法将旧记录和新记录缓冲区作为参数传递。这很好,因为我们没有索引,必须进行表扫描来定位记录!幸运的是,Spartan_data类有update_row()方法可以为您完成这项工作。清单 10-29 显示了修改后的更新方法。

清单 10-29。 更改 ha_spartan.cc 中的 update_row()方法

/* update a record in place */
long long Spartan_data::update_row(uchar *old_rec, uchar *new_rec,
                                   int length, long long position)
{

  DBUG_ENTER("ha_spartan::update_row");
  /*
    Begin critical section by locking the spartan mutex variable.
  */
  mysql_mutex_lock(&share->mutex);
  share->data_class->update_row((uchar *)old_data, new_data,
                 table->s->rec_buff_length, current_position -
                 share->data_class->row_size(table->s->rec_buff_length));
  /*
    End section by unlocking the spartan mutex variable.
  */
  mysql_mutex_unlock(&share->mutex);
  DBUG_RETURN(0);
 }

delete_row()方法类似于更新方法。在这种情况下,我们调用Spartan_data类中的delete_row()方法,传入要删除的行的缓冲区、记录缓冲区的长度以及当前位置的-1来强制表扫描。数据类方法再次为您完成了所有繁重的工作。清单 10-30 显示了修改后的更新方法。

清单 10-30。 修改 ha_spartan.cc 中的 delete_row()方法

int ha_spartan::update_row(const uchar *old_data, uchar *new_data)
{

  DBUG_ENTER("ha_spartan::update_row");
  /*
    Begin critical section by locking the spartan mutex variable.
  */
  mysql_mutex_lock(&share->mutex);
  share->data_class->update_row((uchar *)old_data, new_data,
                 table->s->rec_buff_length, current_position -
                 share->data_class->row_size(table->s->rec_buff_length));
  /*
    End section by unlocking the spartan mutex variable.
  */
  mysql_mutex_unlock(&share->mutex);
  DBUG_RETURN(0);
}

你需要更新的最后一个方法是delete_all_rows() 。这将删除表中的所有数据。最简单的方法是删除数据文件并重新创建它。Spartan_data类的做法略有不同。trunc_table()方法将文件指针重置到文件的开头,并使用my_chsize()方法截断文件。清单 10-31 显示了修改后的更新方法。

清单 10-31。 修改 ha_spartan.cc 中的 delete_all_rows()方法

int ha_spartan::delete_all_rows()
{
  DBUG_ENTER("ha_spartan::delete_all_rows");
  /*
    Begin critical section by locking the spartan mutex variable.
  */
  mysql_mutex_lock(&share->mutex);
  share->data_class->trunc_table();
  /*
    End section by unlocking the spartan mutex variable.
  */
  mysql_mutex_unlock(&share->mutex);
  DBUG_RETURN(0);
}

现在编译服务器并调试任何错误。完成后,你就有了一个完整的第四阶段引擎。剩下要做的就是编译服务器并运行测试。

测试斯巴达发动机的第 4 阶段

首先,验证 Stage 3 引擎中的一切都工作正常,然后继续测试更新和删除操作。当您再次运行测试时,您应该看到所有语句都成功完成。

更新和删除测试要求您创建一个表,并在其中包含数据。您可以像以前一样使用普通的INSERT语句添加数据。您可以随意添加自己的数据,并在表格中再添加几行。

当表中有一些数据时,选择其中一条记录,并使用类似下面的命令对其发出更新命令:

UPDATE t1 SET col_b = "Updated!" WHERE col_a = 1;

当您运行该命令后接一个SELECT *命令时,您应该看到该行被更新。然后,您可以通过发出 delete 命令来删除行,例如:

DELETE FROM t1 WHERE col_a = 3;

当您运行该命令后接一个SELECT *命令时,您应该看到该行已经被删除。这个命令序列将产生的结果的一个例子是:。

mysql> DELETE FROM t1 WHERE col_a = 3;
Query OK, 1 row affected (0.01 sec)

mysql> SELECT * FROM t1;
+−−-----+−−-----------+−−-----+
| col_a | col_b       | col_c |
+−−-----+−−-----------+−−-----+
|     4 | second test |    43 |
|     4 | tenth test  |    11 |
|     5 | Updated!    |   100 |
+−−-----+−−-----------+−−-----+
3 rows in set (0.00 sec)

mysql>

我们错过了什么吗?精明的软件开发者可能会注意到这个测试并不全面,也没有涵盖Spartan_data类必须考虑的所有可能性。例如,删除数据中间的一行不同于删除文件开头或结尾的一行。更新数据也是一样。

这没关系,因为您可以将该功能添加到测试文件中。您可以添加更多的INSERT语句来添加更多的数据,然后更新第一行和最后一行以及中间的一行。您可以对删除操作进行同样的操作。清单 10-32 显示了更新后的Ch10s4.test文件。

清单 10-32。 更新了斯巴达-存储-引擎测试文件(Ch10s4.test)

#
# Simple test for the Spartan storage engine
#
--disable_warnings
drop table if exists t1;
--enable_warnings

CREATE TABLE t1 (
  col_a int,
  col_b varchar(20),
  col_c int
) ENGINE=SPARTAN;

SELECT * FROM t1;
INSERT INTO t1 VALUES(1, ìfirst testî, 24);
INSERT INTO t1 VALUES(4, ìsecond testî, 43);
INSERT INTO t1 VALUES(3, ìfourth testî, -2);
INSERT INTO t1 VALUES(4, ìtenth testî, 11);
INSERT INTO t1 VALUES(1, ìseventh testî, 20);
INSERT INTO t1 VALUES(5, ìthird testî, 100);
SELECT * FROM t1;
UPDATE t1 SET col_b = ìUpdated!î WHERE col_a = 1;
SELECT * from t1;
UPDATE t1 SET col_b = ìUpdated!î WHERE col_a = 3;
SELECT * from t1;
UPDATE t1 SET col_b = ìUpdated!î WHERE col_a = 5;
SELECT * from t1;
DELETE FROM t1 WHERE col_a = 1;
SELECT * FROM t1;
DELETE FROM t1 WHERE col_a = 3;
SELECT * FROM t1;
DELETE FROM t1 WHERE col_a = 5;
SELECT * FROM t1;
RENAME TABLE t1 TO t2;
SELECT * FROM t2;
DROP TABLE t2;

请注意,我添加了一些具有重复值的行。您应该预料到服务器会更新并删除重复行的所有匹配项。运行该测试,看看它会做什么。清单 10-33 显示了这个测试的预期结果的一个例子。当您在测试套件下运行测试时,它应该没有错误地完成。

清单 10-33。 第四阶段测试的样本结果

mysql> INSTALL PLUGIN spartan SONAME 'ha_spartan.so';
Query OK, 0 rows affected (0.01 sec)

mysql> use test;
Database changed
mysql>
mysql> CREATE TABLE t1 (
    ->   col_a int,
    ->   col_b varchar(20),
    ->   col_c int
    -> ) ENGINE=SPARTAN;
Query OK, 0 rows affected (0.01 sec)

mysql>
mysql> SELECT * FROM t1;
Empty set (0.00 sec)

mysql> INSERT INTO t1 VALUES(1, "first test", 24);
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO t1 VALUES(4, "second test", 43);
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO t1 VALUES(3, "fourth test", -2);
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO t1 VALUES(4, "tenth test", 11);
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO t1 VALUES(1, "seventh test", 20);
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO t1 VALUES(5, "third test", 100);
Query OK, 1 row affected (0.00 sec)

mysql> SELECT * FROM t1;
+−−-----+−−------------+−−-----+
| col_a | col_b        | col_c |
+−−-----+−−------------+−−-----+
|     1 | first test   |    24 |
|     4 | second test  |    43 |
|     3 | fourth test  |    -2 |
|     4 | tenth test   |    11 |
|     1 | seventh test |    20 |
|     5 | third test   |   100 |
+−−-----+−−------------+−−-----+
6 rows in set (0.00 sec)

mysql> UPDATE t1 SET col_b = "Updated!" WHERE col_a = 1;
Query OK, 2 rows affected (0.00 sec)
Rows matched: 2  Changed: 2  Warnings: 0

mysql> SELECT * from t1;
+−−-----+−−-----------+−−-----+
| col_a | col_b       | col_c |
+−−-----+−−-----------+−−-----+
|     1 | Updated!    |    24 |
|     4 | second test |    43 |
|     3 | fourth test |    -2 |
|     4 | tenth test  |    11 |
|     1 | Updated!    |    20 |
|     5 | third test  |   100 |
+−−-----+−−-----------+−−-----+
6 rows in set (0.00 sec)

mysql> UPDATE t1 SET col_b = "Updated!" WHERE col_a = 3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT * from t1;
+−−-----+−−-----------+−−-----+
| col_a | col_b       | col_c |
+−−-----+−−-----------+−−-----+
|     1 | Updated!    |    24 |
|     4 | second test |    43 |
|     3 | Updated!    |    -2 |
|     4 | tenth test  |    11 |
|     1 | Updated!    |    20 |
|     5 | third test  |   100 |
+−−-----+−−-----------+−−-----+
6 rows in set (0.01 sec)

mysql> UPDATE t1 SET col_b = "Updated!" WHERE col_a = 5;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT * from t1;
+−−-----+−−-----------+−−-----+
| col_a | col_b       | col_c |
+−−-----+−−-----------+−−-----+
|     1 | Updated!    |    24 |
|     4 | second test |    43 |
|     3 | Updated!    |    -2 |
|     4 | tenth test  |    11 |
|     1 | Updated!    |    20 |
|     5 | Updated!    |   100 |
+−−-----+−−-----------+−−-----+
6 rows in set (0.00 sec)

mysql> DELETE FROM t1 WHERE col_a = 1;
Query OK, 2 rows affected (0.00 sec)

mysql> SELECT * FROM t1;
+−−-----+−−-----------+−−-----+
| col_a | col_b       | col_c |
+−−-----+−−-----------+−−-----+
|     4 | second test |    43 |
|     3 | Updated!    |    -2 |
|     4 | tenth test  |    11 |
|     5 | Updated!    |   100 |
+−−-----+−−-----------+−−-----+
4 rows in set (0.00 sec)

mysql> DELETE FROM t1 WHERE col_a = 3;
Query OK, 1 row affected (0.01 sec)

mysql> SELECT * FROM t1;
+−−-----+−−-----------+−−-----+
| col_a | col_b       | col_c |
+−−-----+−−-----------+−−-----+
|     4 | second test |    43 |
|     4 | tenth test  |    11 |
|     5 | Updated!    |   100 |
+−−-----+−−-----------+−−-----+
3 rows in set (0.00 sec)

mysql> DELETE FROM t1 WHERE col_a = 5;
Query OK, 1 row affected (0.00 sec)

mysql> SELECT * FROM t1;
+−−-----+−−-----------+−−-----+
| col_a | col_b       | col_c |
+−−-----+−−-----------+−−-----+
|     4 | second test |    43 |
|     4 | tenth test  |    11 |
+−−-----+−−-----------+−−-----+
2 rows in set (0.00 sec)

mysql> RENAME TABLE t1 TO t2;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM t2;
+−−-----+−−-----------+−−-----+
| col_a | col_b       | col_c |
+−−-----+−−-----------+−−-----+
|     4 | second test |    43 |
|     4 | tenth test  |    11 |
+−−-----+−−-----------+−−-----+
2 rows in set (0.00 sec)

mysql> DROP TABLE t2;
Query OK, 0 rows affected (0.00 sec)

mysql>

这就是四级发动机。它现在是一个基本的读/写/更新/删除存储引擎。在下一阶段,我们将添加 index 类来提高查询效率。

阶段 5:索引数据

这个阶段的目标是产生一个工作的存储引擎,它包括对单个索引的支持(只需做一点工作,您就可以让它拥有多个索引)。在这个阶段,我将向您展示如何结合Spartan_index类来索引数据。需要做出许多改变。我建议在开始跟随变化之前通读这一部分。

首先将Spartan_index类文件添加到 CMakeLists.txt 文件中,如下所示。

# Spartan storage engine plugin

SET(SPARTAN_PLUGIN_STATIC "spartan")
SET(SPARTAN_PLUGIN_DYNAMIC "spartan")

SET(SPARTAN_SOURCES
   ha_spartan.cc ha_spartan.h
   spartan_data.cc spartan_data.h
   spartan_index.cc spartan_index.h
)

MYSQL_ADD_PLUGIN(spartan ${SPARTAN_SOURCES} STORAGE_ENGINE MODULE_ONLY)

TARGET_LINK_LIBRARIES(spartan mysys)

Spartan_index类通过保存指向Spartan_data类中相应行的记录指针来工作。当服务器通过主键搜索记录时,它可以使用Spartan_index类找到记录指针,然后通过Spartan_data类发出直接读取调用来直接访问记录。这使得读取随机记录的过程比执行表扫描快得多。

本节中的源代码设计用于最基本的索引操作。根据您的查询变得有多复杂,这些更改应该足以满足大多数情况。我将带您了解每个变更,并包括每个变更的完整方法源代码。

更新斯巴达源文件

Spartan_index类只是保存文件的当前位置和键。您需要更新的ha_spartan.cc中的方法包括index_read()index_read_idx()index_next()index_prev()index_first()index_last()。这些方法从索引中读取值并遍历索引,以及转到索引的前面和后面(开始,结束)。幸运的是,Spartan_index类提供了所有这些操作。

更新头文件

要使用 index 类,首先在ha_spartan.h头文件中添加对spartan_index.h文件的引用。清单 10-34 显示了完整的代码变更(为了简洁,我省略了注释)。一旦你做了这个改变,重新编译spartan源文件以确保没有任何错误。

清单 10-34。 修改为 ha_spartan.h 中的 Spartan_share 类

#include "my_global.h"                   /* ulonglong */
#include "thr_lock.h"                    /* THR_LOCK, THR_LOCK_DATA */
#include "handler.h"                     /* handler */
#include "spartan_data.h"
#include "spartan_index.h"

class Spartan_share : public Handler_share {
public:
  mysql_mutex_t mutex;
  THR_LOCK lock;
  Spartan_data *data_class;
  Spartan_index *index_class;
  Spartan_share();
  ∼Spartan_share()
  {
    thr_lock_delete(&lock);
    mysql_mutex_destroy(&mutex);
    if (data_class != NULL)
      delete data_class;
    data_class = NULL;
    if (index_class != NULL)
      delete index_class;
    index_class = NULL;
  }
};
...

打开ha_spartan.h文件并添加#include指令以包含spartan_index.h头文件,如上所示。

完成后,打开 ha_spartan.cc 文件,将索引类初始化添加到构造函数中..清单 10-35 显示了完整的代码变更。一旦你做了这个改变,重新编译spartan源文件以确保没有任何错误。

清单 10-35。 更改 ha_spartan.cc 中的 Spartan_data 构造函数

Spartan_share::Spartan_share()
{
  thr_lock_init(&lock);
  mysql_mutex_init(ex_key_mutex_Spartan_share_mutex,
                   &mutex, MY_MUTEX_INIT_FAST);
  data_class = new Spartan_data();
  index_class = new Spartan_index();
}

当头文件打开时,您需要做一些其他的修改。您必须添加标志来告诉优化器支持哪些索引操作。您还必须设置索引参数的界限:支持的最大键数、键的最大长度和最大键部分。在此阶段,如清单 10-36 所示设置参数。我已经包含了您需要对文件进行的全部更改。注意table_flags()方法。这是您告诉优化器存储引擎有什么限制的地方。我已经将引擎设置为不允许 BLOBs,也不允许自动递增字段。这些标志的完整列表可在handler.h中找到。

清单 10-36。 对 ha_spartan.h 中 ha_spartan 类定义的修改

  /*
    The name of the index type that will be used for display
    don't implement this method unless you really have indexes
   */
  const char *index_type(uint inx) { return "Spartan_index"; }
  /*
    The file extensions.
  */
  const char **bas_ext() const;
  /*
    This is a list of flags that says what the storage engine
    implements. The current table flags are documented in
    handler.h
  */
  ulonglong table_flags() const
  {
    return (HA_NO_BLOBS | HA_NO_AUTO_INCREMENT | HA_BINLOG_STMT_CAPABLE);
  }
  /*
    This is a bitmap of flags that says how the storage engine
    implements indexes. The current index flags are documented in
    handler.h. If you do not implement indexes, just return zero
    here.

    part is the key part to check. First key part is 0
    If all_parts it's set, MySQL want to know the flags for the combined
    index up to and including 'part'.
  */
  ulong index_flags(uint inx, uint part, bool all_parts) const
  {
    return (HA_READ_NEXT | HA_READ_PREV | HA_READ_RANGE |
            HA_READ_ORDER | HA_KEYREAD_ONLY);
  }
  /*
    unireg.cc will call the following to make sure that the storage engine can
    handle the data it is about to send.

    Return *real* limits of your storage engine here. MySQL will do
    min(your_limits, MySQL_limits) automatically

    There is no need to implement ..._key_... methods if you don't suport
    indexes.
  */
  uint max_supported_keys()          const { return 1; }
  uint max_supported_key_parts()     const { return 1; }
  uint max_supported_key_length()    const { return 128; }

如果您在一个用 Spartan 引擎创建的表上执行 SHOW INDEXES FROM 命令,您将会看到上述代码更改的结果,如清单 10-37 所示。请注意输出中报告的索引类型。

清单 10-37。 显示指标输出的例子

mysql> show indexes from test.t1 \G
*************************** 1\. row ***************************
        Table: t1
   Non_unique: 0
     Key_name: PRIMARY
 Seq_in_index: 1
  Column_name: col_a
    Collation: A
  Cardinality: NULL
     Sub_part: NULL
       Packed: NULL
         Null:
   Index_type: Spartan_index
      Comment:
Index_comment:
1 row in set (0.00 sec)

最后需要补充一点。识别记录中的键很容易,但不是很直观。为了使事情更容易处理,我编写了两个助手方法:get_key(),它查找键字段并返回其值,如果没有键,则返回 0;以及get_key_len(),它返回键的长度。将它们的定义添加到类头文件中(ha_spartan.h):

uchar *get_key();
int get_key_len();

您将在ha_spartan.cc类文件中实现这些方法。

更新类文件

现在是编译和检查错误的好时机。完成后,开始修改索引方法。

首先,回顾 open、create、close、write、update、delete 和 rename 方法,并添加对 index 类的调用来维护索引。完成这项工作的代码包括识别作为键的字段,然后将键及其位置保存到索引中,以便以后检索。

open 方法必须同时打开数据和索引文件。唯一的额外步骤是将索引加载到内存中。在类文件中找到open()方法,并添加对 index 类的调用,以打开索引并将索引加载到内存中。清单 10-38 显示了修改后的方法。

清单 10-38。 对 ha_spartan.cc 中 open()方法的修改

int ha_spartan::open(const char *name, int mode, uint test_if_locked)
{
  DBUG_ENTER("ha_spartan::open");
  char name_buff[FN_REFLEN];

  if (!(share = get_share()))
    DBUG_RETURN(1);
  /*
    Call the data class open table method.
    Note: the fn_format() method correctly creates a file name from the
    name passed into the method.
  */
  share->data_class->open_table(fn_format(name_buff, name, "", SDE_EXT,
                                MY_REPLACE_EXT|MY_UNPACK_FILENAME));
  share->index_class->open_index(fn_format(name_buff, name, "", SDI_EXT,
                                MY_REPLACE_EXT|MY_UNPACK_FILENAME));
  share->index_class->load_index();
  thr_lock_data_init(&share->lock,&lock,NULL);
  DBUG_RETURN(0);
}

create 方法必须同时创建数据和索引文件。在类文件中找到create()方法,并添加对 index 类的调用以创建索引。清单 10-39 显示了修改后的方法。

清单 10-39。 对 ha_spartan.cc 中 create()方法的修改

int ha_spartan::create(const char *name, TABLE *table_arg,
                       HA_CREATE_INFO *create_info)
{
  DBUG_ENTER("ha_spartan::create");
  char name_buff[FN_REFLEN];
  char name_buff2[FN_REFLEN];

  if (!(share = get_share()))
    DBUG_RETURN(1);
  /*
    Call the data class create table method.
    Note: the fn_format() method correctly creates a file name from the
    name passed into the method.
  */
  if (share->data_class->create_table(fn_format(name_buff, name, "", SDE_EXT,
                                      MY_REPLACE_EXT|MY_UNPACK_FILENAME)))
    DBUG_RETURN(−1);
   share->data_class->close_table();
   if (share->index_class->create_index(fn_format(name_buff2, name, "", SDI_EXT,
                                      MY_REPLACE_EXT|MY_UNPACK_FILENAME),
                                      128))
  {
     DBUG_RETURN(−1);
  }
  share->index_class->close_index();
  DBUG_RETURN(0);
}

close 方法必须同时关闭数据和索引文件。由于 index 类使用内存中的结构来存储所有更改,因此必须将其写回磁盘。在类文件中找到close()方法,并添加对 index 类的调用,用于保存、销毁内存结构和关闭索引。清单 10-40 显示了修改后的方法。

清单 10-40。 对 ha_spartan.cc 中 close()方法的修改

int ha_spartan::close(void)
{
  DBUG_ENTER("ha_spartan::close");
  share->data_class->close_table();
  share->index_class->save_index();
  share->index_class->destroy_index();
  share->index_class->close_index();
  DBUG_RETURN(0);
 }

现在让我们改变写作和阅读方法。因为有可能不使用任何键,所以该方法必须检查是否有要添加的键。为了使事情更容易处理,我编写了两个助手方法:get_key(),它查找键字段并返回其值,如果没有键,则返回 0;以及get_key_len(),它返回键的长度。清单 10-41 展示了这两个助手方法。现在将这些方法添加到ha_spartan.cc文件中。

列举 10-41。 附加辅助方法在 ha_spartan.cc 中

uchar *ha_spartan::get_key()
{
  uchar *key = 0;

  DBUG_ENTER("ha_spartan::get_key");
  /*
    For each field in the table, check to see if it is the key
    by checking the key_start variable. (1 = is a key).
  */
  for (Field **field=table->field ; *field ; field++)
  {
    if ((*field)->key_start.to_ulonglong() == 1)
    {
      /*
        Copy field value to key value (save key)
      */
      key = (uchar *)my_malloc((*field)->field_length,
                                  MYF(MY_ZEROFILL | MY_WME));
      memcpy(key, (*field)->ptr, (*field)->key_length());
    }
  }
  DBUG_RETURN(key);
}

int ha_spartan::get_key_len()
{
  int length = 0;

  DBUG_ENTER("ha_spartan::get_key");
  /*
    For each field in the table, check to see if it is the key
    by checking the key_start variable. (1 = is a key).
  */
  for (Field **field=table->field ; *field ; field++)
  {
    if ((*field)->key_start.to_ulonglong() == 1)
      /*
        Copy field length to key length
      */
      length = (*field)->key_length();
  }
  DBUG_RETURN(length);
}

write 方法必须将记录写入数据文件,并将键插入索引文件。在类文件中找到write_row()方法,并添加对 index 类的调用,以插入键(如果找到的话)。清单 10-42 显示了修改后的方法。

清单 10-42。 对 ha_spartan.cc 中 write_row()方法的修改

int ha_spartan::write_row(uchar *buf)
{
  DBUG_ENTER("ha_spartan::write_row");
  long long pos;
  SDE_INDEX ndx;

  ha_statistic_increment(&SSV::ha_write_count);
  /*
    Begin critical section by locking the spartan mutex variable.
  */
  mysql_mutex_lock(&share->mutex);
  ndx.length = get_key_len();
  memcpy(ndx.key, get_key(), get_key_len());
  pos = share->data_class->write_row(buf, table->s->rec_buff_length);
  ndx.pos = pos;
  if ((ndx.key != 0) && (ndx.length != 0))
    share->index_class->insert_key(&ndx, false);
  /*
    End section by unlocking the spartan mutex variable.
  */
  mysql_mutex_unlock(&share->mutex);
  DBUG_RETURN(0);
}

更新方式也有点不同。它必须更改数据文件中的记录和索引中的键。因为索引使用内存结构,所以必须更改索引文件,将其保存到磁盘,然后重新加载。

image 注意精明的程序员会在代码中为Spartan_index注意到一些可以防止重载步骤的东西。你知道是什么吗?这里有一个提示:如果 index-class update 方法更新了键,然后在内存结构中重新定位它会怎么样?我将把那个实验留给你。进入索引代码并改进它。

在类文件中找到update_row()方法,并添加对 index 类的调用以更新键(如果找到的话)。清单 10-43 显示了修改后的方法。

清单 10-43。 对 ha_spartan.cc 中 update_row()方法的修改

int ha_spartan::update_row(const uchar *old_data, uchar *new_data)
{

  DBUG_ENTER("ha_spartan::update_row");
  /*
    Begin critical section by locking the spartan mutex variable.
  */
  mysql_mutex_lock(&share->mutex);
  share->data_class->update_row((uchar *)old_data, new_data,
                 table->s->rec_buff_length, current_position -
                 share->data_class->row_size(table->s->rec_buff_length));
  if (get_key() != 0)
  {
    share->index_class->update_key(get_key(), current_position -
                   share->data_class->row_size(table->s->rec_buff_length),
                   get_key_len());
    share->index_class->save_index();
    share->index_class->load_index();
  }
  /*
    End section by unlocking the spartan mutex variable.
  */
  mysql_mutex_unlock(&share->mutex);
  DBUG_RETURN(0);
}

删除方法并不复杂。在这种情况下,该方法只需要删除数据行,并从内存结构中移除索引(如果找到的话)。在类文件中找到delete_row()方法,并添加对 index 类的调用以删除键(如果找到的话)。清单 10-44 显示了修改后的方法。

清单 10-44。 修改 ha_spartan.cc 中的 delete_row()方法

int ha_spartan::delete_row(const uchar *buf)
{
  DBUG_ENTER("ha_spartan::delete_row");
  long long pos;

  if (current_position > 0)
    pos = current_position -
      share->data_class->row_size(table->s->rec_buff_length);
  else
    pos = 0;
  /*
    Begin critical section by locking the spartan mutex variable.
  */
  mysql_mutex_lock(&share->mutex);
  share->data_class->delete_row((uchar *)buf,
                                table->s->rec_buff_length, pos);
  if (get_key() != 0)
    share->index_class->delete_key(get_key(), pos, get_key_len());
  /*
    End section by unlocking the spartan mutex variable.
  */
  mysql_mutex_unlock(&share->mutex);
  DBUG_RETURN(0);
}

同样,删除所有行的方法也非常简单。在这种情况下,我们希望删除数据和索引文件中的所有数据。在类文件中找到delete_all_rows()方法,并添加对 index 类的调用,以销毁索引并截断索引文件。清单 10-45 显示了修改后的方法。

清单 10-45。 修改 ha_spartan.cc 中的 delete_all_rows()方法

int ha_spartan::delete_all_rows()
{
  DBUG_ENTER("ha_spartan::delete_all_rows");
  /*
    Begin critical section by locking the spartan mutex variable.
  */
  mysql_mutex_lock(&share->mutex);
  share->data_class->trunc_table();
  share->index_class->destroy_index();
  share->index_class->trunc_index();
  /*
    End section by unlocking the spartan mutex variable.
  */
  mysql_mutex_unlock(&share->mutex);
  DBUG_RETURN(0);
}

delete_table()方法必须删除数据和索引文件。找到delete_table()方法并添加代码来调用my_delete()函数来删除索引。清单 10-46 显示了修改后的方法。

清单 10-46。 修改 ha_spartan.cc 中的 delete_table()方法

int ha_spartan::delete_table(const char *name)
{
  DBUG_ENTER("ha_spartan::delete_table");
  char name_buff[FN_REFLEN];

  /*
    Call the mysql delete file method.
    Note: the fn_format() method correctly creates a file name from the
    name passed into the method.
  */
  my_delete(fn_format(name_buff, name, "", SDE_EXT,
            MY_REPLACE_EXT|MY_UNPACK_FILENAME), MYF(0));
  /*
    Call the mysql delete file method.
    Note: the fn_format() method correctly creates a file name from the
    name passed into the method.
  */
  my_delete(fn_format(name_buff, name, "", SDI_EXT,
            MY_REPLACE_EXT|MY_UNPACK_FILENAME), MYF(0));

  DBUG_RETURN(0);
}

对常规读写文件操作的最后一个更改是对rename_table()方法的更改。索引的rename_table()方法遵循与前面的更改相同的模式。在类文件中找到rename_table()方法,并添加代码来复制索引文件。清单 10-47 显示了修改后的方法。

清单 10-47。 修改 ha_spartan.cc 中的 rename_table()方法

int ha_spartan::rename_table(const char * from, const char * to)
{
  DBUG_ENTER("ha_spartan::rename_table ");
  char data_from[FN_REFLEN];
  char data_to[FN_REFLEN];
  char index_from[FN_REFLEN];
  char index_to[FN_REFLEN];

  my_copy(fn_format(data_from, from, "", SDE_EXT,
          MY_REPLACE_EXT|MY_UNPACK_FILENAME),
          fn_format(data_to, to, "", SDE_EXT,
          MY_REPLACE_EXT|MY_UNPACK_FILENAME), MYF(0));
  my_copy(fn_format(index_from, from, "", SDI_EXT,
          MY_REPLACE_EXT|MY_UNPACK_FILENAME),
          fn_format(index_to, to, "", SDI_EXT,
          MY_REPLACE_EXT|MY_UNPACK_FILENAME), MYF(0));
  /*
    Delete the file using MySQL's delete file method.
  */
  my_delete(data_from, MYF(0));
  my_delete(index_from, MYF(0));

  DBUG_RETURN(0);
}

哇哦!变化真大。如您所见,支持索引使得代码变得更加复杂。我希望您现在能更好地理解 MySQL 中现有的存储引擎是如何构建的。现在,让我们继续对索引方法进行更改。

必须实现几种方法来完成第 5 阶段存储引擎的索引机制。请注意,在使用这些方法时,有些方法根据传入的索引从数据文件中返回一行,而有些方法返回一个键。文档对此并不清楚,参数的名称也没有给我们多少线索,但是我会向您展示它们是如何使用的。这些方法必须返回未找到的键或文件结束返回代码。注意正确编写这些 return 语句,否则您可能会遇到一些奇怪的查询结果。

第一种方法是index_read_map()法。这会将行缓冲区设置为文件中与传入的键相匹配的行。如果传入的键为 null,该方法应该返回文件中的第一个键值。找到index_read_map()方法,添加代码从索引中获取文件位置,并从数据文件中读取相应的行。清单 10-48 显示了修改后的方法。

清单 10-48。 更改 ha_spartan.cc 中的 index_read_map()方法

int ha_spartan::index_read_map(uchar *buf, const uchar *key,
                               key_part_map keypart_map __attribute__((unused)),
                               enum ha_rkey_function find_flag
                               __attribute__((unused)))
{
  int rc;
  long long pos;
  DBUG_ENTER("ha_spartan::index_read");
  MYSQL_INDEX_READ_ROW_START(table_share->db.str, table_share->table_name.str);
  if (key == NULL)
    pos = share->index_class->get_first_pos();
  else
    pos = share->index_class->get_index_pos((uchar *)key, keypart_map);
  if (pos ==1)
    DBUG_RETURN(HA_ERR_KEY_NOT_FOUND);
  current_position = pos + share->data_class->row_size(table->s->rec_buff_length);
  rc = share->data_class->read_row(buf, table->s->rec_buff_length, pos);
  share->index_class->get_next_key();
  MYSQL_INDEX_READ_ROW_DONE(rc);
  DBUG_RETURN(rc);
}

接下来的索引方法是index_next()。此方法获取索引中的下一个键,并从数据文件中返回匹配的行。它在范围索引扫描期间被调用。找到index_next()方法,添加代码从索引中获取下一个键,并从数据文件中读取一行。清单 10-49 显示了修改后的方法。

清单 10-49。 更改 ha_spartan.cc 中的 index_next()方法

int ha_spartan::index_next(uchar *buf)
{
  int rc;
  uchar *key = 0;
  long long pos;

  DBUG_ENTER("ha_spartan::index_next");
  MYSQL_INDEX_READ_ROW_START(table_share->db.str, table_share->table_name.str);
  key = share->index_class->get_next_key();
  if (key == 0)
    DBUG_RETURN(HA_ERR_END_OF_FILE);
  pos = share->index_class->get_index_pos((uchar *)key, get_key_len());
  share->index_class->seek_index(key, get_key_len());
  share->index_class->get_next_key();
  if (pos ==1)
    DBUG_RETURN(HA_ERR_KEY_NOT_FOUND);
  rc = share->data_class->read_row(buf, table->s->rec_buff_length, pos);
  MYSQL_INDEX_READ_ROW_DONE(rc);
  DBUG_RETURN(rc);
}

下一个索引方法也是一个范围查询。index_prev()方法获取索引中的前一个键,并从数据文件中返回匹配的行。它在范围索引扫描期间被调用。找到index_prev()方法,添加代码以从索引中获取前一个键,并从数据文件中读取一行。清单 10-50 显示了修改后的方法。

清单 10-50。 对 ha_spartan.cc 中 index_prev()方法的修改

int ha_spartan::index_prev(uchar *buf)
{
  int rc;
  uchar *key = 0;
  long long pos;

  DBUG_ENTER("ha_spartan::index_prev");
  MYSQL_INDEX_READ_ROW_START(table_share->db.str, table_share->table_name.str);
  key = share->index_class->get_prev_key();
  if (key == 0)
    DBUG_RETURN(HA_ERR_END_OF_FILE);
  pos = share->index_class->get_index_pos((uchar *)key, get_key_len());
  share->index_class->seek_index(key, get_key_len());
  share->index_class->get_prev_key();
  if (pos ==1)
    DBUG_RETURN(HA_ERR_KEY_NOT_FOUND);
  rc = share->data_class->read_row(buf, table->s->rec_buff_length, pos);
  MYSQL_INDEX_READ_ROW_DONE(rc);
  DBUG_RETURN(rc);
}

请注意,我不得不稍微移动一下索引指针,以使下一个和上一个代码能够工作。第一次使用 index 类时,范围查询会生成对它的两个调用:第一个调用获取第一个键(index_read),然后第二个调用下一个键(index_next)。随后对index_next()进行索引调用。因此,我必须调用Spartan_index类方法get_prev_key()来正确重置键。这将是重新设计 index 类以更好地处理 MySQL 中的范围查询的又一个好机会。

下一个索引方法也是一个范围查询。方法获取索引中的第一个键并返回它。找到index_first()方法,添加代码以从索引中获取第一个键并返回该键。清单 10-51 显示了修改后的方法。

清单 10-51。 更改 ha_spartan.cc 中的 index_first()方法

int ha_spartan::index_first(uchar *buf)
{
  int rc;
  uchar *key = 0;
  DBUG_ENTER("ha_spartan::index_first");
  MYSQL_INDEX_READ_ROW_START(table_share->db.str, table_share->table_name.str);
  key = share->index_class->get_first_key();
  if (key == 0)
    DBUG_RETURN(HA_ERR_END_OF_FILE);
  else
    rc = 0;
  memcpy(buf, key, get_key_len());
  MYSQL_INDEX_READ_ROW_DONE(rc);
  DBUG_RETURN(rc);
}

最后一个索引方法也是范围查询之一。方法获取索引中的最后一个键并返回它。找到index_last()方法并添加代码以从索引中获取最后一个键并返回该键。清单 10-52 显示了修改后的方法。

清单 10-52。 对 ha_spartan.cc 中 index_last()方法的修改

int ha_spartan::index_last(uchar *buf)
{
  int rc;
  uchar *key = 0;

  DBUG_ENTER("ha_spartan::index_last");
  MYSQL_INDEX_READ_ROW_START(table_share->db.str, table_share->table_name.str);
  key = share->index_class->get_last_key();
  if (key == 0)
    DBUG_RETURN(HA_ERR_END_OF_FILE);
  else
    rc = 0;
  memcpy(buf, key, get_key_len());
  MYSQL_INDEX_READ_ROW_DONE(rc);
  DBUG_RETURN(rc);
}

现在编译服务器并调试任何错误。完成后,你将拥有一个完整的第五阶段引擎。剩下要做的就是编译服务器并运行测试。

如果您决定调试 Spartan 存储引擎代码,您可能会在调试期间注意到一些索引方法可能没有被调用。这是因为索引方法在优化器中有多种用途。调用的顺序在很大程度上取决于优化器做出的选择。如果你很好奇(像我一样),想看看每个方法是如何工作的,你需要创建一个更大的数据集,并执行更复杂的查询。您还可以查看源代码和参考手册,以了解 handler 类中支持的每个方法的更多详细信息。

测试斯巴达发动机的第五阶段

当您再次运行测试时,您应该看到所有语句都成功完成。验证 Stage 4 引擎中的一切都工作正常,然后继续测试索引操作。

索引测试将要求您创建一个表,并在其中包含数据。您可以像以前一样使用普通的INSERT语句添加数据。现在您需要测试索引。输入一个在索引列(col_a)上有一个WHERE子句的命令,例如:

SELECT * FROM t1 WHERE col_a = 2;

当您运行该命令时,应该会看到返回的行。那不是很有趣,是吗?你已经做了所有的工作,它只是返回行。了解索引是否有效的最好方法是拥有包含各种索引值的大型数据表。这需要一段时间,我鼓励你这样做。

还有一个办法。您可以启动服务器,在源代码中附加断点(使用调试器),并发出基于索引的查询。这听起来像是大量的工作,您可能没有时间运行,但有几个例子。这很好,因为您可以将该功能添加到测试文件中。您可以将键列添加到CREATE中,并添加更多带有WHERE子句的SELECT语句来执行点和范围查询。清单 10-53 显示了更新后的Ch10s5.test文件。

清单 10-53。 更新了斯巴达存储引擎测试文件(Ch10s5.test)

#
# Simple test for the Spartan storage engine
#
--disable_warnings
drop table if exists t1;
--enable_warnings

CREATE TABLE t1 (
  col_a int KEY,
  col_b varchar(20),
  col_c int
) ENGINE=SPARTAN;

INSERT INTO t1 VALUES (1, "first test", 24);
INSERT INTO t1 VALUES (2, "second test", 43);
INSERT INTO t1 VALUES (9, "fourth test", -2);
INSERT INTO t1 VALUES (3, 'eighth test', -22);
INSERT INTO t1 VALUES (4, "tenth test", 11);
INSERT INTO t1 VALUES (8, "seventh test", 20);
INSERT INTO t1 VALUES (5, "third test", 100);
SELECT * FROM t1;
UPDATE t1 SET col_b = "Updated!" WHERE col_a = 1;
SELECT * from t1;
UPDATE t1 SET col_b = "Updated!" WHERE col_a = 3;
SELECT * from t1;
UPDATE t1 SET col_b = "Updated!" WHERE col_a = 5;
SELECT * from t1;
DELETE FROM t1 WHERE col_a = 1;
SELECT * FROM t1;
DELETE FROM t1 WHERE col_a = 3;
SELECT * FROM t1;
DELETE FROM t1 WHERE col_a = 5;
SELECT * FROM t1;
SELECT * FROM t1 WHERE col_a = 4;
SELECT * FROM t1 WHERE col_a >= 2 AND col_a <= 5;
SELECT * FROM t1 WHERE col_a = 22;
DELETE FROM t1 WHERE col_a = 5;
SELECT * FROM t1;
SELECT * FROM t1 WHERE col_a = 5;
UPDATE t1 SET col_a = 99 WHERE col_a = 8;
SELECT * FROM t1 WHERE col_a = 8;
SELECT * FROM t1 WHERE col_a = 99;
RENAME TABLE t1 TO t2;
SELECT * FROM t2;
DROP TABLE t2;

请注意,我已经更改了一些INSERT语句,以使索引方法能够工作。运行该测试,看看它会做什么。清单 10-54 显示了这个测试的预期结果的一个例子。当您在测试套件下运行测试时,它应该没有错误地完成。

清单 10-54。 第五阶段测试的样本结果

mysql> INSTALL PLUGIN spartan SONAME 'ha_spartan.so';
Query OK, 0 rows affected (0.01 sec)

mysql> use test;
Database changed
mysql> CREATE TABLE t1 (col_a int, col_b varchar(20), col_c int) ENGINE=SPARTAN;
Query OK, 0 rows affected (0.04 sec)

mysql> INSERT INTO t1 VALUES (1, "first test", 24);
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO t1 VALUES (2, "second test", 43);
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO t1 VALUES (9, "fourth test", -2);
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO t1 VALUES (3, 'eighth test', -22);
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO t1 VALUES (4, "tenth test", 11);
Query OK, 1 row affected (0.01 sec)

mysql> INSERT INTO t1 VALUES (8, "seventh test", 20);
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO t1 VALUES (5, "third test", 100);
Query OK, 1 row affected (0.01 sec)

mysql> SELECT * FROM t1;
+−−-----+−−------------+−−-----+
| col_a | col_b        | col_c |
+−−-----+−−------------+−−-----+
|     1 | first test   |    24 |
|     2 | second test  |    43 |
|     9 | fourth test  |    -2 |
|     3 | eighth test  |   -22 |
|     4 | tenth test   |    11 |
|     8 | seventh test |    20 |
|     5 | third test   |   100 |
+−−-----+−−------------+−−-----+
7 rows in set (0.00 sec)

mysql> UPDATE t1 SET col_b = "Updated!" WHERE col_a = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT * from t1;
+−−-----+−−------------+−−-----+
| col_a | col_b        | col_c |
+−−-----+−−------------+−−-----+
|     1 | Updated!     |    24 |
|     2 | second test  |    43 |
|     9 | fourth test  |    -2 |
|     3 | eighth test  |   -22 |
|     4 | tenth test   |    11 |
|     8 | seventh test |    20 |
|     5 | third test   |   100 |
+−−-----+−−------------+−−-----+
7 rows in set (0.00 sec)

mysql> UPDATE t1 SET col_b = "Updated!" WHERE col_a = 3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT * from t1;
+−−-----+−−------------+−−-----+
| col_a | col_b        | col_c |
+−−-----+−−------------+−−-----+
|     1 | Updated!     |    24 |
|     2 | second test  |    43 |
|     9 | fourth test  |    -2 |
|     3 | Updated!     |   -22 |
|     4 | tenth test   |    11 |
|     8 | seventh test |    20 |
|     5 | third test   |   100 |
+−−-----+−−------------+−−-----+
7 rows in set (0.01 sec)

mysql> UPDATE t1 SET col_b = "Updated!" WHERE col_a = 5;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT * from t1;
+−−-----+−−------------+−−-----+
| col_a | col_b        | col_c |
+−−-----+−−------------+−−-----+
|     1 | Updated!     |    24 |
|     2 | second test  |    43 |
|     9 | fourth test  |    -2 |
|     3 | Updated!     |   -22 |
|     4 | tenth test   |    11 |
|     8 | seventh test |    20 |
|     5 | Updated!     |   100 |
+−−-----+−−------------+−−-----+
7 rows in set (0.00 sec)

mysql> DELETE FROM t1 WHERE col_a = 1;
Query OK, 1 row affected (0.01 sec)

mysql> SELECT * FROM t1;
+−−-----+−−------------+−−-----+
| col_a | col_b        | col_c |
+−−-----+−−------------+−−-----+
|     2 | second test  |    43 |
|     9 | fourth test  |    -2 |
|     3 | Updated!     |   -22 |
|     4 | tenth test   |    11 |
|     8 | seventh test |    20 |
|     5 | Updated!     |   100 |
+−−-----+−−------------+−−-----+
6 rows in set (0.00 sec)

mysql> DELETE FROM t1 WHERE col_a = 3;
Query OK, 1 row affected (0.00 sec)

mysql> SELECT * FROM t1;
+−−-----+−−------------+−−-----+
| col_a | col_b        | col_c |
+−−-----+−−------------+−−-----+
|     2 | second test  |    43 |
|     9 | fourth test  |    -2 |
|     4 | tenth test   |    11 |
|     8 | seventh test |    20 |
|     5 | Updated!     |   100 |
+−−-----+−−------------+−−-----+
5 rows in set (0.01 sec)

mysql> DELETE FROM t1 WHERE col_a = 5;
Query OK, 1 row affected (0.00 sec)

mysql> SELECT * FROM t1;
+−−-----+−−------------+−−-----+
| col_a | col_b        | col_c |
+−−-----+−−------------+−−-----+
|     2 | second test  |    43 |
|     9 | fourth test  |    -2 |
|     4 | tenth test   |    11 |
|     8 | seventh test |    20 |
+−−-----+−−------------+−−-----+
4 rows in set (0.00 sec)

mysql> SELECT * FROM t1 WHERE col_a = 4;
+−−-----+−−----------+−−-----+
| col_a | col_b      | col_c |
+−−-----+−−----------+−−-----+
|     4 | tenth test |    11 |
+−−-----+−−----------+−−-----+
1 row in set (0.01 sec)

mysql> SELECT * FROM t1 WHERE col_a >= 2 AND col_a <= 5;
+−−-----+−−-----------+−−-----+
| col_a | col_b       | col_c |
+−−-----+−−-----------+−−-----+
|     2 | second test |    43 |
|     4 | tenth test  |    11 |
+−−-----+−−-----------+−−-----+
2 rows in set (0.00 sec)

mysql> SELECT * FROM t1 WHERE col_a = 22;
Empty set (0.01 sec)

mysql> DELETE FROM t1 WHERE col_a = 5;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM t1;
+−−-----+−−------------+−−-----+
| col_a | col_b        | col_c |
+−−-----+−−------------+−−-----+
|     2 | second test  |    43 |
|     9 | fourth test  |    -2 |
|     4 | tenth test   |    11 |
|     8 | seventh test |    20 |
+−−-----+−−------------+−−-----+
4 rows in set (0.00 sec)

mysql> SELECT * FROM t1 WHERE col_a = 5;
Empty set (0.00 sec)

mysql> UPDATE t1 SET col_a = 99 WHERE col_a = 8;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT * FROM t1 WHERE col_a = 8;
Empty set (0.01 sec)

mysql> SELECT * FROM t1 WHERE col_a = 99;
+−−-----+−−------------+−−-----+
| col_a | col_b        | col_c |
+−−-----+−−------------+−−-----+
|    99 | seventh test |    20 |
+−−-----+−−------------+−−-----+
1 row in set (0.00 sec)

mysql> RENAME TABLE t1 TO t2;
Query OK, 0 rows affected (0.01 sec)

mysql> SELECT * FROM t2;
+−−-----+−−------------+−−-----+
| col_a | col_b        | col_c |
+−−-----+−−------------+−−-----+
|     2 | second test  |    43 |
|     9 | fourth test  |    -2 |
|     4 | tenth test   |    11 |
|    99 | seventh test |    20 |
+−−-----+−−------------+−−-----+
4 rows in set (0.01 sec)

mysql> DROP TABLE t2;
Query OK, 0 rows affected (0.00 sec)

mysql>

这是第五阶段的引擎。它现在是一个基本的带索引的读/写/更新/删除存储引擎,这是 MySQL 中大多数存储引擎实现的阶段。事实上,对于除事务环境之外的所有环境,这应该足以满足您的存储需求。在下一阶段,我将讨论添加事务支持这一更加复杂的主题。

第六阶段:添加事务支持

目前,MySQL 中唯一支持事务的存储引擎是 InnoDB 。 7 事务提供了一种机制,允许一组操作作为单个原子操作来执行。例如,如果为一个银行机构建立一个数据库,从一个账户转移资金到另一个账户(从一个账户转移资金到另一个账户)的宏操作最好完全执行,不中断。事务允许将这些操作封装在一个原子操作中,如果在所有操作完成之前发生错误,该原子操作将取消任何更改,从而避免数据从一个表中删除,并且永远不会进入下一个表。清单 10-55 中显示了一组包含在事务命令中的 SQL 语句形式的示例操作。

清单 10-55。 示例事务 SQL 命令

START TRANSACTION;
UPDATE SavingsAccount SET Balance = Balance—100
WHERE AccountNum = 123;
UPDATE CheckingAccount SET Balance = Balance + 100
WHERE AccountNum = 345;
COMMIT;

实际上,如果需要更快的访问速度,大多数数据库专业人员会指定 MyISAM 表类型,如果需要事务支持,则指定 InnoDB。幸运的是,Oracle 提供了支持事务的存储引擎插件。

start_stmt()external_lock()方法支持在存储引擎中执行事务的功能。当一个事务开始时,调用start_stmt()方法。external_lock()方法用于通知表的特定锁,并在发出显式锁时调用。您的存储引擎必须通过创建一个保存点并使用trans_register_ha()方法向服务器注册事务,以start_stmt()方法实现新事务。该方法将当前线程、是否要跨所有线程设置事务以及 handlerton 的地址作为参数。调用此方法会导致事务启动。清单 10-56 中显示了start_stmt()方法的一个示例实现。

清单 10-56。 示例 start_stmt()方法实现

int my_handler::start_stmt(THD *thd, thr_lock_type lock_type)
{
  DBUG_ENTER("my_handler::index_last");
  int error= 0;
  /*
    Save the transaction data
  */
  my_txn *txn= (my_txn *) thd->ha_data[my_handler_hton.slot];
  /*
    If this is a new transaction, create it and save it to the
    handler's slot in the ha_data array.
  */
  if (txn == NULL)
    thd->ha_data[my_handler_hton.slot]= txn= new my_txn;
  /*
    Start the transaction and create a savepoint then register
    the transaction.
  */
  if (txn->stmt == NULL && !(error= txn->tx_begin()))
  {
    txn->stmt= txn->new_savepoint();
    trans_register_ha(thd, FALSE, &my_handler_hton);
  }
  DBUG_RETURN(error);
}

external_lock()开始一个事务有点复杂。MySQL 在事务开始时为每个正在使用的表调用external_lock()方法。因此,您需要做更多的工作来检测事务并相应地处理它。这可以从对trx->active_trans旗的检查中看出。当对第一个表调用external_lock()方法时,也意味着开始事务操作。列出 10-57 展示了external_lock()方法的一个示例实现(为了简洁,省略了一些部分)。完整代码见ha_innodb.cc文件。

列举 10-57。 示例 external_lock()方法实现(来自 InnoDB)

int ha_innobase::external_lock(THD*  thd, int Lock_type)
{
  row_prebuilt_t* prebuilt = (row_prebuilt_t*) innobase_prebuilt;
  trx_t*    trx;

  DBUG_ENTER("ha_innobase::external_lock");
  DBUG_PRINT("enter",("lock_type: %d", lock_type));

  update_thd(thd);

  trx = prebuilt->trx;

  prebuilt->sql_stat_start = TRUE;
  prebuilt->hint_need_to_fetch_extra_cols = 0;

  prebuilt->read_just_key = 0;
  prebuilt->keep_other_fields_on_keyread = FALSE;

  if (lock_type == F_WRLCK) {

    /* If this is a SELECT, then it is in UPDATE TABLE ...
    or SELECT ... FOR UPDATE */
    prebuilt->select_lock_type = LOCK_X;
    prebuilt->stored_select_lock_type = LOCK_X;
  }

  if (lock_type != F_UNLCK)
       {
    /* MySQL is setting a new table lock */

    trx->detailed_error[0] = '\0';

    /* Set the MySQL flag to mark that there is an active
    transaction */
    if (trx->active_trans == 0) {

      innobase_register_trx_and_stmt(thd);
      trx->active_trans = 1;
    } else if (trx->n_mysql_tables_in_use == 0) {
      innobase_register_stmt(thd);
    }

    trx->n_mysql_tables_in_use++;
    prebuilt->mysql_has_locked = TRUE;

...
    DBUG_RETURN(0);
  }

  /* MySQL is releasing a table lock */

  trx->n_mysql_tables_in_use--;
  prebuilt->mysql_has_locked = FALSE;

  /* If the MySQL lock count drops to zero we know that the current SQL
  statement has ended */

  if (trx->n_mysql_tables_in_use == 0) {

...
  DBUG_RETURN(0);
}

现在您已经看到了如何启动事务,让我们看看它们是如何停止的(也称为提交或回滚)。提交事务只是意味着将挂起的更改写入磁盘,存储适当的键,并清理事务。Oracle 在 handlerton ( int (*commit)(THD *thd, bool all))中提供了一个方法,可以使用这里显示的函数描述来实现该方法。参数是当前线程和您是否想要提交整个命令集。

int (*commit)(THD *thd, bool all);

回滚事务更加复杂。在这种情况下,您必须撤销自事务最后一次启动以来所做的一切。Oracle 使用 handlerton ( int  (*rollback)(THD *thd, bool all))中的回调来支持回滚,可以使用这里显示的函数描述来实现回调。参数是当前线程和是否应该回滚整个事务。

int (*rollback)(THD *thd, bool all);

为了实现事务,存储引擎必须提供某种缓冲机制来保存未保存的对数据库的更改。一些存储引擎使用类似堆的结构;其他的使用队列和类似的内存结构。如果您打算在存储引擎中实现事务,您将需要创建一个内部缓存(有时称为版本控制)机制。当发出 commit 时,数据必须从缓冲区中取出并写入磁盘。发生回滚时,必须取消操作并撤销其更改。

保存点是另一种事务机制,可用于在事务期间管理数据。保存点是内存中允许您保存信息的区域。您可以使用它们在事务过程中保存信息。例如,您可能希望保存有关内部缓冲区的信息,您实现该缓冲区是为了存储“脏的”或“未提交的”更改。保存点概念就是为了这种用途而创建的。

Oracle 提供了几种可以在 handlerton 中定义的保存点操作。这些出现在清单 10-1 中的 handlerton 结构的第 13 到 15 行。保存点方法的方法描述如下:

uint savepoint_offset;
int (*savepoint_set)(THD *thd, void *sv);
int (*savepoint_rollback)(THD *thd, void *sv);
int (*savepoint_release)(THD *thd, void *sv);

savepoint_offset值是您想要保存的存储区域的大小。savepoint_set()方法允许您为参数sv设置一个值,并将其保存为保存点。当回滚操作被触发时,调用savepoint_rollback()方法。在这种情况下,服务器将保存在sv中的信息返回给方法。类似地,当服务器响应释放保存点事件并通过设置为保存点的sv返回数据时,会调用savepoint_release() 。有关保存点的更多信息,请参见 MySQL 源代码和在线参考手册。

image 提示关于事务设施如何工作的优秀示例,请参见ha_innodb.cc源文件。您也可以在在线参考手册中找到相关信息。

简单地使用 MySQL 机制添加事务支持并不是故事的结尾。使用索引 8 的存储引擎必须提供允许事务的机制。这些操作必须能够标记已经被事务中的操作改变的节点,保存已经改变的数据的原始值,直到事务完成。此时,所有的更改都被提交到物理存储中(对于索引和数据)。这需要对Spartan_index类进行修改。

显然,在存储引擎插件中实现事务需要很多仔细的思考和计划。我强烈建议,如果您打算在您的存储引擎中实现事务支持,您应该学习 BDB 和 InnoDB 存储引擎以及在线参考手册。您甚至可能想要设置调试器并观察事务的执行。无论您以哪种方式实现事务,请放心,如果您让它工作,您将会有一些特别的东西。很少有优秀的存储引擎支持事务,也没有(到目前为止)超过原生 MySQL 存储引擎的能力。

摘要

在这一章中,我带你浏览了存储引擎插件的源代码,并向你展示了如何创建你自己的存储引擎。通过 Spartan 存储引擎,您学习了如何构建一个可以读写数据并支持并发访问和索引的存储引擎。虽然我解释了构建这个存储引擎的所有阶段,但是我将添加事务支持留给您去试验。

我也没有实现存储处理程序所有可能的功能。相反,我只是实现了一些基本功能。既然您已经看到了基本的操作并有机会进行实验,我建议您在设计自己的存储引擎时学习在线文档和源代码。

如果你觉得这一章是个挑战,没关系。创建数据库物理存储机制不是一项简单的任务。我希望您从本章中能够更好地理解构建一个存储引擎需要什么,并对那些实现索引和事务支持的 MySQL 存储引擎有一个正确的认识。这些任务都不是微不足道的努力。

最后,我看到了我提供的数据和索引类的几个可能改进的地方。虽然数据类对大多数应用来说似乎不错,但索引类还可以改进。如果您计划使用这些类作为您自己的存储引擎的起点,我建议让您的存储引擎像现在这样使用这些类,然后返回并更新或替换它们。

我建议更新索引类中的几个区域。也许我推荐的最重要的改变是将内部缓冲区改为更有效的树结构。有很多可以选择,比如无处不在的 B 树或者哈希机制。我还建议您更改该类处理范围查询的方式。最后,需要进行更改来处理事务支持。该类需要支持您用来处理提交和回滚的任何缓冲机制。

在下一章中,我将讨论数据库服务器设计和实现中的一些高级主题。这一章将为你使用 MySQL 服务器源代码作为研究数据库系统内部的实验平台做准备。

1 尽管使用了聚集索引和其他数据文件优化。

2 一些特殊的存储引擎可能根本不需要写数据。例如,黑洞存储引擎实际上不实现任何写功能。嘿,这是个黑洞!

3 将存储引擎正在读写的数据称为存储介质更为正确,因为没有任何东西规定数据必须驻留在传统的数据存储机制上。

4MyISAM 和 InnoDB 存储引擎包含附加源文件。这些是最老的存储引擎,也是最复杂的。

这一章的灵感来源于那些寻求开发自己的存储引擎的人所缺乏的内容。很少有参考资料在其示例中超出了创建阶段 1 引擎的范围。

6 嗯,也许是低级 I/O 源代码。自从我写了那个类之后,总是有可能我错过了一些东西或者服务器中的一些东西已经改变了。

7集群存储引擎(NDB)也支持事务。

8 根据记录,有可能存在不支持索引的第 6 级引擎。事务处理不需要索引。然而,唯一性应该是一个问题,性能会受到影响。