MySQL-专家级教程-全-

MySQL 专家级教程(全)

原文:Expert MySQL, 2nd Edition

协议:CC BY-NC-SA 4.0

零、简介

MySQL 被认为是世界上最流行的开源数据库,也是业内发展最快的数据库系统。本书介绍了高级数据库系统的一些主题,研究了 MySQL 的体系结构,并提供了一个专家工作簿,用于检查、集成和修改企业环境中使用的 MySQL 源代码。这本书深入探讨了如何修改 MySQL 系统,以满足系统集成商和教育者的独特需求。

这本书是给谁的

我写这本书时考虑到了各种各样的读者。无论您已经从事数据库系统工作多年,或者可能已经上过数据库理论入门课,或者刚刚读过一本关于 MySQL 的好书,您都会从这本书中学到很多。最棒的是,你甚至可以接触到源代码。如果你曾经想知道是什么让像 MySQL 这样的数据库系统运行,这就是你的书!

这本书的结构

介绍的材料分为三个部分,后面有一个附录。每一部分都旨在呈现一系列主题,从 MySQL 和开源革命的介绍材料到 MySQL 系统的扩展和定制。甚至还介绍了如何构建一个实验性的查询优化器和执行引擎,作为 MySQL 查询引擎的替代。

第一部分

这本书的第一部分介绍了开发和修改开源系统的概念。第一部分提供了开始探索本书其余部分中介绍的更高级的数据库概念所必需的工具和资源。章节包括:

  1. MySQL 和开源革命——这一章比本书的其余部分更少技术含量,包含更多叙述。它指导您了解开源系统集成商的好处和责任。它强调了 MySQL 的快速增长及其在开源和数据库系统市场的重要性。此外,它为开源革命提供了一个清晰的视角。
  2. 数据库系统的剖析——本章涵盖了什么是数据库系统以及如何构建数据库系统的基础知识。MySQL 系统的剖析被用来说明现代关系数据库系统的关键组件。
  3. MySQL 源代码之旅——本章介绍了 MySQL 源代码的完整介绍,以及如何获得和构建该系统。向您介绍了源代码的机制,以及代码维护的编码指南和最佳实践。
  4. 测试驱动的 MySQL 开发——本章介绍了为 MySQL 系统生成高质量扩展的一个关键要素。软件测试与如何测试大型系统的通用实践一起被提出。具体的例子被用来说明测试 MySQL 系统的可接受的实践。

第二部分

本书的这一部分提供了工具,使用动手的方法来研究 MySQL 系统。它向您介绍如何修改 MySQL 代码,以及如何将该系统用作嵌入式数据库系统。示例和项目说明了如何调试源代码,如何修改 SQL 命令以扩展语言,以及如何构建自定义存储引擎。

  • 5.调试——本章为您提供调试技巧和技术,帮助您简化开发,减少失败。介绍了几种调试技术,以及每种技术的优缺点。
  • 6.嵌入式 MySQL——本章将向您介绍如何在企业应用中嵌入 MySQL 系统的教程。示例项目帮助您将所展示的技能应用到您自己的集成需求中。
  • 7.向 MySQL 添加函数和命令——本章介绍了对 MySQL 代码最流行的修改。向您展示了如何修改 SQL 命令以及如何构建定制的 SQL 命令。它展示了如何修改 SQL 命令以添加新参数、函数和新命令的示例。
  • 8.扩展 MySQL 高可用性—本章概述了 MySQL 的高可用性特性,包括复制源代码和如何扩展该特性以满足高可用性需求的示例。
  • 9.开发 MySQL 插件——本章介绍了 MySQL 中的可插拔架构。您将发现如何构建插件,并看到一个身份验证插件的详细示例。
  • 10.构建您自己的存储引擎—本章演示了可插拔存储引擎的特性。我们将使用示例和项目来探究该架构,这些示例和项目允许您构建一个示例存储引擎。

第三部分

本书的这一部分对 MySQL 系统进行了更深入的研究,并从内部人士的角度向您展示了该系统的工作原理。本节首先介绍更高级的数据库技术。理论和实践以一种严肃的方式呈现,使您能够应用所获得的知识来处理数据库系统的更复杂的主题。本节还提供了如何实现内部查询表示、替代查询优化器和替代查询执行机制的示例。详细讨论了示例和项目。第 12 章到 14 章提供了改变 MySQL 系统内部结构以使用替代机制执行所需的技能和技术。这些章节为你提供了如何构建和修改大型系统的独特见解。

  • 11.hapter 旨在通过研究 MySQL 架构来展示更高级的数据库技术。主题包括查询执行、多用户问题和编程考虑。
  • 12.内部查询表示——本章介绍 MySQL 内部查询表示。为您提供了一个替代查询表示的示例。还讨论了如何修改 MySQL 源代码来实现另一种查询表示。
  • 13.查询优化——本章介绍 MySQL 内部查询优化器。我们为您提供了一个备选查询优化器示例,它使用了上一章中的备选查询表示。它讨论了如何修改 MySQL 源代码来实现替代的查询优化器。
  • 14.查询执行——本章结合了前几章的技术,为您提供了如何修改 MySQL 系统以实现替代查询处理引擎技术的指导。

附录

本书的这一部分提供了一个关于 MySQL、数据库系统和开源软件的资源列表。

使用这本书来教授数据库系统的内部知识

许多优秀的数据库教科书提供了关系理论和实践的覆盖面。然而,很少有人提供适用于教室或实验室环境的材料。可供学生探索数据库系统内部工作的资源甚至更少。这本书为教师提供了一个通过动手实验来扩充数据库课程的机会。该文本可以在课堂环境中以三种方式使用:

  1. 该文本可用于增加本科生或研究生数据库入门课程的深度。第 1 部分第 2 部分可用于提供数据库系统中特殊主题的深入报道。建议的讲座主题包括在第 2 章第 3 章第 4 章第 6 章中提出的主题。除了更传统的数据库理论或系统文本之外,还可以使用这些。动手练习或课堂项目可从第 6 章第 7 章中抽取。
  2. 本科或研究生的高级数据库课程可以基于part 12;每章可以在 8 到 12 周内完成。讲座的其余部分可以讨论物理存储层的实施和存储引擎的概念。学期项目可以基于第 10 章,让学生构建自己的存储引擎。
  3. 为高年级本科生或研究生开设的关于数据库系统内部的专题课程可以基于整篇课文,授课基于前 11 章。学期专题可以从课文的第三部分衍生而来,学生实现数据库实验平台的其余功能。这些特性包括语言理论、查询优化器和查询执行算法的应用。

约定

在整本书中,我保持了一致的风格来呈现 SQL 和结果。当文本中出现一段代码、一个 SQL 保留字或一段 SQL 时,它会以固定宽度的 Courier 字体显示,例如:

select * from dual

在讨论 SQL 命令的语法和选项时,我使用了对话式的风格,这样您可以快速理解命令或技术。这意味着我没有复制更适合参考手册的大型语法图。

下载代码

本书中给出的例子的代码可以在 press 网站www.apress.com上找到。在该书的信息页面上的源代码/下载选项卡下可以找到一个链接。该选项卡位于页面相关标题部分的下方。

联系作者

如果你有任何问题或意见——甚至是你认为我应该知道的错误——你可以打电话到drcharlesbell@gmail.com联系我。

一、MySQL 和开源革命

开源系统正在迅速改变软件领域。世界各地的信息技术专业人员都注意到了开源软件供应商提供的高质量,在许多情况下是世界级的开发和支持。公司开始注意了,因为他们第一次有了商业专有软件供应商的替代品。小企业正在关注,因为开源软件可以显著降低他们信息系统的成本。个人之所以关注,是因为他们有了比以往更多的选择。使互联网成为今天这个样子的大部分基础都是基于开源软件系统,比如 Linux、Apache HTTP server、BIND、Sendmail、OpenSSL、MySQL 等等。

企业使用开源软件最常见的原因是成本。开源软件,就其本质而言,降低了总拥有成本(TCO ),并提供了一种可行的商业模式,企业可以在此基础上建立或改善其市场。对于开源数据库系统来说尤其如此,因为商业专有系统的成本很容易达到数万或数十万美元。

对于刚刚起步的小企业来说,这笔资金支出可能会影响其成长。例如,如果一家初创公司不得不花费大量储备,它可能无法将其产品推向市场,因此可能无法在竞争激烈的市场中站稳脚跟。开源为创业公司提供了推迟购买软件的机会,直到他们能够承担投资。然而,这并不意味着他们在用劣质组件构建基础设施。

开源软件曾经被许多人认为是仅限于爱好者或黑客,他们致力于颠覆大型商业软件公司的市场。虽然一些开发者可能觉得他们在扮演微软巨人的角色,但是开源社区完全不是这样。它并不自称是商业专有软件的替代品,而是提出开源哲学作为替代。正如您将在本章中看到的,开源不仅是商业软件的一个可行的替代方案,而且它还推动了一场软件开发、发展和营销方式的世界性革命。

image 在本书中,术语“黑客”指的是理查德·斯托尔曼的定义:“喜欢编程并喜欢耍小聪明的人,” 1 而不是人们通常认为的一个一心盗窃信用卡和破坏计算机系统的邪恶恶棍。

以下部分是为那些不熟悉开源软件或 MySQL 哲学的人提供的。如果你已经熟悉开源软件的理念,你可以跳到“用 MySQL 开发”一节

什么是开源软件?

开源软件是从对公司财产心态的有意识抵制中成长起来的。20 世纪 70 年代,在麻省理工学院(MIT)人工智能实验室工作时,理查德·斯托尔曼开始了一场代码共享运动。在让所有程序员都能使用常用代码的愿望驱使下,斯托曼看到了开发者合作社区的需要。这种哲学对 Stallman 和他的小团体很有效——直到行业集体决定软件是财产,不应该与潜在的竞争者分享。这导致许多麻省理工学院的研究人员被吸引到这些公司工作。最终,合作社区逐渐消失。

幸运的是,斯托曼抵制了这种趋势,离开了麻省理工学院,开始了 GNU (GNU 不是 Unix)项目和自由软件基金会(FSF)。GNU 项目的目标是生产一个免费的类似 Unix 的操作系统。这个系统将是免费的(包括访问源代码),任何人都可以使用。“免费”的概念是不禁止任何人使用和修改系统。

斯托曼的目标是重建在麻省理工学院运作良好的开发者合作社区。然而,他有先见之明,意识到这个系统需要版权许可来保证某些自由。(有些人把斯托曼对版权的理解称为“左版权”,因为它保障了自由,而不是限制了自由。)斯托曼创建了 GNU 公共许可证(GPL)。GPL 是一个巧妙的法律许可作品,它允许代码不受限制地被复制和修改,规定衍生作品(修改后的副本)必须在与原始版本相同的许可下发布,没有任何附加限制。从本质上讲,这是通过完全去除专有元素来利用版权法对抗版权。

不幸的是,斯托曼的 GNU 项目从未完全实现,但它的几个部分已经成为许多开源系统的基本元素。其中最成功的包括用于 C 编程语言的 GNU 编译器(GCC)和 GNU 文本编辑器(Emacs)。尽管 GNU 操作系统未能完成,但斯托尔曼和他的追随者们的开拓性努力使得 Linus Torvalds 在 1991 年用他的 Linux 操作系统填补了空白,当时它还处于婴儿期。Linux 已经成为斯托曼设想的免费的类 Unix 操作系统(参见“为什么 Linux 如此受欢迎?”).今天,Linux 是世界上最流行和最成功的开源操作系统。

为什么 LINUX 这么受欢迎?

Linux 是建立在开源模式上的类 Unix 操作系统。因此,任何人都可以免费使用、分发和修改它。Linux 使用了一种保守的内核设计,这种设计已经被证明是易于发展和改进的。自从 1991 年发布以来,Linux 已经在全世界范围内赢得了寻求提高其性能和可靠性的开发者的支持。有些人甚至声称 Linux 是所有操作系统中发展得最好的。自发布以来,Linux 已经获得了世界服务器和工作站安装的巨大市场份额。Linux 经常被认为是迄今为止最成功的开源努力。

我们可以从社区中较小的团体带来的许多变体中看到 Linux 的成功。这些变体中的许多,比如 Ubuntu,都是由控制产品发展的公司(Canonical)所拥有的。虽然仍然是 Linux,但 Ubuntu 是一个很好的例子,说明所有权如何通过核心产品的增值改造来推动创新和差异化。

自由软件运动有一个问题。“免费”的本意是保证使用、修改和分发的自由,而不是免费或免费到好人家(通常解释为“免费”是言论自由,而不是免费啤酒)。为了消除这种误解,开放源码倡议(OSI)成立了,后来采用并推广了“开放源码”一词来描述 GPL 所保证的自由;在www.opensource.org访问网站。

OSI 的努力改变了自由软件运动。软件开发者有机会区分真正没有成本的自由软件和作为合作社区一部分的开放软件。随着互联网的爆炸,合作社区已经成为一个全球性的开发者社区,确保了斯托尔曼愿景的延续。

因此,开放源码软件是一种被许可的软件,它保证开发者在加入一个合作团体时使用、复制、修改和分发他们的软件的权利,该团体的自然目标是发展和培养更高质量的软件。开源并不意味着零成本。这确实意味着任何人都可以参与软件的开发,并且可以免费使用软件。另一方面,许多开源系统是由销售软件支持服务的组织托管和分发的。这允许使用该软件的组织通过消除启动成本和在许多情况下节省大量维护成本来降低他们的信息技术成本。

今天所有的开源系统都是从 Stallman 和其他人努力创造的软件乌托邦的工作基础中汲取血统,Stallman 认为组织应该通过出售服务而不是私有产权来获得收入。有几个 Stallman 的设想变成现实的例子。GNU/Linux(以下简称为 Linux)运动已经产生了许多成功的(并且盈利的)公司,比如 Red Hat 和 Slackware,它们出售定制的发行版和对 Linux 的支持。另一个例子是 MySQL,它已经成为最成功的开源数据库系统。

尽管软件乌托邦的概念在今天可能还没有成为现实,但下载一整套系统和工具来驱动个人或商业计算机而不在软件本身上花费任何钱是可能的。任何人都可以下载和使用免费版本的软件,从操作系统和服务器系统(如数据库和 web 服务器)到生产力软件。

为什么要使用开源软件?

迟早有人会问为什么使用开源软件是个好主意。为了成功抵御来自商业专有软件支持者的挑战,你应该有一个可靠的答案。采用开源软件的最重要原因是:

  • 开源软件的使用成本很低甚至为零。这对于非营利组织、大学和社区组织来说尤其重要,因为它们的预算在不断缩减,每年都必须以更少的资源做更多的事情。
  • 您可以修改来满足您的特定需求。
  • 可用的许可机制比商业许可更加灵活。
  • 开源软件比商业专有软件更健壮(经过测试)。
  • 开源软件比商业专有软件更加可靠和安全。

虽然你可能不会被质疑或被要求证明采用开源软件的任何理由,但你可能会受到矛盾的挑战——也就是说,商业专有软件的支持者(开源的反对者)会试图通过陈述为什么不应该使用开源软件进行开发来质疑这些说法。让我们从商业专有软件的角度来研究一些不使用开源软件的更流行的理由,并用开源观点来反驳它们。

神话 1:商业专有软件培养更大的创造力

理由是:大多数企业级商业专有软件都提供了应用编程接口(API ),允许开发者扩展其功能,从而使软件更加灵活,并确保开发者有更大的创造力。

这其中有些是真的。API 确实允许开发者扩展软件,但他们通常以严格禁止开发者向基础软件添加功能的方式来扩展软件。这些 API 经常迫使开发者进入沙箱,进一步限制了她的创造力。

例如,微软。Net 语言 C#被公认为是一门非常好的语言。然而,API 不容易修改。事实上,只有在安装宿主产品 Visual Studio 时,才会收到二进制形式的库。您可以用类派生来扩充 API,但是严格地说,您不能编辑 API 本身的源代码。

image 注意沙箱经常被创建来限制开发者影响核心系统的能力,很大程度上是为了安全。API 越开放,邪恶的开发者就越有可能创建恶意代码来破坏系统或其数据。

开源软件也可能支持并提供 API,但它为开发者提供了查看核心系统实际源代码的能力。他们不仅可以看到源代码,还可以自由地(并被鼓励)修改它!(例如,当一个关键特性不可用时,或者您需要系统读取或写入特定格式时,您可能想要修改核心系统。)因此,开源软件比商业专有软件更能培养创造力。

误区二:商业专有软件比开源软件 更安全

理由是:在当今互联网连接的社会中,组织要求他们的信息系统比以往任何时候都更安全。商业专有软件本质上更安全,因为销售软件的公司在确保他们的产品能够抵御当今数字掠夺者的攻击方面有更大的利益。

尽管这一声明的目标很可能作为任何商业软件供应商的口头禅出现在会议室的墙上,但这一目标的实现,或者在某些情况下的营销声明,通常是误导性的或无法实现的。

研究表明,开源软件开发的本质有助于使软件更加安全,因为根据定义,开源软件是由一个团体和一个社区开发的,他们有兴趣为产品寻找最好的东西。事实上,对源代码的严格审查和开放确保了没有任何东西可以隐藏起来,无论是缺陷还是遗漏。因为源代码对所有人都是可用的,所以强化他的代码符合每个开源开发者的最大利益——不管是恶意的还是良性的。

误解 3:商业专有软件被测试的次数比开源软件多

说法是:软件商卖软件。他们出售的产品必须保持高质量标准,否则顾客不会购买。开源软件没有任何这样的压力,因此没有像商业专利软件那样严格测试。

这个论点很有说服力。事实上,它打动了所有信息技术收购代理人的心。他们相信,你花钱买的东西比免费获得的软件更可靠,没有缺陷。不幸的是,这些人忽略了开源软件的一个重要概念:它是由全球开发者社区开发的,其中许多人认为自己是缺陷侦探(测试员),并以发现和报告缺陷为荣。在某些情况下,开源软件公司为发现可重复错误的开发者提供奖励。

诚然,软件供应商雇佣软件测试人员(毫无疑问,他们是他们领域中的佼佼者),但更多的时候,商业软件项目被推向一个特定的截止日期,并从公司目标的角度关注产品的好处——通常由营销机会驱动。设置这些截止日期是为了确保战略发布日期或竞争优势。很多时候,这些截止日期迫使软件供应商在他们软件开发过程的某些部分上妥协——通常是后面的部分:测试。可以想象,减少测试人员对软件的访问(测试时间)意味着他们会发现更少的缺陷。

开源软件公司通过获得全球开发者社区的帮助和支持,确保他们的软件被更多的人更频繁地测试,这些人只考虑产品本身的好处,而不是通常被可能影响他们审查软件的能力的目标所驱动。事实上,一些开源社区成员在评估一个新特性或版本时有时会毫不留情。相信我,如果没有达到他们的期望,他们会让你知道的。

误解 4:商业专有系统比开源系统 拥有更复杂的功能和更完整的特性集

论点是:商业专有数据库系统是复杂的服务器系统。开源系统既不够大,也不够复杂,无法处理关键任务企业 数据。

尽管一些开源系统很好地模仿了它们所模仿的商业系统,但对于像 MySQL 这样的数据库系统来说就不一样了。MySQL 的早期版本不具备商业专有数据库系统中的所有功能,但从 5.0 版本开始,尤其是最新版本,MySQL 包含了一些主要功能,被认为是世界上最受欢迎的开源数据库系统。

此外,MySQL 已被证明能够提供大型企业对关键任务数据所需的可靠性、性能和可伸缩性,许多知名组织都在使用它。MySQL 是一个开源系统,它提供了最好的商业专有数据库系统的所有特性和功能。

误解 5:商业专有软件供应商反应更快,因为他们有专门的员工

论点是这样的:当一个软件系统被购买时,该软件附带了保证,即生产它的公司将提供帮助或帮助解决问题。因为没有人“拥有”开源系统,所以获得帮助要困难得多。

大多数开源软件都是由全球开发者社区构建的。然而,日益增长的趋势是将商业模式建立在开源哲学的基础上,并围绕它建立一家公司,为该公司监管的软件销售支持和服务。大多数主要的开源产品都以这种方式得到支持。例如,甲骨文公司拥有其 MySQL 产品的源代码。(关于甲骨文 MySQL 开源许可的完整描述,请参见www . MySQL . com/company/legal/licensing/opensource-license . htm。)

开源软件的开发者对问题的反应比商业开发者要快得多。事实上,许多人对公开他们的产品感到非常自豪,并密切关注世界对他们的看法。另一方面,与商业软件开发者直接对话几乎是不可能的。例如,微软有一个全面的支持机制,可以满足几乎任何组织的需求。然而,如果你想与微软产品的开发者交谈,你必须通过适当的渠道。这需要与支持层级的每一个阶段进行沟通——即使这样,你也不能保证与开发者取得联系。

另一方面,开源开发者使用互联网作为他们主要的交流方式。由于他们已经在互联网上,他们更有可能在论坛或新闻组中看到你的问题。此外,像 Oracle 这样的开源公司积极地监控他们的社区,并且可以快速地响应他们的客户。

因此,购买商业专有软件并不能保证你的响应时间比开源软件更快。在许多情况下,开源软件开发者比商业软件开发者反应更快(更容易接触到)。

如果他们想要证据呢?

当你试图在你的组织中采用开源软件时,这些仅仅是可能导致你悲伤的一些争论。一些研究人员试图证明开源软件比商业软件更好。一个是 James W. Paulson,他对开放源代码和商业专有软件(他称之为“封闭的”)进行了一项实证研究,该研究检验了前面的论点,并证明了开放源代码软件开发可以证明相对于商业专有软件开发的可测量的改进。参见 2004 年 4 月出版的 IEEE 软件工程汇刊中 Paulson 的文章“开源和闭源软件产品的实证研究”。

开源真的是商业软件的威胁吗?

直到最近,开源软件还不被认为是对商业专有软件巨头的威胁,但甲骨文的一个竞争对手开始展示出经典的 应对竞争威胁的迹象。尽管微软最近做出了开放的努力 2 ,微软继续公开反对开源软件,谴责 MySQL 是世界一流的数据库服务器,同时消极地忽视威胁,甲骨文却采取了一种截然不同的策略。

自从通过收购 Sun Microsystems 获得 MySQL 以来,Oracle 继续投入大量资源来增强 MySQL。Oracle 已经并将继续投资于开发,以使 MySQL 成为世界上最好的 web 数据库系统。

竞争的压力不仅限于 MySQL 和专有数据库系统。至少有一个开源数据库系统 Apache Derby 将自己标榜为 MySQL 的替代者,并且最近将其帽子扔进了环中,作为 LAMP 栈中“M”的替代者(参见“什么是 LAMP 栈?”).Apache Derby 的支持者列举了 MySQL 的许可问题和功能限制。既没有阻止 MySQL 的安装基础,也没有这些“问题”限制 MySQL 的日益普及。

什么是灯栈?

LAMP 代表 Linux,Apache,MySQL,以及 PHP/Perl/Python 。LAMP stack 是一组开源服务器、服务和编程语言,允许快速开发和部署高质量的 web 应用。关键组件包括

  • Linux :类似 Unix 的操作系统。Linux 以其高度的可靠性和速度以及支持的硬件平台的多样性而闻名。
  • Apache :以高可靠性和易于配置著称的 web 应用服务器。Apache 运行在大多数 Unix 操作系统上。
  • MySQL :许多 web 应用开发者的首选数据库系统。MySQL 以其速度和较小的执行占用空间而闻名。
  • PHP/Perl/Python :这些是脚本语言,可以嵌入到 HTML 网页中,用于事件的编程执行。这些脚本语言代表 LAMP 堆栈的活动编程元素。它们用于连接系统资源和后端数据库系统,为用户提供活动内容。虽然大多数 LAMP 开发者更喜欢 PHP 而不是其他脚本语言,但是每种语言都可以用来成功地开发 web 应用。

使用灯堆栈进行开发有很多好处。最大的是成本。所有 LAMP 组件都可以作为免费的开源许可证获得。组织可以在几个小时内下载、安装和开发 web 应用,而软件的初始成本很少或没有。

提供开源数据库系统的好处的一个有趣的指标是最近一些专有数据库供应商提供的“免费”版本。微软一直是开源软件的公开反对者,现在它提供了一个名为 SQL Server Express 的免费版本的 SQL Server 数据库系统。尽管下载该软件是免费的,并且您被允许将该软件与您的应用一起分发,但您可能看不到源代码或以任何方式修改它。该版本的功能集有限,如果不购买额外的软件和服务,就无法扩展到完整的企业级数据库服务器。

显然,甲骨文公司以其 MySQL 服务器产品开辟的道路证明了对专有数据库市场的威胁——商业专有软件行业正在认真对待这一威胁。尽管微软继续试图削弱开源软件市场,它也开始看到免费软件的智慧。

法律问题和 GNU 宣言

商业专有软件许可证旨在限制您的自由和使用。大多数商业许可证都明确声明,作为软件的购买者,您并不拥有该软件,但可以在非常特定的条件下使用它。在几乎所有情况下,这意味着您不能以任何方式复制、分发或修改系统。这些许可证也清楚地表明,源代码是由许可方独家拥有的,您,即被许可方,不允许查看或重新设计它。

image 注意本节是对通用公共许可证的一般性讨论。软件提供商通常有自己的许可证形式,并可能以微妙但不同的方式解释其合法性。请始终联系软件提供商,以澄清您希望行使的许可证的任何部分。如果您希望在自己的产品或服务中修改或包含软件的任何部分,这一点尤其正确。

开源系统通常使用基于 GNU 的许可协议(GNU 代表 GNU,而不是 Unix)授权,称为通用公共许可证(GPL)。更多详情见http://www.gnu.org/licenses/。大多数 GPL 许可证允许自由使用原始源代码,但有一个限制,即所有修改都必须公开或作为合法所有权返还给创作者。此外,大多数开放源码系统使用 GPL 协议,该协议声明它旨在保证您复制、分发和修改软件的权利。请注意,GPL 并不限制您使用软件的权利;事实上,它特别授予你以你想要的方式使用软件的权利。GPL 也保证了你访问源代码的权利。所有这些权利都在 GNU 宣言和 GPL 协议(【www.gnu.org/licenses/gpl.html】??)中有详细说明。

最有趣的是,GPL 特别允许你对原始源代码的发布收取发布费(或媒体费),并为你提供使用整个系统或修改系统的权利,以创建衍生产品,这也受同一 GPL 的保护。唯一的问题是,您需要将修改后的源代码提供给任何想要它的人。

这些限制并不妨碍你通过努力工作获得收入。相反,只要你通过原始所有者发布你的源代码,你就可以向你的客户收取衍生作品的费用。有些人可能会争辩说,这意味着你永远无法获得真正的竞争优势,因为你的源代码对每个人都是可用的,但在实践中恰恰相反。Canonical、Red Hat 和 Oracle 等供应商已经从基于 GPL 的商业模型中获利。

GPL 的唯一限制可能会让你犹豫,那就是对保证的限制,以及要求在你的软件中放置一个横幅说明作品的来源(原始和许可)。

如果你考虑到大多数商业许可都包含类似的条款,那么对明示担保的限制就不足为奇了。GPL 的独特之处在于无责任损失的概念。GPL 明确免除了创作者和您,即修改者(或发布者)因安装或使用软件而导致的损失或损害。Stallman 不希望法律行业因开源软件的责任问题而获利。逻辑很简单。您免费获得了该软件,并且您没有获得任何关于其性能或保护其免受因使用而造成的损害的保证。在这种情况下,没有交换条件,因此没有任何形式的保证。

开源运动的反对者将引用这一点作为避免开源软件的理由,声称这是“使用风险自担”,因此引入了太多的风险。虽然这是事实,但是当您从开源供应商那里购买支持时,这一论点就被削弱或无效了。开源供应商的支持选项通常包括某些责任权利和进一步的保护。这也许是购买开源软件支持的最有说服力的理由。在这种情况下,有交换条件,而且在许多情况下有可靠的保证。

在你的软件中一个可见的地方放置一个横幅的需求并不是那么繁重。GPL 只需要清楚地声明软件的来源和出处,并标记软件受 GPL 保护。这将告知使用本软件的任何人他们使用、复制、分发和修改本软件的权利(自由)。

也许 GNU 宣言中最重要的声明是“GNU 将如何可用”下面的陈述。在这一节中,宣言声明尽管每个人都可以修改和重新发布 GNU,但是没有人可以限制它的进一步重新发布。这意味着没有人可以把一个基于 GNU 宣言的开源系统变成一个专有系统或者进行专有修改。

属性

如果不包括财产问题,关于开放源码软件许可的讨论将是不完整的。财产仅仅是被拥有的东西。虽然通常认为财产是有形的东西,但在软件的情况下,这个概念变得有问题。当我们说软件是财产时,到底是什么意思?属性的概念适用于源代码、二进制文件(可执行文件)、文档,还是所有这些?

当谈到开源软件时,财产的概念通常是一个棘手的话题。如果软件是由全球开发者社区开发的,谁是所有者?在大多数情况下,开源软件始于某人或某个组织开发的项目。当软件足够成熟,对其他人有用时,项目就变成了开源。无论这是在早期阶段,当软件不精炼时,还是在后期,当软件达到一定的可靠性水平时,都不重要。重要的是启动项目的人或组织被认为是所有者。以 MySQL 为例,Oracle 公司发起了这个项目,因此它拥有 MySQL 系统。

根据 MySQL 坚持的 GPL,Oracle 拥有所有的源代码和在 GPL 下所做的任何修改。GPL 给了你修改 MySQL 的权利,但是它没有给你将源代码作为你的财产的权利。

甲骨文真的拥有 MYSQL 吗?

MySQL 作为一个组织进化的详细历史超出了本书的范围。MySQL 品牌、产品及其开发组织完全归甲骨文公司所有。作为 2010 年 1 月执行的 Sun Microsystems 合并的一部分,甲骨文收购了 MySQL。

尽管在欧洲有一些反垄断的争议,但合并是成功的,甲骨文承诺继续发展和演变 MySQL。到目前为止,Oracle 已经实现了这些承诺,并继续将 MySQL 发展成为世界领先的开源数据库系统。Oracle 继续像过去一样定位 MySQL:web 数据库(LAMP 中的 M)。

自收购以来,Oracle 发布了几个 MySQL 版本,包括更好的性能、集成 InnoDB 作为默认数据库、Windows 平台改进以及对复制的大量改进和创新,从而实现了高可用性功能。甲骨文确实是 MySQL 的所有者,也是迄今为止最大的保管者。

伦理的一面

当你第一次开始使用开源软件时,道德困境比比皆是。例如,开源软件可以免费下载,但你必须将你的任何改进交给原所有者。你怎么能从你不得不放弃的东西中赚钱呢?

要理解这一点,您必须考虑 Stallman 在开发 GNU 许可证模型时的目标:在全世界的开发者中建立一个合作和团结的社区。他希望源代码公开,生成的软件免费供任何人使用。你工作挣钱(获得报酬)的权利不受限制。你可以出售你的衍生作品。你不能声称拥有源代码的所有权。你在伦理上(和法律上!)必将回馈全球开发者社区。

当你修改开源软件供自己使用时,另一个道德困境出现了。例如,您下载了最新版本的 MySQL,并添加了一个特性,允许您对 SQL 命令使用自己的缩写快捷方式,因为您已经厌倦了键入冗长的 SQL 语句(我相信有人已经这样做了)。在这种情况下,你正在以一种只有利于你自己的方式修改系统。那么你为什么要交出你的修改呢?虽然这种困境对我们大多数人来说可能不是问题,但如果你继续使用带有你个人修改的软件并最终创作出一个衍生作品,它可能会成为你的问题。基本上,你所做的任何生产性的和有意义的修改必须被认为是原创者的财产,不管它的使用或使用的限制。

如果您将修改源代码作为一项学术练习(然而,我将在本书后面告诉您如何做),您应该在完成练习或实验后放弃修改。一些开源软件为这些类型的使用做了准备。大多数人认为探索和试验源代码是对软件的“使用”,而不是修改,因此允许在学术研究中使用源代码。

让革命继续!

自由的理念驱使理查德·斯托尔曼开始寻求改革软件开发。虽然自由是开源运动的催化剂,但它已经成为一场革命,因为组织现在可以通过投资低成本软件系统来避免在竞争对手手中过时,同时保持收入以在市场中竞争。

采用开源软件作为产品线一部分的组织可能是所有组织中最具革命性的。大多数已经采用了基于 GPL 的商业模式,这种模式允许他们获得开源系统带来的所有经验和健壮性,同时仍然为他们自己的想法和附加内容创造收入。

软件行业对开源软件既嗤之以鼻,又赞不绝口。一些人鄙视开源,因为他们认为这是对商业专有软件行业的攻击。他们还声称开源只是一种时尚,不会长久。他们认为生产、贡献或使用开源软件的组织已经过时了,并相信世界迟早会醒悟并忘记开源软件。有些人并不轻视开源,因为他们看不到盈利的可能性,因此认为这个想法是徒劳的。

另一些人将开源软件视为救世主,将我们从商业专有软件的暴君手中拯救出来,他们相信大型软件公司迟早会被迫将他们的产权模式转变为开源或其某种变体。真相大概在中间。我认为开源行业是一个充满活力、不断发展的行业,由志同道合的人组成,他们的目标是创建安全、可靠、健壮的软件。他们通过提供基于和支持开源软件的服务来赚钱。有时是通过许可或支持销售,有时是通过定制和咨询。

无论是哪种方法,好的开源软件都可以独立成为一门生意。同样,无论你的观点如何,你必须得出结论,开源运动已经在各地的软件开发者中引起了一场革命。

现在你已经对开源革命有了一个很好的介绍,你可以决定你是否同意它的理念。如果你这样做了(我真诚地希望我已经说服了你),那么欢迎来到全球开发者社区。革命万岁!

用 MySQL 开发

您已经了解了什么是开源软件,以及使用和开发开源软件的法律后果。现在您将学习如何使用 MySQL 开发产品。正如您将会看到的,MySQL 为开发者提供了一个独特的机会,他们可以利用主要的服务器软件技术,而不必将他们的开发局限于一组固定的规则或有限的 API 套件。

MySQL 是一个关系数据库管理系统,设计用于客户机/服务器架构。MySQL 也可以作为嵌入式 数据库库。当然,如果您以前使用过 MySQL,您会熟悉它的功能,并且毫无疑问会选择 MySQL 来满足您的部分或全部数据库需求。

在系统的最底层,服务器是使用 C 和 C++混合编写的多线程模型构建的。这个核心功能大部分是在 20 世纪 80 年代早期构建的,后来在 1995 年用结构化查询语言(SQL)层进行了修改。MySQL 是使用 GNU C 编译器(GCC)构建的,它为目标环境提供了极大的灵活性。这意味着 MySQL 可以在任何 Linux 操作系统上编译使用。甲骨文在开发微软视窗和麦金塔操作系统的变体方面也取得了相当大的成功。MySQL 的客户端工具大部分是用 C 编写的,以获得更好的可移植性和速度。客户端库和访问机制可用于。NET、Java、ODBC 和其他一些语言。

这个++是什么意思?

有一次,当我还是大学生的时候。我旁听了一门 C++课程,主要是作为学习这门语言的动力。我发现学习一门新的编程语言是徒劳的,如果没有掌握这门语言的动机——比如及格。第一天上课,一个学生(不是我)问老师++代表什么。他的回答是,“额外的东西。”基于这个异想天开且不完全符合历史的答案,以及 MySQL 源代码有真正的 C 和 C++的事实,它更像 C+/-而不是 C 或 C++。C++最初被它的创建者命名为“C with classes ”,但后来在 1983 年改为 C++,对增量运算符使用了一个糟糕的双关语。换句话说,C++就是进化加法 3 的 C。

MySQL 是使用并行开发路径构建的,以确保在计划和开发软件的新版本时,产品线继续发展。软件开发遵循一个分阶段的开发过程,在这个过程中,每个阶段都会产生多个版本。MySQL 开发过程的各个阶段是:

  1. 开发—新产品或功能集作为开发树的新路径被计划和实现。
  2. Alpha—实现了特性细化和缺陷修正(bug 修复)。
  3. Beta—特性被“冻结”(不能添加新特性),并实施额外的密集测试和缺陷修正。
  4. 发布候选版本——没有重大缺陷的稳定 beta 状态,代码被冻结(只有缺陷可能被修复)并进行最后几轮测试。
  5. 一般可用(GA)—如果没有发现重大缺陷,代码被声明为稳定的,并准备好生产发布。

在这些阶段中的任何一个阶段,您都会经常看到各种版本的 MySQL 软件。通常情况下,只有 beta 版、候选版和正式版可供下载,但根据功能的重要性或支持订阅提出的功能请求的状态,alpha 版也可能提供。

当某个特定功能代表现有功能的重大变化或显著改进某个特定功能时,可能会在 dev.mysql.com 上提供实验室版本。一个实验室版本被认为是该特性的预览版,因此,它是用于评估目的的。通常,实验室发布的文档很少或没有。事实上,甲骨文在实验室网站上声明,“不适合生产[使用]。”你可以从 http://labs.mysql.com/下载实验报告。

当一组特性代表了功能或性能的重大进步时,可能会提供一个开发里程碑版本(DMR) 。DMR 可能具有处于不同发展阶段的几个特征。一个 DMR 软件可能有大部分功能处于测试状态,但也有一些功能处于测试状态,甚至是接近发布候选状态。因此,DMR 是跟踪和准备采用 MySQL 开发中的主要进展的关键方法。你可以在 http://dev.mysql.com/downloads/mysql/#downloads找到 DMRs。

你可以在 http://dev . MySQL . com/doc/MySQL-development-cycle/en/index . html 上阅读更多关于 MySQL 开发、实验室发布和 DMRs 的信息。

并行开发策略允许 Oracle 在开发新功能的同时维护其当前版本。当开发和缺陷修复在 5.5 中继续时,在 5.6 中读到新的特性是很常见的。这似乎令人困惑,因为我们已经习惯了商业专有软件供应商对他们自己的开发策略保密。MySQL 版本号用于跟踪版本;它们包含一个由两部分组成的产品系列号和一个版本号。例如,版本 5.6.12 是 5.6 产品线的第十二个版本。

image 提示与 Oracle 通信时,始终包含完整的版本号。仅仅陈述“alpha 版本”或“最新版本”是不够清楚的,不足以恰当地满足您的需求。

这种多次发布的理念有一些有趣的副作用。遇到使用旧版本 MySQL 的组织并不罕见。事实上,我遇到过几个与我合作的机构,他们仍然在使用 4.x 版本的产品线。这一理念实际上消除了商业专有软件所经历的升级 Shell 游戏。也就是说,每当供应商发布一个新版本,它就停止对旧版本的开发,在许多情况下停止对旧版本的支持。随着主要架构的改变,客户被迫相应地改变他们的环境和开发工作。这增加了维护基于商业专有软件的产品线的大量成本。多版本哲学通过允许组织将他们自己的产品在流通中保持更长的时间,并保证持续的支持,将组织从这种负担中解放出来。即使出现新的架构变化,如 MySQL 版,组织也有更长的准备时间,因此可以以最有效的方式消耗资源,而不必匆忙或改变长期计划。

虽然您可以下载任何版本的 MySQL,但首先要考虑您对该软件的使用。如果您计划在自己的生产环境中将它用作企业服务器,您可能希望将下载限制在产品线的稳定版本上。另一方面,如果您正在使用 LAMP 栈或另一个开发环境构建一个新的系统,那么任何其他的发布阶段都可以用于开发工作。大多数用户将下载他们打算在其环境中使用的最新版本的稳定版本。

我应该使用这本书的哪个版本?

就本书中的练习和实验而言,MySQL 5.6 的任何版本(阶段)都适用。MySQL 5.6 是 MySQL 发展过程中的一个重要里程碑,不仅因为它的高级特性和性能改进,还因为它在架构和源代码方面的重大变化。虽然本书的某些部分可能适用于 5.1 或 5.5 版本(例如,添加新功能),但大多数示例都是针对 5.6 版本的。

对于任何新的开发,Oracle 建议使用最新的稳定版本。这意味着,如果您计划向 MySQL 添加功能,并且您正在参与全球开发者社区,那么您应该在服务器最稳定的时候向其添加新功能。这为您的代码获得成功提供了最大的机会。

还要考虑到,虽然版本的阶段可能表明了它相对于新特性的状态,但是您不应该自动地将不稳定性与早期阶段相关联,或者将稳定性与后期阶段相关联。根据您对软件的使用,稳定性可能会有所不同。例如,如果您正在开发中使用 MySQL 在 LAMP stack 中构建一个新的电子商务站点,并且您没有使用 alpha 阶段引入的任何新特性,那么您使用的稳定性实际上与任何其他阶段都是一样的。最好的经验是选择具有您在最新开发阶段需要的特性的版本。

克隆人战争?

如果你花时间研究 MySQL,你很可能遇到过至少一个声称是 MySQL 的衍生物的数据库系统。虽然一些供应商提供了 MySQL 的变体(有时称为 fork 或 port)——一些由小型创业公司支持,一个由 MySQL 的原始开发者支持——但这些供应商中没有一个能够与 Oracle 培育 MySQL 的专业知识和开发能力相媲美。

你可能会遇到一些变体,也许你可以用在这本书上。然而,考虑到 MySQL 在最近版本中的主要进步,您最好使用官方的 MySQL 源代码来探索源代码。一些变体最终可能会包含最新的功能,但目前它们不太可能与 Oracle 的版本保持同步。提供变体的供应商包括 MariaDB、SkySQL 和 Percona。

为什么要修改 MySQL?

修改 MySQL 不是一件小事。如果您是一名经验丰富的 C/C++程序员,并且理解关系数据库系统的构造,您可能会马上投入其中。我们其余的人需要考虑为什么要修改数据库服务器系统,并仔细计划我们的修改。

你想修改 MySQL 的原因有很多。也许您需要一个不可用的数据库服务器或客户机功能。或者,您可能有一个定制的应用套件,它需要特定类型的数据库行为,而不是必须适应商业专有系统,对您来说,修改 MySQL 来满足您的需求更容易、更便宜。最有可能的情况是,您的组织负担不起复制 MySQL 数据库系统的复杂性和精细化,但是您需要一些东西作为您的解决方案的基础。使您的应用成为世界级的,还有什么比基于世界级的数据库系统更好的方法呢?

image 如果一个特性真的有用,有人认为它有益,开源的好处在于这个特性会融入到产品中。某个地方的某个人会贡献并构建这个特性。

像所有有效的软件开发者一样,你必须首先计划好你要做什么。从您最熟悉的计划设备和材料开始,列出您认为需要数据库服务器(或客户机)做的所有事情。花些时间评估 MySQL,看看是否有你想要的特性已经存在,并记录下它们的行为。完成这项研究后,你会对差距所在有更好的了解。这种“差距分析”将为您提供一个集中的特性和所需修改的列表。一旦确定了需要添加的特性,就可以开始检查 MySQL 源代码并尝试添加新特性。

image 警告在计划修改时,一定要彻底调查当前的 MySQL 特性。您将需要检查和试验与您的需求相似的所有 SQL 命令。虽然您可能无法使用当前的功能,但是检查现有的功能将使您能够形成一个已知行为和性能的基线,您可以用它来比较您的新功能。可以肯定的是,全球开发者社区的成员将会仔细检查新的特性,并删除那些他们认为使用当前特性可以最好地实现的特性。

这本书将向您介绍 MySQL 源代码,并教您如何添加新功能,以及改变什么(和不改变什么)的最佳实践。

后面的章节还将详细介绍您获取源代码的选项,以及如何将您的更改合并到适当的代码路径(分支)中。您还将了解 Oracle 编码指南的详细内容,这些指南规定了您的代码应该是什么样子,以及您应该避免哪些代码结构。

在 MySQL 中可以修改什么?有限制吗?

开源软件的美妙之处在于,您可以访问该软件的源代码(由各自的开源许可证保证)。这意味着你可以访问整个软件的所有内部工作。你有没有想过一个特定的开源软件产品是如何工作的?您可以通过下载源代码并研究它来找到答案。

有了 MySQL,事情就没那么简单了。MySQL 中的源代码非常复杂,在某些情况下,很难阅读和理解。可以说代码的可理解性很低。原始开发者通常认为源代码具有“天才因素”,即使对于最好的 C/C++程序员来说,这也是一个挑战。

虽然 C/C++代码的复杂性挑战可能是一个问题,但它决不会限制您修改软件的能力。大多数开发者修改源代码来添加新的 SQL 命令或改变现有的 SQL 命令,以更好地满足他们的数据库需求。然而,机会远不止简单地改变 MySQL 的 SQL 行为。您可以更改优化器、内部查询表示,甚至查询缓存机制。

你可能遇到的一个挑战不会来自你的任何一个开发者;它可能来自你的高级技术涉众。例如,我曾经对 MySQL 源代码进行了重大修改,以解决一个具有挑战性的问题。组织中的高级技术涉众质疑我的项目的有效性,不是因为解决方案或它的设计,而是因为我正在修改服务器代码本身的基础。一个利益相关者坚持认为我的改变“违背了 30 年来的数据库理论,并尝试了真正的实现。”我当然希望你永远不要遇到这种行为,但是如果你遇到了,并且你已经研究过哪些功能是可用的,它们如何不能满足(或部分满足)你的需求,你的答案应该包含无可争议的事实。如果你确实得到了这个问题或者类似的问题,提醒你的高级技术涉众开源软件的优点是它可以被修改,而且经常被修改。你也可以考虑解释一下你的新特性是做什么的,以及它将如何为每个人改善整个系统。如果你能做到这一点,你就能度过难关。

在修改 MySQL 时,你可能面临的另一个挑战是“为什么要用 MySQL?”专家们会很快指出有几个开源数据库系统可供选择。最流行的是 MySQL、Firebird、PostgreSQL 和 Berkeley DB。在开发项目中选择 MySQL 而不是其他数据库系统的原因包括:

  • MySQL 是一个支持全套 SQL 命令的关系数据库管理系统。一些开源数据库系统,如 PostgreSQL,是对象关系数据库系统,使用 API 或库进行访问,而不是接受 SQL 命令。一些开源系统使用的架构可能不适合您的环境。例如,Apache Derby 基于 Java,可能无法为您的嵌入式应用提供最佳性能。
  • MySQL 是使用 C/C++构建的,它可以为几乎所有的 Linux 平台以及 Microsoft Windows 和 Macintosh 操作系统构建。有些开源系统可能不适合您选择的开发语言。如果您必须将系统移植到您正在运行的 Linux 版本,这可能是一个问题。
  • MySQL 被设计成客户机/服务器体系结构。一些开源系统不可扩展到基于客户端的嵌入式系统之外。例如,Berkeley DB 是一组客户端库,而不是一个独立的数据库系统。
  • MySQL 是一个成熟的数据库服务器,拥有世界领先的数据库系统所拥有的经过验证的稳定性记录。一些开源数据库系统可能没有 MySQL 的安装基础,或者可能不提供您在企业数据库服务器中需要的特性。

很明显,对于开发需求和修改发生的环境来说,挑战是独特的。无论您的需求是什么,您都可以确保您可以完全访问所有的源代码,并且您的修改只受您的想象力的限制。

MySQL 的双许可证

MySQL 被授权为 GPL 下的开源软件。服务器和客户端软件以及工具和库都包含在 GPL 中。Oracle 已经将 GPL 作为其商业模式的一个主要焦点。它坚定地致力于 GNU 开源社区。

image 提示【MySQL 的完整 GPLv2 许可文本可以在http://dev . MySQL . com/doc/ref man/5.6/en/license-GNU-GPL-2-0 . html找到。如果你打算修改 MySQL 或者你以前从未见过 GPL 许可,请仔细阅读。如果您对如何解释您使用的许可证有任何疑问,请联系 Oracle。

Oracle 通过向全球开发者社区公开其源代码获得了许多好处。源代码由公众定期审查评估,第三方组织定期审核源代码,开发过程促进了公开交流和反馈的论坛,源代码在许多不同的环境中编译和测试。没有其他数据库供应商能够在保持世界级的稳定性、可靠性和功能的同时做出这样的声明。

MySQL 也作为商业产品获得许可。商业许可证允许 Oracle 拥有源代码(如前所述)以及名称、徽标和文档(如书籍)的版权。这是独一无二的,因为大多数开源公司并不自称拥有任何东西——相反,他们的知识产权是他们的经验和专业知识。Oracle 保留了该软件的知识产权,同时利用全球开发者社区的支持来扩展和发展该软件。甲骨文拥有自己的 MySQL 开发团队,在全球拥有 100 多名工程师。尽管来自世界各地的开发者参与了 MySQL 的开发,但 Oracle 雇佣了其中的许多人。

自由开放源码例外

甲骨文的自由/开源软件例外条款允许在应用中使用 GPL 许可的客户端库,而不要求衍生作品受 GPL 约束。如果您正在开发使用 MySQL 客户端库的应用,请查看 MySQL FOSS 异常以了解完整的详细信息。

http://www.mysql.com/about/legal/licensing/foss-exception/

Oracle 提供了服务器的几个主要 MySQL 版本。大多数是商业产品,可能没有相应的 GPL 版本。例如,虽然您可以下载 MySQL Cluster 的 GPL 版本,但您不能下载 MySQL Cluster 运营商级版本 的商业版本。表 1-1 总结了 Oracle 目前提供的各种服务器版本(撰写本章时)及其基本许可费用。

表 1-1 。MySQL 服务器产品和定价

Table1.1

****Costs shown are representative as of the publication of this work. Contact Oracle for accurate and up-to-date pricing.

image 提示http://mysql.com/buy-mysql/了解有关甲骨文定价和购买选项的更多信息

.

那么,到底能不能修改 MySQL 呢?

在讨论了在 GNU 公共许可证下使用开源软件的局限性之后,您可能想知道,您是否真的可以修改它。答案是:看情况。

您可以在 GPL 下修改 MySQL,当然,前提是如果您打算发布您的更改,您必须将这些更改交给项目所有者,从而履行您参与全球开发者社区的义务。如果您出于个人或教育目的试验或使用修改,您没有义务交出您的更改。

问题的核心归结于修改的好处。如果你增加了别人感兴趣的能力,而不是你自己,你应该分享它们。无论在何种情况下,在进行任何想要共享的修改之前,请务必咨询 Oracle。

修改 MySQL 的指南

在处理诸如修改 MySQL 等系统的任务时要小心。关系数据库系统是一组复杂的服务,这些服务以提供快速、可靠的数据访问的方式分层。您不希望打开源代码,插入自己的代码来看看会发生什么(但是欢迎您尝试)。相反,计划您的更改,并仔细瞄准与您的需求相关的源代码部分。

在修改了 MySQL 这样的大型系统之后,我想传授一些简单的指导原则,让你在修改 MySQL 时有一个积极的体验。

首先,决定您将使用哪个许可证。如果您已经在开源许可下使用 MySQL,并且可以自己实现修改,请继续使用 GPL。在这种情况下,你必须延续开源咒语,并回馈社区以换取免费提供的东西。根据 GPL 的条款,开发者必须做出这些改变。如果您在商业许可下使用 MySQL 或需要修改支持,请购买合适的 MySQL 版本以匹配您的服务器(CPU 核心数),并就您的修改咨询 Oracle。但是,如果您不打算分发这些修改,并且您可以在 MySQL 的未来版本中支持它们,那么您不需要更改为商业许可或者将您的商业许可更改为 GPL。

另一个建议是创建一个开发者日志,记录你所做的每一个改变或者你发现的每一个有趣的发现。你不仅可以一步一步地记录你的工作,还可以用日志来记录你正在做的事情。通过回顾和阅读你过去的日志,你会惊奇地发现你的研究。我在我的工程笔记本上发现了许多潦草的金块信息。

在尝试源代码的同时,也要在源代码中做笔记。在更改前后用注释行或注释块对源代码进行注释。这使得使用您最喜欢的文本解析器或搜索程序来定位您的所有更改变得很容易。下面演示了一种注释更改的方法:

/* BEGIN MY MODIFICATION */
/* Purpose of modification: experimentation. */
/* Modified by: Chuck */
/* Date modified: 30 May 2012 */
if (something_interesting_happens_here)
{
do_something_really_cool;
}
/* END MY MODIFICATION */

最后,不要害怕探索 MySQL 网站上的免费知识库和论坛,或者寻求全球开发者社区的帮助。这些是你最大的财富。在你发帖到某个论坛之前,确保你已经做了功课。失去信心的最快方式是在论坛上发布一条消息,却得到某人简短(但礼貌地)引用文档的回复。让你的帖子简明扼要。你不需要详细解释你为什么要这么做——只需要发布你的问题,并提供关于你所遇到的问题的所有相关信息。确保你发布到正确的论坛。大多数都是有主持人的,如果你有任何疑问,请咨询主持人,以确保你在正确的论坛上发布你的主题。

image 提示http://planet.mysql.com/是一个了解 MySQL 社区动态的好网站 ,这里汇集了来自世界各地的许多关于 MySQL 的博客帖子。

一个真实世界的例子:TiVo

你有没有想过是什么让你的 TiVo 滴答作响?如果您知道它运行在一个嵌入式 Linux 版本上,您会感到惊讶吗?

吉姆·巴顿和迈克·拉姆齐在 1997 年设计了最初的 TiVo 产品。它被定位为基于家庭网络的多媒体服务器,为瘦客户机提供流媒体内容。自然,像这样的设备必须容易学习,甚至更容易使用,但最重要的是,它必须无错误地操作,并优雅地处理电源中断(和用户错误)。

Barton 正在试验几种形式的 Linux,在 Silicon Graphics (SGI)工作时,他赞助了一个 Linux 到 SGI Indy 平台的移植。主要由于稳定的文件系统、网络、内存处理和开发者工具的支持,Barton 相信将一个版本的 Linux 移植到 TiVo 平台是可能的,并且 Linux 可以处理 TiVo 产品的实时性能目标。

然而,巴顿和拉姆齐面临着来自同龄人的挑战。当时,许多人对开源持怀疑和蔑视的态度。商业软件专家断言,开放源码软件在实时环境中永远不可靠。此外,他们认为,基于 GPL 的商业专有产品不允许修改,如果他们继续下去,该项目将成为版权诉讼和无休止的法律辩论的噩梦。幸运的是,巴顿和拉姆齐没有被吓住,仔细研究了 GPL。他们的结论是,GPL 不仅是可行的,它还允许他们保护自己的知识产权。

尽管最初的 TiVo 产品旨在成为服务器,但巴顿和拉姆齐认为带宽不足以支持如此崇高的目标。相反,他们将产品重新设计成一种客户端设备,称为 TiVo 客户端设备(TCD),就像一台复杂的录像机。他们想提供付费服务来提供电视指南和 TCD 接口。这将允许家庭用户提前选择他们想要的节目,并对 TCD 进行编程以记录它们。实际上,他们创造了现在众所周知的数字录像机(DVR)。

TCD 硬件包括一台小型嵌入式计算机,带有硬盘和内存。使用 MPEG 2 编码器和解码器创建硬件接口来读取和写入视频(视频输入和视频输出)。附加的输入/输出(I/O)设备包括音频和电信(用于访问 TiVo 服务)。TCD 还必须允许多重处理能力,以便允许记录一个信号(通道)同时回放另一个信号(通道)。这些特性需要良好的内存和磁盘管理子系统。巴顿和拉姆齐意识到这些目标对任何控制系统都是一个挑战。此外,视频接口绝不能以任何方式中断或损坏。

Barton 和 Ramsay 最需要的是一个具有完善的磁盘子系统、支持多任务处理、能够优化硬件(CPU、内存)使用的系统。因此,Linux 是 TCD 操作系统的合理选择。生产目标和预算约束限制了 CPU 的选择。TCD 选择了 IBM PowerPC 403GCX 处理器。不幸的是,在选择的处理器上没有运行 Linux 的端口。这意味着巴顿和拉姆齐必须将 Linux 移植到处理器平台上。

虽然移植是成功的,但 Barton 和 Ramsay 发现他们需要对 Linux 内核进行一些专门的定制,以满足硬件的需求和限制。例如,它们绕过了文件系统缓冲区高速缓存,以便允许更快地移动或处理进出用户空间的视频信号。他们还增加了广泛的性能增强、日志记录和恢复功能,以确保 TCD 可以从断电或用户错误中快速恢复。

运行 TCD 的应用是在基于 Linux 的个人计算机上构建的,并毫不夸张地移植到修改后的 Linux 操作系统上——这证明了 Linux 操作系统的稳定性和互操作性。当 Barton 和 Ramsay 完成移植和应用工作后,他们进行了广泛的测试,并于 1999 年 3 月交付了世界上第一台 DVR。

TCD 是使用最广泛的消费产品之一,运行定制的嵌入式 Linux 操作系统。显然,TCD 的故事是一个通过修改开源软件可以实现什么的光辉例子。然而,故事并没有到此结束。Barton 和 Ramsay 发表了他们的 Linux 内核移植,并附有源代码。他们的增强已经融入了最新版本的 Linux 内核。

说服你的老板修改开源软件

如果你有一个想法和一个基于它的商业模式,走开源路线可以在将你的产品推向市场的过程中节省大量时间。事实上,您的项目可能会节省大量的开发收入,并允许您比竞争对手更快地将产品推向市场。如果你需要修改开源软件,这一点尤其正确——你已经做了功课,可以展示使用开源软件的成本效益。

不幸的是,许多经理已经被商业专有软件世界所限制,拒绝接受基于开源软件的产品来产生收入的概念。那么如何改变他们的想法呢?用 TiVo 的故事作为弹药。向你的老板展示你从 TiVo 故事和本章剩余部分中获得的知识,以驱散关于 GPL 和开源软件可靠性的神话。不过,要小心。如果你像大多数开源专家一样,你的热情经常会被解释为对高级技术人员的威胁。

列出坚持商业专有观点的技术利益相关者。让这些人参与关于开源软件的对话,并回答他们的问题。最重要的是,要有耐心。这些人并没有你想象的那么厚,最终他们会分享你的热情。

一旦你让高级技术人员接受了教育,并且有了开源思维,就用一个修改过的提议重新让你的管理层参与进来。一定要带上一个高级技术人员作为挡箭牌(和理智的声音)。在这种情况下,胜利正在扭转商业所有权统治的潮流。

摘要

在本章中,您探索了开源软件的起源以及 MySQL 成为世界级数据库管理系统的过程。您了解了什么是开源系统,以及它们与商业专有系统的比较。您看到了开源许可的弱点,并发现了作为全球开发者社区成员的责任。

您还了解了如何使用 MySQL 进行开发,并了解了源代码的特征和修改指南。您了解了 Oracle 的双许可实践以及根据您的需求修改 MySQL 的意义。最后,您看到了一个在商业产品中成功集成开源系统的例子。

在接下来的章节中,您将了解更多关于关系数据库系统的剖析,以及如何开始定制 MySQL 以满足您的需求。稍后,在本书的第 2 部分和第 3 部分,将向您介绍 MySQL 的内部工作原理,并探索代码中最私密的部分。

http://www . GNU . org/GNU/thegnupproject . html

2http://www.microsoft.com/en-us/openness/default.aspx#home】??

3http://www.research.att.com/∼bs/bs_faq.html#name】??

二、数据库系统的剖析

虽然您可能知道关系数据库管理系统(RDBMS)的基础知识,并且是管理该系统的专家,但是您可能从未探索过数据库系统的内部工作方式。我们中的大多数人都接受过管理数据库系统的培训,并拥有这方面的经验,但是无论是学术培训还是专业培训都没有包括太多关于数据库系统构建方式的内容。数据库专业人员可能永远不需要这些知识,但是了解系统如何工作是有好处的,这样您就可以了解如何最好地优化您的服务器,甚至如何最好地利用它的特性。

虽然理解 RDBMS 的内部工作方式对于托管数据库甚至维护服务器或开发使用该系统的应用来说都不是必需的,但是了解系统的组织方式对于修改和扩展其功能来说是必不可少的。掌握最流行的数据库系统的基本原理以理解这些系统与 RDBMS 相比如何也是很重要的。

本章涵盖了 RDBMSs 包含的子系统的基础知识以及它们是如何构造的。我使用 MySQL 系统的剖析来说明现代 RDBMSs 的关键组件。那些已经研究过这种系统的内部工作原理,并希望了解 MySQL 架构的人可以跳到“MySQL 数据库系统”

数据库系统的类型

大多数数据库专业人员使用 RDBMSs,但是其他几种类型的数据库系统也越来越流行。下面几节简要概述了三种最流行的数据库系统:面向对象的、对象关系的和关系的。了解这些系统的体系结构和一般特性非常重要,这样才能充分认识到 Oracle 通过将 MySQL 开发为开源软件并向所有人公开系统的源代码所提供的机会。这允许我向你们展示盒子里发生了什么。

面向对象的数据库系统

面向对象数据库系统(OODBSs)是一种存储和检索机制,它通过将数据作为对象直接操作来支持面向对象编程(OOP)范例。它们包含真正的面向对象(OO)类型的系统,允许对象在应用和使用中持久化。然而,大多数缺乏标准的查询语言 1 (对数据的访问通常是通过编程接口),因此不是真正的数据库管理系统。

OODBSs 是 RDBMS 的一个有吸引力的替代方案,特别是在 RDBMS 的建模能力或性能不足以将数据作为对象存储在表中的应用领域。这些应用维护大量从不删除的数据,从而管理单个对象的历史。OODBSs 最不寻常的特性是,它通过指定可以通过 OOP 接口应用于这些对象的结构和操作来提供对复杂对象的支持。

OODBSs 特别适合于尽可能接近真实世界的建模,而不会在实体之间和实体内部强加不自然的关系。面向对象的哲学提供了一个整体的以及面向建模的真实世界的视图。这些视图对于处理难以捉摸的主题是必要的,例如建模时间变化,特别是在向结构化数据添加 OO 特性时。尽管有许多开放源码的面向对象数据库,但大多数都部分基于支持查询语言接口的关系系统,因此不是真正的面向对象数据库;相反,它们更像具有面向对象接口的关系数据库。真正的面向对象数据库需要通过编程接口进行访问。

面向对象数据库系统的应用领域包括地理信息系统、科学和统计数据库、多媒体系统、图像存档和通信系统、语义网解决方案和 XML 仓库。

OODBS 最大的适应性是对数据(或对象)及其行为(或方法)的裁剪。大多数 OODBS 系统集成商依赖 OO 方法来描述数据,并在设计中用这种表达能力来构建他们的解决方案。因此,面向对象的数据库系统是用特定的实现来构建的,而不是通用的或一般化的,具有像 RDBMSs 那样的语句-响应类型的接口。

对象关系数据库系统

对象关系数据库管理系统是面向对象理论在关系数据库管理系统中的应用。ORDBMSs 提供了一种机制,允许数据库设计者实现面向对象数据的结构化存储和检索机制。ORDBMSs 提供了关系模型的基础——含义、完整性、关系等——同时扩展了模型,以一种以对象为中心的方式存储和检索数据。在许多情况下,实现纯粹是概念性的,因为 OO 概念到关系概念的映射充其量只是尝试性的。对关系技术的修改或扩展包括对 SQL 的修改,允许表示对象类型、标识、操作封装和继承。这些通常作为复杂类型松散地映射到关系理论。尽管很有表现力,但 SQL 扩展并不允许真正的对象操作和对 OODBSs 的控制。一个流行的 ORDBMS 是 ESRI 的 ArcGIS 地理数据库环境。其他例子包括 Illustra、PostgreSQL 和 Informix。

关系数据库管理系统中使用的技术通常基于关系模型。大多数关系数据库管理系统是使用现有的商业关系数据库管理系统(RDBMSs)如 Microsoft SQL Server 实现的。因为这些系统是基于关系模型的,所以它们受到将 OO 概念转换成关系机制的问题的困扰。在面向对象的应用中使用关系数据库的许多问题包括:

  • OO 概念模型不容易映射到数据表。
  • 复杂的映射意味着复杂的程序和查询。
  • 复杂的程序意味着维护问题。
  • 复杂的程序意味着可靠性问题。
  • 复杂的查询可能无法优化,可能会导致性能下降。
  • 对象概念到复杂类型 2 的映射比关系系统更容易受到模式变化的影响。
  • 全选的 OO 性能...WHERE 查询速度较慢,因为它涉及多次连接和查找。

尽管这些问题看起来很严重,但是通过在底层关系数据库和面向对象应用之间进行通信的面向对象应用层的应用,它们很容易被减轻。这些应用层允许将对象转换成结构化的(或持久的)数据存储。有趣的是,这种做法违反了 ORDBMS 的概念,因为您现在使用面向对象的访问机制来访问数据,这并不是创建 ORDBMS 的原因。创建它们是为了通过提供查询语言的扩展来允许在 RDBMS 中存储和检索对象。

虽然 ORDBMSs 和 oodbs 很相似,但是 oodbs 在哲学上有很大的不同。OODBSs 试图通过编程接口和平台向 OO 编程语言添加数据库功能。相比之下,ORDBMSss 试图使用传统的查询语言和扩展向 RDBMS 添加丰富的数据类型。OODBSs 试图实现与 OOP 语言的无缝集成。ORDBMS 不尝试这种级别的集成,通常需要一个中间应用层来将信息从面向对象应用翻译到 ORDBMS 甚至主机 RDBMS。类似地,面向对象数据库的目标是以面向对象观点作为其核心工程观点的应用。ORDBMSs 针对支持大量数据的大型数据存储和基于对象的系统(如 GIS 应用)进行了优化。最后,OODBSs 的查询机制以使用专门的 OO 查询语言的对象操作为中心。ORDBMS 查询机制适合于使用 SQL 标准的扩展来快速检索大量数据。与具有优化查询机制的真正面向对象数据库不同,如对象描述语言(ODL)和对象查询语言(OQL),ORDBMSs 使用的查询机制是 SQL 查询语言的扩展。

GIS 应用的 ESRI 产品套件包含一个名为 Geodatabase(地理数据库的简称)的产品,它支持地理数据元素的存储和管理。地理数据库是一种支持空间数据的对象关系数据库。这是作为 ORDBMS 实现的空间数据库的一个例子。

image 空间数据库系统不需要在 ORDBMSs 甚至 OODBSs 中实现。ESRI 已经选择将地理数据库作为一个 ORDBMS 来实现。更重要的是,GIS 数据可以存储在 RDBMS 中,RDBMS 已经扩展为支持空间数据。看哪!这正是 MySQL 发生的事情。Oracle 向 MySQL RDBMS 添加了一个空间数据引擎。

尽管 ORDBMSs 是基于关系数据库平台的,但它们也提供了一些数据封装和行为层。大多数关系数据库管理系统是关系数据库管理系统的特殊形式。那些提供 ORDBMSs 的数据库供应商通常通过修改 SQL 来包含对象描述符和空间查询机制,从而构建对语句响应接口的扩展。这些系统通常是为特定的应用而构建的,并且像 OODBSs 一样,它们的一般用途是有限的。

关系数据库系统

RDBMS 是一种基于数据关系模型的数据存储和检索服务,由 E. F. Codd 于 1970 年提出。这些系统是结构化数据的标准存储机制。大量的研究致力于改进 Codd 提出的基本模型,正如 C. J. Date 在数据库关系模型:回顾和分析中所讨论的。 3 这种理论和实践的演变最好地记录在第三个宣言中。

关系模型是存储库(数据库)的直观概念,可以通过使用一种称为查询语言的机制来检索、更新和插入数据,从而方便地查询存储库。关系模型已经被许多厂商实现,因为它具有完善的系统理论、坚实的数学基础和简单的结构。最常用的查询机制是结构化查询语言(SQL),它类似于自然语言。虽然关系模型中不包含 SQL,但是 SQL 提供了关系模型在 RDBMSs 中实际应用的一个组成部分。

数据被表示为关于某个实体的相关信息(属性)。属性的值集被形成为一个元组(有时被称为记录)。然后将元组存储在包含具有相同属性集的元组的表中。然后,表可以通过对域、键、属性和元组的约束与其他表相关联。

记录还是元组:有区别吗?

许多人错误地认为记录是元组的通俗说法。一个重要的区别是元组是一组有序的元素,而记录是没有顺序感的相关项目的集合。但是,列的顺序在记录的概念中很重要。有趣的是,在 SQL 中,查询的结果可以是一条记录,而在关系理论中,每个结果都是一个元组。许多文本交替使用这些术语,给许多人造成了混淆。

在探索 MySQL 架构和源代码时,我们会遇到专门用来描述结果集中的一行或数据更新的一行的术语 record。虽然 MySQL 中的记录数据结构是有序的,但与元组的相似之处仅此而已。

大多数实现选择的查询语言是结构化查询语言(SQL)。SQL 是在 20 世纪 80 年代作为一种标准提出的,目前是一种行业标准。不幸的是,许多人似乎认为 SQL 是基于关系理论的,因此是一个合理的理论概念。这种误解可能是由工业带来的一种现象助长的。几乎所有的 RDBMSs 都实现某种形式的 SQL。这种流行错误地忽略了 SQL 的许多缺点,包括:

  • SQL 不支持关系模型所描述的域。
  • 在 SQL 中,表可以有重复的行。
  • 结果(表)可以包含未命名的列和重复的列。
  • 主机数据库系统对空值(缺失值)的实现已经被证明是不一致和不完整的。因此,许多人错误地将空值的错误处理与 SQL 联系起来,而事实上,SQL 只是返回数据库系统提供的结果。 5

RDBMSs 中使用的技术多种多样。一些系统被设计来优化关系模型的一些部分或者模型对数据的一些应用。RDBMSs 的应用范围从简单的数据存储和检索到具有复杂数据、过程和工作流的复杂应用套件。这可以是简单的存储光盘或 DVD 收藏的数据库,或者是设计用来管理酒店预订系统的数据库,甚至是设计用来管理网上信息的复杂的分布式系统。正如我在第 1 章中提到的,许多网络和社交媒体应用实现了 LAMP 堆栈,MySQL 由此成为存储托管数据的数据库。

关系数据库系统提供了最健壮的数据独立性和数据抽象。通过使用关系的概念,RDBMS 提供了一种真正通用的数据存储和检索机制。当然,缺点是这些系统非常复杂,需要大量的专业知识来构建和修改。

在下一节中,我将介绍一个典型的 RDBMS 体系结构,并研究该体系结构的每个组件。稍后,我将研究一个 RDBMS (MySQL)的特定实现。

MYSQL 是关系数据库系统吗?

许多数据库理论家会告诉你,世界上真正的 RDBMSs 非常少。他们还会指出,什么是关系型,什么不是关系型,很大程度上取决于您对数据库系统所支持的特性的定义,而不是系统符合 Codd 的关系型模型的程度。

从纯营销的角度来看,MySQL 提供了许多被认为是 RDBMSs 所必需的特性。这些包括但不限于使用外键将表相互关联的能力、关系代数查询机制的实现以及索引和缓冲机制的使用。显然,MySQL 提供了所有这些特性,甚至更多。

那么 MySQL 是 RDBMS 吗?这取决于你对关系的定义。如果你考虑 MySQL 的特性和演变,你应该得出结论,它确实是一个 RDBMS。然而,如果您坚持 Codd 关系模型的严格定义,您将会得出结论,MySQL 缺少模型中表示的一些特性。但是话说回来,许多其他 RDBMSs 也是如此。

关系数据库系统体系结构

RDBMS 是一个复杂的系统,由专门的机制组成,用于处理存储和检索信息所需的所有功能。RDBMS 的体系结构经常被比作操作系统的体系结构。如果你考虑 RDBMS 的使用,特别是作为一个客户机主机的服务器,你会发现它们与操作系统有许多共同之处。例如,拥有多个客户机意味着系统必须支持许多请求,这些请求可能会也可能不会读写相同的数据或来自相同位置的数据(比如一个表)。因此,RDBMSs 必须有效地处理并发性。类似地,RDBMSs 必须为每个客户机提供对数据的快速访问。这通常是通过使用文件缓冲技术来实现的,该技术将最近或最常用的数据保存在内存中,以便更快地访问。并发性要求内存管理技术类似于操作系统中的虚拟内存系统。与操作系统的其他相似之处包括网络通信支持和优化算法,旨在最大限度地提高查询执行的性能。

我将从用户的角度开始我们对体系结构的探索,从发出查询到检索数据。下面几节写的是让你可以跳过你熟悉的,读你感兴趣的。但是,我鼓励您阅读所有章节,因为它们详细介绍了典型的 RDBMS 是如何构建的。

客户端应用

大多数 RDBMS 客户机应用都是作为独立的可执行程序开发的,这些程序通过通信路径(例如,诸如套接字或管道之类的网络协议)连接到数据库。有些通过编程接口直接连接到数据库系统,其中数据库系统成为客户机应用的一部分。在这种情况下,我们称数据库为嵌入式系统。有关嵌入式数据库系统的更多信息,请参见第 6 章

大多数通过通信路径连接到数据库的系统都是通过一组称为数据库连接器的协议来实现的。数据库连接器最常基于开放式数据库连接(ODBC) 6 模型。MySQL 还支持 Java (JDBC)、PhP、Python 和微软的连接器。NET(参见“MySQL 连接器”).大多数 ODBC 连接器的实现也支持网络协议上的通信。

ODBC 是应用编程接口(API)的规范。ODBC 设计用于将 SQL 命令传输到数据库服务器,检索信息,并将其提供给调用应用。ODBC 实现包括一个设计为使用 API 的应用,它充当 ODBC 库的中介,一个支持 API 的核心 ODBC 库,以及一个为特定数据库系统设计的数据库驱动程序。我们通常将客户端访问、API 和驱动程序的集合称为连接器。因此,ODBC 连接器充当客户机应用和数据库服务器之间的“解释器”。ODBC 已经成为几乎所有关系(和大多数对象关系)数据库系统的标准。数百个连接器和驱动程序可用于各种各样的客户机和数据库系统。

当我们考虑客户机应用时,我们通常会考虑向数据库服务器发送信息和从数据库服务器检索信息的程序。甚至我们用来配置和维护数据库服务器的应用也是客户端应用。大多数这些实用程序通过与数据库应用相同的网络路径连接到服务器。有些使用 ODBC 连接器或类似 Java 数据库连接(JDBC) 的变体。少数使用专门的协议来管理服务器,以达到特定的管理目的。其他的,比如 phpMyAdmin,使用端口或套接字。

不管它们的实现如何,客户机应用都向数据库系统发出命令,并检索这些命令的结果,解释和处理这些结果,然后将它们呈现给用户。标准的命令语言是 SQL。客户端通过 ODBC 连接器向服务器发出 SQL 命令,ODBC 连接器使用驱动程序指定的网络协议将命令传输到数据库服务器。该过程的图形描述如图 2-1 中的所示。

9781430246596_Fig02-01.jpg

图 2-1。客户端应用/数据库服务器通信

MYSQL 连接器

Oracle 为开发者提供了几个数据库连接器,使应用能够与 MySQL 进行交互。这些连接器可以用于与行业标准兼容的应用和工具,包括 ODBC 和 JDBC。这意味着任何使用 ODBC 或 JDBC 的应用都可以使用 MySQL 连接器。适用于 Oracle 的 MySQL 连接器有:

  • 连接器/ODBC–用于 Windows、Linux、Mac OS X 和 Unix 平台的标准 ODBC 连接器。
  • connector/J[ava]–用于 Java 平台和开发。
  • 连接器/网络–窗户。Net 应用开发。
  • 连接器/MXJ–用于在 Java 应用中嵌入 MySQL 服务器的 MBean。
  • 连接器/c++–c++开发。
  • connector/C(libmysql)–C 应用开发。
  • 连接器/Python–Python 应用开发。

您可以在 http://www.mysql.com/downloads/connector/的查看和下载连接器

查询界面

诸如 SQL 的查询语言是一种可以表示向数据库系统提出的问题的语言(它具有语法和语义)。事实上,在数据库系统中使用 SQL 被认为是它们成功的主要原因之一。SQL 提供了几个语言组,形成了使用数据库系统的全面基础。数据库专业人员使用数据定义语言 (DDL) 来创建和管理数据库。任务包括创建和修改表、定义索引和管理约束。数据操作语言 (DML) 被数据库专业人员用来查询和更新数据库中的数据。任务包括添加和更新数据以及查询数据。这两种语言组构成了数据库系统支持的大多数命令。

SQL 命令是使用专门的语法形成的。下面给出了 SQL 中的一个 SELECT 命令的语法。符号用斜体表示用户定义的变量,用方括号([])表示可选参数。

SELECT [DISTINCT] listofcolumns
FROM listoftables
[WHERE expression (predicates in CNF)]
[GROUP BY listofcolumns]
[HAVING expression]
[ORDER BY listof columns];

该命令的语义是: 7

  1. 形成来自子句的中的表的笛卡尔积,从而仅形成那些出现在其他子句中的引用的投影。
  2. 如果存在一个 WHERE 子句,则应用引用的给定表的所有表达式。
  3. 如果存在一个 GROUP BY 子句,则在指定属性的结果中形成组。
  4. 如果存在具有子句的,则为组应用过滤器。
  5. 如果存在一个 ORDER BY 子句,则以指定的方式对结果进行排序。
  6. 如果存在 DISTINCT 关键字,则从结果中删除重复的行。

前面的代码示例代表了大多数 SQL 命令;所有这样的命令都有必需的部分,大多数还有可选的部分以及基于关键字的修饰符。

一旦查询语句通过网络协议(称为运输传输到客户端,数据库服务器就必须解释并执行命令。从这一点开始,查询语句被简称为查询,因为它表示数据库系统必须提供答案的问题。此外,在接下来的部分中,我假设查询属于 SELECT 类型,其中用户已经发出了一个数据请求。然而,所有的查询,无论是数据操作还是数据定义,在系统中都遵循相同的路径。也正是在这一点上,我们考虑在数据库服务器本身中执行的操作。这个过程的第一步是解读客户的要求——也就是说,查询必须被解析并分解成可以执行的元素。

查询处理

在以客户机/服务器模式运行的数据库系统的环境中,数据库服务器负责处理客户机提出的查询并相应地返回结果。这被称为查询传送 ,其中查询被传送到服务器并返回有效负载(数据)。查询传送的好处是减少了查询的通信时间,并且能够利用服务器资源,而不是使用更有限的客户端资源来执行查询。该模型还允许将数据在服务器上的存储和检索方式与数据在客户机上的使用方式分开。换句话说,客户机/服务器模型支持数据独立性。

数据独立性是 Codd 在 1970 年引入的关系模型的主要优点之一:将物理实现逻辑模型分离。按 Codd, 8

必须保护大型数据库的用户,使他们不必知道数据在机器中是如何组织的。。。当数据的内部表示改变时,终端用户的活动和大多数应用应该保持不受影响。

这种分离允许开发一组强大的逻辑语义,独立于特定的物理实现。数据独立(Elmasri 和 Navathe 9 称之为物理数据独立)的目标是每个逻辑元素独立于所有物理元素(见表 2-1 )。例如,数据与由元组(行)排列的属性(字段)的关系(表)的逻辑布局完全独立于数据在存储介质上的存储方式。

表 2-1 。数据库设计的逻辑和物理模型

逻辑模型 物理模型
查询语言 排序算法
关系代数 存储机制
关系演算 索引机制
雷尔瓦斯 数据表示法

数据独立性的一个挑战是数据库编程变成了一个两部分的过程。首先,编写逻辑查询——描述查询应该做什么。其次,是物理计划的编写,它向展示了如何实现逻辑查询。

逻辑查询通常可以以许多不同的形式编写,例如 SQL 之类的高级语言或代数查询树。 10 例如,在传统的关系模型中,一个逻辑查询可以用关系演算或关系代数来描述。关系演算在关注需要计算什么方面更好。关系代数更接近于提供一种算法,让您找到您正在查询的内容,但是仍然省略了查询评估中涉及的许多细节。

物理计划是一个查询树,其实现方式可以被数据库系统的查询执行引擎理解和处理。查询树 是一种树形结构,其中每个节点包含一个查询操作符,并且有多个子节点,这些子节点对应于操作中涉及的表的数量。查询树可以通过优化器转换成执行计划。这个计划可以看作是查询执行引擎可以执行的程序。

查询语句在执行之前要经过几个阶段;解析、验证、优化、计划生成/编译和执行。图 2-2 描述了一个典型的数据库系统会采用的查询处理步骤。对每个查询语句进行有效性分析,并检查语法是否正确以及查询操作的标识。然后,解析器以中间形式输出查询,以允许优化器形成高效的查询执行计划。然后,执行引擎执行查询,并将结果返回给客户端。这个过程在图 2-2 中显示,一旦解析完成,查询将被验证错误,然后被优化;选择并编制计划;最后执行查询。

9781430246596_Fig02-02.jpg

图 2-2。查询处理步骤

这个过程的第一步是将逻辑查询从 SQL 转换成关系代数中的查询树。这一步由解析器完成,通常包括将 SQL 语句分成几个部分,然后从那里构建查询树。下一步是将逻辑代数中的查询树翻译成物理计划。通常,许多计划都可以实现查询树。寻找最佳执行计划的过程称为查询优化 。也就是说,对于某些查询执行性能度量(例如,执行时间),我们希望找到具有最佳执行性能的计划。在优化器的搜索空间内,计划应该是最优的或接近最优的。优化器首先将关系代数查询树复制到它的搜索空间中。然后,优化器通过形成备选执行计划(到有限迭代)来扩展搜索空间,然后搜索最佳计划(执行最快的计划)。

在这个通用级别上,优化器可以被视为 SQL 语言查询编译器的代码生成部分。事实上,在一些数据库系统中,编译步骤将查询翻译成可执行程序。然而,大多数数据库系统将查询翻译成可以使用内部执行步骤库执行的形式。这种情况下的代码编译会生成由查询执行引擎解释的代码,只是优化器的重点是生成“非常高效”的代码。例如,优化器使用数据库系统的目录来获得关于查询所引用的存储关系的信息(例如,元组的数量),这是传统编程语言编译器通常不做的。最后,优化器将最佳物理计划从其内存结构中复制出来,并发送给查询执行引擎,查询执行引擎使用存储的数据库中的关系作为输入来执行计划,并生成与查询条件匹配的行的表。

所有这些活动都需要额外的处理时间,并通过迫使数据库实施者将查询优化器和执行引擎的性能作为其整体效率的一个因素来考虑,从而给该过程带来了更大的负担。这种优化的成本很高,因为有许多备选执行计划使用不同的访问方法(读取数据的方式)和不同的执行顺序。因此,可以为一个查询生成无限数量的计划。然而,数据库系统通常将问题局限于少数已知的最佳实践。

大量查询计划的一个主要原因是,许多重要运行时参数的不同值都需要优化,这些参数的实际值在优化时是未知的。数据库系统对数据库内容(例如,关系属性中的值分布)、物理模式(例如,索引类型)、系统参数的值(例如,可用缓冲区的数量)以及查询常量的值做出某些假设。

查询优化器

有些人错误地认为查询优化器执行了查询执行阶段的所有步骤。正如您将看到的,查询优化只是查询执行过程中的一个步骤。以下段落详细描述了查询优化器,并说明了优化器在查询执行过程中的作用。

查询优化是查询编译过程的一部分,它将高级非过程语言(如 SQL)中的数据操作语句翻译成更详细的过程操作符序列,称为查询计划。查询优化器通常通过估计许多备选计划的成本来选择一个计划,然后选择其中最便宜的(执行速度最快的)。

使用基于计划的方法进行查询优化的数据库系统假定可以使用许多计划来产生任何给定的查询。虽然这是真的,但不是所有的计划在执行查询所需的资源数量(或成本)上都是相等的,也不是所有的计划都在相同的时间内执行。那么,目标就是找到成本最低和/或运行时间最少的计划。当设计用于嵌入式集成或在小平台上运行的系统(资源可用性低)与对更高吞吐量(或时间)的需求相比时,资源使用或成本使用的区别是经常遇到的权衡。

图 2-3 描述了一个基于计划的查询处理策略,其中查询沿着箭头的路径进行。SQL 命令被传递给查询解析器,在那里被解析和验证,然后被转换成内部表示,通常基于关系代数表达式或查询树,如前所述。然后将查询传递给查询优化器,查询优化器检查所有等价的代数表达式,为每个组合生成不同的计划。然后,优化器选择成本最低的计划,并将查询传递给代码生成器,代码生成器将查询转换为可执行的形式,可以是直接可执行的,也可以是解释性代码。然后,查询处理器执行查询,并在结果集中一次返回一行。

这种常见的实现方案是大多数数据库系统的典型特征。然而,随着时间的推移,运行数据库系统的机器已经有所改进。查询计划不再有不同的执行成本。事实上,大多数查询计划的执行成本大致相同。这种认识导致一些数据库系统实现者采用一种查询优化器,这种查询优化器使用一些众所周知的最佳实践或规则(称为试探法)来优化查询。一些数据库系统使用基于一种形式的混合优化技术,同时在执行过程中保持其他技术的某些方面。

9781430246596_Fig02-03.jpg

图 2-3。基于计划的查询处理

执行查询优化的四种主要方法是

  • 基于成本的优化
  • 试探优化
  • 语义优化
  • 参数优化

虽然没有任何优化技术可以保证最佳的执行计划,但是所有这些方法的目标都是为查询生成一个有效的执行,保证正确的结果。

基于成本的优化器通过使用等价规则从给定的查询生成一系列查询评估计划,并根据收集的关于执行查询所需的关系和操作的度量(或统计)选择成本最低的计划。对于一个复杂的查询,许多等价的计划是可能的。基于成本的优化的目标是利用从过去的查询中收集的索引和统计信息来安排查询执行和表访问。Microsoft SQL Server 和 Oracle 等系统使用基于成本的优化器。

启发式优化器在选择替代实现之前,使用关于如何将查询塑造成最佳形式的规则。试探法或规则的应用可以消除可能低效的查询。使用试探法来形成查询计划可以确保查询计划在评估之前最有可能(但不总是)得到优化。启发式优化的目标是应用确保查询执行“良好”实践的规则。使用启发式优化器的系统包括 Ingres 和各种学术变体。这些系统通常使用启发式优化来避免真正糟糕的计划,而不是作为优化的主要手段。

语义优化的目标是形成查询执行计划,该计划使用数据库的语义或拓扑以及其中的关系和索引来形成查询,以确保在给定数据库中执行查询的最佳实践。虽然还没有在商业数据库系统中作为主要的优化技术来实现,但是语义优化目前是大量研究的焦点。语义优化的前提是优化器对实际的数据库模式有基本的了解。当提交一个查询时,优化器使用它对系统约束的了解来简化或忽略一个特定的查询,如果它保证返回一个空的结果集的话。这项技术很有希望在未来的 RDBMSs 中进一步提高查询处理效率。

参数查询优化将启发式方法的应用与基于成本的优化相结合。所得到的查询优化器提供了一种生成较小的有效查询计划集的方法,可以根据这些计划来估计成本,从而可以执行该计划集中成本最低的计划。

使用混合优化器的数据库系统的一个例子是 MySQL。MySQL 中的查询优化器是围绕 select-project-join 策略设计的,该策略结合了基于成本的优化器和使用已知优化机制的启发式优化器,从而减少了基于成本的优化可以选择的最小执行路径。这种策略确保了一个整体“好”的执行计划,但是它不一定产生最好的计划。这种策略已经被证明对于在不同环境中运行的各种各样的查询非常有效。MySQL 的内部表示已经表现得足够好,可以与最大的生产数据库系统的执行速度相媲美。

使用基于成本的优化器的数据库系统的一个例子是微软的 SQL Server。SQL Server 中的查询优化器是围绕经典的基于成本的优化器设计的,该优化器将查询语句转换为可以高效执行并返回所需结果的过程。优化器使用从过去的查询中记录的值和数据库中数据的特征收集的信息或统计数据来创建表示相同查询的替代过程。统计数据应用于每个过程,以预测哪个过程可以更有效地执行。一旦确定了最有效的过程,就开始执行并将结果返回给客户机。

通过使用未绑定的参数,例如用户谓词,查询的优化可能会变得复杂。例如,当执行存储过程时,如果存储过程中的查询接受来自用户的参数,则创建未绑定参数。在这种情况下,查询优化可能是不可能的,或者它可能不会产生最低的成本,除非在执行之前获得了谓词的一些知识。如果很少有记录满足谓词,那么即使是基本索引也远远优于文件扫描。如果许多记录都符合条件,则情况正好相反。如果在执行优化时因为谓词未绑定而导致选择性未知,那么在这些备选计划中的选择应该推迟到执行时进行。

选择性问题可以通过构建优化器来解决,这些优化器可以采用谓词作为开放变量,并通过基于历史查询执行生成所有可能发生的查询计划,以及利用基于成本的优化器的统计信息(包括谓词属性的频率分布)来执行查询计划规划。

查询的内部表示

在数据库系统中,可以使用几种不同形式的原始 SQL 命令来表示查询。这些替代形式的存在是由于 SQL 中的冗余、特定约束下的子查询和连接的等价性,以及可以从 WHERE 子句中的谓词得出的逻辑推理。拥有替代形式的查询给数据库实现者带来了一个问题,因为查询优化器必须为查询选择最佳的访问计划,而不管用户最初是如何形成查询的。

一旦查询优化器形成了有效的执行计划(启发式和混合优化器)或者选择了最有效的计划(基于成本的优化器),查询就被传递到流程的下一个阶段:执行。

查询执行

数据库系统可以使用几种方法来执行查询。大多数使用迭代解释执行策略。

迭代方法提供了产生可用于处理离散操作(连接、项目等)的调用序列的方法。),但它们的设计并不包含内部表示的功能。将查询转换成迭代方法使用了函数式编程和程序转换技术。一些可用的算法从基于代数的查询规范生成迭代程序。例如,一些算法将查询规范翻译成递归程序,在算法生成执行计划之前,递归程序通过转换规则集进行简化。另一种算法使用两级翻译。第一级使用一组较小的转换规则来简化内部表示,第二级在生成执行计划之前应用功能转换。

这种机制的实现创建了一组使用高级语言形成的已定义的编译功能原语,然后通过调用堆栈或过程调用序列将它们链接在一起。当创建并选择执行查询执行计划时,编译器(通常与创建数据库系统的编译器相同)用于将过程调用编译成二进制可执行文件。由于迭代方法的高成本,编译后的执行计划通常会被存储起来,以供类似或相同的查询重用。

另一方面,解释方法使用基本操作的现有编译抽象来形成查询执行。所选择的查询执行计划被重新构造为一个方法调用队列,每个方法调用都从队列中取出并进行处理。然后将结果存储在内存中,供下一次或后续调用使用。这种策略的实现通常被称为懒惰评估,因为可用的编译方法集并没有针对最佳性能进行优化;相反,这些方法是为通用性而优化的。大多数数据库系统使用查询执行的解释方法。

一个经常混淆的领域是编译的概念。一些数据库专家认为编译后的查询是迭代查询执行计划的实际编译,但是在 Date 的工作中,编译后的查询只是一个已经优化并存储以供将来执行的查询。我不会使用单词 compiled,,因为 MySQL 查询优化器和执行引擎不会存储查询执行计划以备后用(MySQL 查询缓存是一个例外)。我不认为我们可以在这里比较或提及查询缓存。对已执行查询的评估甚至与计划无关,而是与接收的 SQL 和存储的 SQL 之间的文献比较有关,直接与一组已检索的信息相关,查询执行也不需要任何编译或汇编工作。有趣的是,存储过程的概念符合第二个类别;它被编译(或优化)以便以后执行,并且可以对满足其输入参数的数据运行多次。

查询执行评估查询树(或由内部结构表示的查询)的每个部分,并为每个部分执行方法。支持的方法反映了在关系代数、投影、限制、联合、交集等中定义的那些操作。对于这些操作中的每一项,查询执行引擎都会执行一个方法来评估传入的数据,并将处理后的数据传递给下一步。例如,在项目操作中只返回数据的某些属性(或列)。在这种情况下,查询执行引擎将剥离不符合限制规范的属性的数据,并将剩余的数据传递给树(或结构)中的下一个操作。表 2-2 列出了支持的最常见操作,并简要描述了每种操作。

表 2-2。查询操作

操作 描述
限制 返回与 WHERE 子句的条件(谓词)匹配的元组(某些系统以相同或相似的方式处理 HAVING 子句)。这个操作通常被定义为 SELECT。
项目 返回在计算的元组的列列表中指定的属性。
加入 返回与称为连接条件(或连接谓词)的特殊条件相匹配的元组。联接有多种形式。请参阅“连接”了解每个连接的描述。

加入

连接操作可以采取多种形式。这些经常被数据库专业人员所混淆,在某些情况下要不惜一切代价避免。SQL 的表达能力允许将许多连接写成 WHERE 子句中的简单表达式。虽然大多数数据库系统能够正确地将这些查询转换成连接,但这被认为是一种懒惰的形式。下面列出了在 RDBMS 中可能遇到的联接类型,并对每种类型进行了描述。连接操作可以有连接条件(theta 连接)、要比较的属性值的匹配(equijoins)或没有条件(笛卡尔乘积)。连接操作细分为:

  • Inner: 两个关系的连接,返回匹配的元组。
  • Outer (left,right,full): 返回 from 子句中提到的至少一个表或视图中的所有行,只要这些行满足任何 WHERE 搜索条件。所有行都是从用左外部联接引用的左表中检索的;右表中的所有行都在右外部联接中被引用。两个表中的所有行都在完全外部联接中返回。不匹配行的属性值作为空值返回。
  • 右外:两个关系的连接,返回匹配的元组,加上右边指定的关系中的所有元组,留下另一个关系中指定的不匹配属性为空(null)。
  • Full outer: 两个关系的连接返回两个关系中的所有元组,将另一个关系中指定的不匹配属性保留为空(null)。
  • 叉积:两个关系的连接,将第一个关系中的每个元组映射到另一个关系中的所有元组。
  • Union: 集合运算,其中只返回来自两个具有相同模式的关系的匹配。
  • Intersect: 集合运算,其中仅返回来自具有相同模式的两个关系的不匹配。

决定如何执行查询(或选择的查询计划)只是事情的一半。另一件要考虑的事情是如何访问数据。有许多方法可以从磁盘(文件)中读取和写入数据,但是选择最佳的方法取决于查询试图做什么。创建文件访问机制是为了最小化从磁盘访问数据的成本,并最大化查询执行的性能。

文件存取

文件访问机制,也称为物理数据库设计,在数据库系统开发的早期就很重要。然而,由于操作系统支持的通用文件系统的有效性和简单性,文件访问的重要性已经降低。今天,文件访问仅仅是文件存储和索引最佳实践的应用,例如将索引文件从数据文件中分离出来,并分别放在单独的磁盘输入/输出(I/O)系统上以提高性能。一些数据库系统使用不同的文件组织技术来使数据库适应特定的应用需求。在这方面,MySQL 可能是最独特的,因为它支持许多文件访问机制(称为存储引擎)。

必须满足明确的目标,以最小化数据库系统中的 I/O 成本。这些包括利用允许通过有效的访问路径仅有效检索相关数据的磁盘数据结构,以及组织磁盘上的数据,使得检索相关数据的 I/O 成本最小化。因此,最重要的性能目标是最小化磁盘访问(或磁盘 I/o)的数量。

有许多处理数据库设计的技术可用。可用于文件访问机制(数据文件的实际物理实现)的更少。此外,许多研究人员同意,最佳的数据库设计(从物理角度来看)通常是不可实现的,而且也不应该追求。优化是不可实现的,主要是因为现代磁盘子系统的效率大大提高了。相反,正是这些技术和研究的知识允许数据库实现者以尽可能好的方式实现数据库系统,以满足那些将使用该系统的人的需求。

要创建一个性能良好的结构,您必须考虑许多因素。早期的研究人员考虑根据数据的内容或上下文将数据分割成子集。例如,包含相同部门编号的所有数据将被分组在一起,并与相关数据的引用一起存储。这个过程可以永久化,因为集合可以被分组在一起以形成超集,从而形成分层的文件组织。

在这种配置中访问数据涉及在最高级别扫描集合,以便只访问和扫描那些对于获得所需信息是必要的集合。这个过程大大减少了要扫描的元素数量。将要扫描的数据项放在一起可以最大限度地减少搜索时间。将磁盘上的数据整理成结构化文件称为文件组织 。我们的目标是设计一种访问方法,它提供了一种逐个立即处理事务的方式,从而允许我们保存真实世界情况的最新存储图片。

随着操作系统的发展,对文件组织技术进行了修订,以确保更高的存储和检索效率。现代数据库系统带来了新的挑战,目前公认的方法可能不足以应对这些挑战。对于在磁盘速度更快、数据吞吐量更高的硬件上执行的系统来说尤其如此。此外,理解数据库设计方法,不仅在教科书中描述,而且在实践中,将增加对数据库系统的要求,从而增加进一步研究的动力。例如,最近工业上对冗余和分布式系统的采用引发了这些领域中的额外研究,以利用新的硬件和/或增加数据可用性、安全性和恢复的需求。

由于从磁盘访问数据的成本很高,因此使用缓存机制,有时也称为缓冲区 ,可以显著提高从磁盘读取数据的性能,从而降低存储和检索数据的成本。这个概念包括复制部分数据,或者是为了下次磁盘读取,或者是基于一种算法,该算法旨在将最常用的数据保留在内存中。有效地处理磁盘和主存之间的差异是高质量数据库系统的核心。应该理解数据库系统使用磁盘还是使用主存之间的权衡。请参见表 2-3 以了解物理存储(磁盘)和辅助存储(内存)之间的性能权衡。

表 2-3 。性能权衡

问题 主内存与磁盘
速度 主存至少比磁盘快 1000 倍。
储存空间 同样的成本,磁盘可以容纳比内存多几百倍的信息。
坚持 当电源关闭时,磁盘保存数据,主内存会忘记一切。
存取时间 主存开始发送数据需要纳秒,而磁盘需要毫秒。
块大小 主存储器可以一次访问一个字,磁盘可以一次访问一个块。

数据库物理存储的进步已经见证了许多关于存储策略和缓冲机制的相同改进,但是在探索性地研究物理存储的基本元素方面几乎没有发生。一些人从硬件层面探讨了这个话题,而另一些人则从更实用的层面探讨了我们到底需要存储什么。持久存储的主题在很大程度上被遗忘了,因为主机操作系统中提供了强大而高效的机制。

文件访问机制用于存储和检索数据库系统包含的数据。大多数文件访问机制都有附加的功能层,允许更快地定位文件中的数据。这些层被称为索引机制 。索引机制提供了访问路径(数据将被搜索和检索的方式),旨在基于称为的数据子部分来定位特定数据。索引机制的复杂程度不一,从简单的关键字列表到旨在最大化关键字搜索的复杂数据结构。

目标是快速有效地找到我们想要的数据,而不必请求和读取非绝对必要的更多磁盘块。这可以通过保存标识数据(或键)的值和记录在磁盘上的位置来实现,以形成数据的索引。此外,读取索引数据比读取所有数据更快。使用索引的主要好处是,它允许我们有效地搜索大量数据,而不必检查,或者在许多情况下阅读每一项,直到找到我们要搜索的内容。因此,索引与搜索包含存储在磁盘上的数据的大文件的方法有关。这些方法被设计用于数据的快速随机存取以及数据的顺序存取。

大多数(但不是全部)索引机制涉及一个存储键和磁盘块地址的树形结构。例子包括 B 树、B +树和散列树。这些结构通常由一个或多个算法遍历,这些算法被设计成最小化在结构中搜索关键字所花费的时间。大多数数据库系统在其索引机制中使用这种或那种形式的 B 树。这些树算法提供了非常快的搜索速度,而不需要很大的存储空间。

在查询执行期间,解释查询执行方法访问分配的索引机制,并通过指定的访问方法请求数据。然后,执行方法读取数据,通常一次读取一条记录;通过评估表达式来分析查询与谓词的匹配;然后通过任何转换传递数据,最后传递到服务器的传输部分,将数据发送回客户端。

查询结果

一旦处理了查询中引用的表中的所有元组,就沿着相同的或者有时是替代的通信路径将元组返回给客户端。然后,元组被传递到 ODBC 连接器,以便封装并呈现给客户端应用。

关系数据库体系结构概述

在这一节中,我详细介绍了通过典型的关系数据库系统架构查询数据的步骤。正如您将看到的,查询从客户端发出的 SQL 命令开始;然后,使用通信路径(网络)通过 ODBC 连接器将其传递给数据库系统。查询被解析、转换成内部结构、优化和执行,结果返回给客户机。

现在,我已经向您简要介绍了处理查询所涉及的所有步骤,并且您已经看到了数据库系统子组件的复杂性,现在是时候来看一个真实的例子了。在下一节中,我将深入介绍 MySQL 数据库系统架构。

MySQL 数据库系统

MySQL 源代码是高度组织化的,使用许多结构化的类构建(有些是复杂的数据结构,有些是对象,但大多数是结构)。虽然通过添加插件使系统更加模块化的努力正在进行中,但源代码还不是真正的模块化架构,但现在随着新的插件机制的出现,已经非常接近了。当您探索架构时,理解这一点很重要,更重要的是,当您以后探索源代码时。这意味着您有时会发现源代码中没有清晰的架构元素划分。关于 MySQL 源代码的更多信息,包括如何获得它,参见第三章。

虽然有些人可能会将 MySQL 架构描述为一个由一组模块化子组件构建的基于组件的系统,但事实是,尽管它高度组织化,但它既不是基于组件的,也不是模块化的。源代码是使用 C 和 C++混合构建的,并且在系统的许多功能中使用了许多对象。该系统不是真正意义上的面向对象编程中的面向对象。相反,该系统是建立在函数库和数据结构的基础上的,这些函数库和数据结构被设计成围绕体系结构优化源代码的组织,其中一些部分是使用对象编写的。

然而,MySQL 架构是高度组织化的子系统的智能设计,这些子系统和谐地工作以形成有效且高度可靠的数据库系统。我在本章前面描述的所有技术都存在于系统中。实现这些技术的子系统设计良好,并使用整个系统中相同精度的源代码来实现。有趣的是,许多有成就的 C 和 C++程序员评论源代码的优雅和简洁。我经常发现自己惊叹于代码的宁静复杂和优雅。事实上,甚至代码作者自己也承认,他们的代码有一种天才的直觉,这种直觉通常只有在彻底分析后才能被完全理解或欣赏。你也会惊奇地发现,一旦你弄明白了,一些源代码是多么的有效,多么的简单。

image 注意事实证明,MySQL 系统对一些人来说很难学习,当出现问题时,诊断起来也很麻烦。然而,很明显,一旦掌握了 MySQL 体系结构和源代码的复杂性,这个系统就非常容易适应,并且有希望成为实验数据库工作的第一个也是最好的平台。

这意味着 MySQL 架构和源代码对于 C++程序员新手来说通常是具有挑战性的。如果你发现自己开始重新考虑接手源代码,请继续阅读;我将是你导航源代码的向导。但是让我们先来看看这个系统是如何构成的。

MySQL 系统架构

MySQL 架构最好被描述为子系统的分层系统。虽然源代码不是作为单独的组件或模块编译的,但是子系统的源代码是以分层的方式组织的,这种方式允许子系统被分离(封装)在源代码中。大多数子系统依赖基础库来实现低级功能(例如,线程控制、内存分配、网络、日志和事件处理以及访问控制)。基本库、基于这些库构建的子系统、甚至是基于其他子系统构建的子系统共同构成了抽象的 API,即 C 客户端 API。这个强大的 API 允许 MySQL 系统在更大的应用中作为独立的服务器或嵌入式数据库系统使用。

该架构为 SQL 接口、查询解析、查询优化和执行、缓存和缓冲以及可插拔存储引擎提供了封装。图 2-4 描绘了 MySQL 架构及其子系统。在图的顶部是提供对客户机应用的访问的数据库连接器。正如您所看到的,几乎任何您想要的编程环境的连接器都存在。在绘图的左侧,辅助工具按管理和企业服务分组列出。关于管理和企业服务工具的完整讨论,请参见迈克尔·克鲁肯伯格和杰伊·皮普斯的Pro MySQL12这是一篇关于 MySQL 所有管理事务的极好参考。

体系结构中从连接器往下的下一层是连接池层。这一层处理客户端连接的所有用户访问、线程处理、内存和进程缓存需求。该层之下是数据库系统的核心。这里是解析和优化查询以及管理文件访问的地方。下一层是可插拔存储引擎层。在这一层,MySQL 体系结构的部分优点大放异彩。可插拔存储引擎层允许构建系统来处理各种不同的数据或文件存储和检索机制。这种灵活性是 MySQL 独有的。目前没有其他数据库系统能够通过提供几种数据存储机制来提供数据库调优能力。

image 注意可插拔存储引擎功能从版本 5.1 开始提供。

可插拔存储引擎下面是系统的最低层,即文件访问层。在这一层,存储机制读写数据,系统读写日志和事件信息。这一层与线程、进程和内存控制最接近操作系统。

让我们从系统中从客户端应用到数据的流程开始讨论 MySQL 架构。第一层遇到了曾经的客户端连接器(ODBC,。NET、JDBC、C API 等。)已经将 SQL 语句传送到服务器的是 SQL 接口。

9781430246596_Fig02-04.jpg

图 2-4。 MySQL 服务器架构(版权 Oracle。经许可转载。)

SQL 接口

SQL 接口提供了接收命令和向用户传输结果的机制。MySQL SQL 接口是根据 ANSI SQL 标准构建的,接受与大多数 ANSI 兼容的数据库服务器相同的基本 SQL 语句。尽管 MySQL 中支持的许多 SQL 命令都有非 ANSI 标准的选项,但 MySQL 开发者已经非常接近 ANSI SQL 标准。

从网络通信路径接收到数据库服务器的连接,并为每个连接创建一个线程。线程化进程是 MySQL 服务器中可执行路径的核心。MySQL 是作为真正的多线程应用构建的,每个线程独立于其他线程执行(除了某些助手线程)。传入的 SQL 命令存储在类结构中,通过将结果写出到网络通信协议,将结果传输到客户端。一旦创建了一个线程,MySQL 服务器就会尝试解析 SQL 命令,并将各个部分存储在内部数据结构中。

解析器

当客户机发出查询时,会创建一个新线程,并将 SQL 语句转发给解析器进行语法验证(或因错误而拒绝)。MySQL 解析器是使用一个用 Bison 编译的大型 Lex-YACC 脚本实现的。解析器构建一个查询结构,用于将内存中的查询语句(SQL)表示为可用于执行查询的树结构(也称为抽象语法树)。

image 提示sql _ yacc . YY、sql_lex.h 和 lex.h 文件是您开始在 MySQL 中构建自己的 SQL 命令或数据类型的地方。这些文件将在第 7 章的中详细讨论。

被许多人认为是 MySQL 源代码中最复杂和最优雅的部分,解析器是使用 lex 和 YACC 实现的,它们最初是为编译器构造而构建的。这些工具用于构建词法分析器,该分析器读取 SQL 语句并将语句分解成多个部分,将命令部分、选项和参数分配给变量和列表结构。这个结构(被形象地命名为 Lex)是 SQL 查询的内部表示。因此,查询过程中的每个其他步骤都会使用该结构。Lex 结构包含正在使用的表的列表、引用的字段名、连接条件、表达式以及存储在单独空间中的查询的所有部分。

解析器的工作方式是读取 SQL 语句,并将表达式(由标记和符号组成)与源代码中定义的规则进行比较。这些规则用 Lex 和 YACC 构建到代码中,然后用 Bison 编译形成词法分析器。如果您检查 C 形式的解析器(一个名为/sql/sql_yacc.cc 的文件),您可能会被 switch 语句的简洁和庞大所淹没。 13 检查解析器的一个更好的方法是在编译之前查看 Lex 和 YACC 格式(一个名为/sql/sql_yacc.yy 的文件)。这个文件包含了为 YACC 写的规则,更容易破译。解析器的构造说明了 Oracle 的开源哲学在起作用:为什么要创建自己的语言处理程序,而 Lex、YACC 和 Bison 等特殊的编译器构造工具就是为做这件事而设计的呢?

一旦解析器识别出一个正则表达式并将查询语句分成几个部分,它就将适当的命令类型分配给线程结构,并将控制返回给命令处理器(它有时被认为是解析器的一部分,但更准确地说是主代码的一部分)。命令处理器被实现为一个大的 switch 语句,支持每个命令的用例。查询解析器只检查 SQL 语句的正确性。它不验证所引用的表或属性(字段)是否存在,也不检查语义错误,例如没有使用 GROUP BY 子句的聚合函数。相反,验证工作留给了优化器。因此,来自解析器的查询结构被传递给查询处理器。从那里,控制切换到查询优化器。

莱克斯和 YACC

Lex 代表“词法分析器生成器”,被用作解析器来识别语言的标记和文字以及语法。YACC 代表“又一个编译器编译器”,用于识别和处理语言的语义定义。这些工具与 Bison(一个 YACC 编译器)一起使用,为创建能够解析和处理语言命令的子系统提供了丰富的机制。事实上,这正是 MySQL 使用这些技术的方式。

查询优化器

一些人认为 MySQL 查询优化器子系统命名不当。所使用的优化器是一个选择-项目-连接策略,它试图通过首先执行任何限制(选择)来缩小要处理的元组的数量,然后执行投影来减少结果元组中的属性(字段)的数量,最后评估任何连接条件来重新构造查询。虽然不被认为是极其复杂的查询优化器类别中的一员,但是 SELECT-PROJECT-JOIN 策略属于启发式优化器类别。在这种情况下,试探法(规则)很简单:

  • 通过计算 WHERE (HAVING)子句中的表达式来消除多余的数据。
  • 通过将数据限制为属性列表中指定的属性来消除额外的数据。例外情况是在连接子句中使用的属性的存储可能不会保留在最终查询中。
  • 评估连接表达式。

这就产生了一种策略,可以确保已知良好的访问方法以高效的方式检索数据。尽管有批评性的评论,SELECT-PROJECT-JOIN 策略在执行事务处理中的典型查询时被证明是有效的。图 2-5 描绘了一个描述 MySQL 查询处理方法的框图。

9781430246596_Fig02-05.jpg

图 2-5。 MySQL 查询处理方法

优化器的第一步是检查表的存在和用户的访问控制。如果有错误,将返回相应的错误消息,并将控制权返回给线程管理器或侦听器。一旦确定了正确的表,就打开它们,并为并发控制应用适当的锁。

一旦所有维护和设置任务完成,优化器就使用内部查询结构(Lex)并评估查询的 WHERE 条件(限制操作)。结果作为临时表返回,为下一步做准备。如果存在 UNION 操作符,优化器会在继续之前执行循环中所有语句的 SELECT 部分。

优化器的下一步是执行预测。它们的执行方式与限制部分类似,同样将中间结果存储为临时表,并且只保存那些在 SELECT 语句的列规范中指定的属性。最后,针对使用 JOIN 类构建的任何 JOIN 条件来分析该结构,然后调用 join::optimize() 方法。在这个阶段,通过评估表达式并消除任何导致死分支或始终为真或始终为假的条件(以及许多其他类似的优化)来优化查询。优化器试图在执行连接之前消除查询中任何已知的不良条件。这样做是因为连接是所有关系运算符中最昂贵和耗时的。请注意,连接优化步骤是为所有具有 WHEREHAVING 子句的查询执行的,不管是否有任何连接条件。这使得开发者能够将所有的表达式求值代码集中在一个地方。一旦连接优化完成,优化器就使用一系列条件语句将查询路由到适当的库方法来执行。

查询优化器和执行引擎可能是第二难理解的领域,因为它的 SELECT-PROJECT-JOIN 优化器方法。使事情变得复杂的是,服务器的这一部分是 C 和 C++代码的混合,其中典型的 select 执行被写成 C 方法,而 join 操作被写成 C++对象。在第 13 章中,我将向你展示如何编写你自己的查询优化器,并使用它来代替 MySQL 优化器。

查询执行

查询的执行由一组旨在实现特定查询的库方法来处理。例如, mysql_insert() 方法就是为了插入数据而设计的。同样,还有一个 mysql_select() 方法,用于查找并返回与 WHERE 子句匹配的数据。这个执行方法库位于多个具有相似名称的文件下的源代码文件中(例如, sql_insert.ccsql_select.cc )。所有这些方法都有一个 thread 对象作为参数,该对象允许方法访问内部查询结构并简化执行。使用网络通信路径库返回每种执行方法的结果。查询执行库方法显然是使用查询执行的解释模型实现的。

查询缓存

虽然不是它自己的子系统,查询缓存应该被认为是查询优化和执行子系统的重要部分。查询缓存是一项了不起的发明,它不仅缓存查询结构,还缓存查询结果本身。这使系统能够检查经常使用的查询,并简化整个查询优化和执行阶段。这是 MySQL 独有的另一项技术。其他数据库系统会缓存查询,但不会缓存实际结果。如您所知,查询缓存还必须考虑到结果“脏”的情况,即自上次运行查询以来发生了一些变化(例如,对基表运行了 INSERTUPDATEDELETE ),缓存的查询可能需要偶尔清除。

image 提示查询缓存默认开启。如果要关闭特定 SQL 语句的查询缓存,使用 SQL_NO_CACHE SELECT 选项: SELECT SQL_NO_CACHE id,lname FROM myCustomer。否则,可以使用服务器变量(query_cache_type、query_cache_size 也释放缓冲区)全局禁用它。

如果您不熟悉这项技术,请尝试一下。找到一个有足够数量元组的表,并执行一个复杂的查询,比如一个连接或复杂的 WHERE 子句。记录执行所用的时间,然后再次执行相同的查询。注意时差。这是运行中的查询缓存。

清单 2-1 展示了使用 SHOW 命令显示与查询缓存相关的系统变量的练习。请注意多次运行查询是如何将查询添加到缓存中的,随后的调用是如何从缓存中读取查询的。还要注意,SQL_NO_CACHE 选项不影响查询缓存变量(因为它不使用查询缓存)。

清单 2-1。MySQL 查询缓存在行动

mysql> CREATE DATABASE test_cache;
Query OK, 1 row affected (0.00 sec)

mysql> CREATE TABLE test_cache.tbl1 (a int);
Query OK, 0 rows affected (0.01 sec)

mysql> INSERT INTO test_cache.tbl1 VALUES (100), (200), (300);
Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0

mysql> USE test_cache;
Database changed

mysql> SELECT * FROM tbl1;
+------+
| a    |
+------+
|  100 |
|  200 |
|  300 |
+------+
3 rows in set (0.00 sec)

mysql> show status like "Qcache_hits";
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Qcache_hits   | 0     |
+---------------+-------+
1 row in set (0.00 sec)

mysql> select length(now()) from tbl1;
+---------------+
| length(now()) |
+---------------+
|            19 |
|            19 |
|            19 |
+---------------+
3 rows in set (0.00 sec)

mysql> show status like "Qcache_hits";
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Qcache_hits   | 0     |
+---------------+-------+
1 row in set (0.01 sec)

mysql> show status like "Qcache_inserts";
+----------------+-------+
| Variable_name  | Value |
+----------------+-------+
| Qcache_inserts | 1     |
+----------------+-------+
1 row in set (0.00 sec)

mysql> show status like "Qcache_queries_in_cache";
+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| Qcache_queries_in_cache | 1     |
+-------------------------+-------+
1 row in set (0.00 sec)

mysql> SELECT * FROM tbl1;
+------+
| a    |
+------+
|  100 |
|  200 |
|  300 |
+------+
3 rows in set (0.00 sec)

mysql> show status like "Qcache_hits";
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Qcache_hits   | 1     |
+---------------+-------+
1 row in set (0.00 sec)

mysql> SELECT SQL_NO_CACHE * FROM tbl1;
+------+
| a    |
+------+
|  100 |
|  200 |
|  300 |
+------+
3 rows in set (0.00 sec)

mysql> show status like "Qcache_hits";
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Qcache_hits   | 1     |
+---------------+-------+
1 row in set (0.00 sec)

mysql> SELECT * FROM tbl1;
+------+
| a    |
+------+
|  100 |
|  200 |
|  300 |
+------+
3 rows in set (0.00 sec)

mysql> show status like "Qcache_hits";
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Qcache_hits   | 2     |
+---------------+-------+
1 r
mysql>

缓存和缓冲区

缓存和缓冲子系统负责确保最常用的数据(或结构,正如您将看到的)以最有效的方式可用。换句话说,数据必须是常驻的,或者随时可以读取。缓存大大增加了对数据请求的响应时间,因为数据在内存中,因此检索数据不需要额外的磁盘访问。创建缓存子系统是为了将所有的缓存和缓冲封装到一组松散耦合的库函数中。尽管您会发现缓存是在几个不同的源代码文件中实现的,但它们被认为是同一个子系统的一部分。

在这个子系统中实现了许多缓存。大多数缓存机制使用相同或相似的概念,将数据存储为链表中的结构。缓存在代码的不同部分中实现,以根据缓存的数据类型定制实现。让我们来看看每个缓存。

表格缓存

创建表缓存是为了最小化打开、读取和关闭表的开销。磁盘上的 FRM 文件)。因此,它被设计为在内存中存储关于表的元数据。这是通过利用一种叫做 Unireg 的数据分块机制来实现的。Unireg 是 MySQL 的创始人创建的一种格式,曾经用于编写 TTY 应用。它将数据存储在称为屏幕的数据段中,这些数据段最初是为在监控器上显示数据而设计的。Unireq 使存储数据和显示(屏幕)更容易,从而加快了数据刷新。您可能已经猜到,这是一项过时的技术,它显示了 MySQL 源代码的时代。好消息是,正在计划重新设计表缓存,并最终替换或删除 Unireg 机制。

关于 FRM 档案的一句话

如果您检查 MySQL 安装的数据目录,您将看到一个名为 data 的文件夹,其中包含为每个创建的数据库命名的子文件夹。在这些文件夹中,您会看到以表格名称命名的文件,文件扩展名为。frm 。许多 MySQL 开发者称这些文件为“FRM 文件”。因此,database1 中名为 table1 的表有一个名为/data/database 1/table 1 . frm的 FRM 文件。

当您试图打开这些文件时,您会看到它们是二进制文件,无法通过正常方式读取。事实上,这些文件的格式多年来一直是个谜。因为 FRM 文件包含表的元数据,所以所有的列定义和表选项(包括索引定义)都存储在该文件中。这意味着应该可以从 FRM 文件中提取重建 CREATE TABLE 语句所需的数据。不幸的是,考虑到 Unireg 的接口和唯一性,要解析这些文件中的信息并不容易。幸运的是,有人正在努力通过 Python 工具解密 FRM 文件,该工具是 MySQL Workbench 的 MySQL 工具插件的一部分。如果您需要读取一个 FRM 文件来恢复一个表,请参阅在线 MySQL 实用程序文档了解更多细节:【http://dev.mysql.com/doc/workbench/en/mysql-utilities.html

这使得线程可以更快地读取表的模式,而不必每次都重新打开文件。每个线程都有自己的表缓存结构列表。这允许线程维护它们自己的表视图,这样,如果一个线程正在改变一个表的模式(但是还没有提交改变),另一个线程可以使用具有原始模式的那个表。使用的结构很简单,包含了一个表的所有元数据信息。这些结构存储在内存中的链表中,并与每个线程相关联。

缓冲池

缓冲池是 InnoDB 存储引擎使用的特殊缓存。它缓存表和索引数据,允许从内存中读取最常用的数据和索引,而不是从磁盘中重新读取。缓冲池显著提高了性能。根据您的访问模式,您可以调整缓冲池来分配更多的物理内存。这是用于调整 InnoDB 存储引擎性能的关键参数之一。

InnoDB 使用其他几种缓存。如果您需要 InnoDB 安装的额外性能,请参考在线参考手册中的“InnoDB 性能调优和故障排除”一节,了解 InnoDB 调优最佳实践。

记录缓存

创建记录缓存是为了增强存储引擎的顺序读取。因此,记录缓存通常只在表扫描期间使用。它的工作方式类似于预读缓冲区,一次检索一个数据块,从而减少扫描期间的磁盘访问。较少的磁盘访问通常等同于性能的提高。有趣的是,记录缓存也用于顺序写入数据,首先将新的(或更改的)数据写入缓存,然后在缓存满时写入磁盘。这样,写入性能也得到提高。这种顺序行为(称为引用局部性)是记录缓存最常与 MyISAM 存储引擎一起使用的主要原因,尽管它不限于 MyISAM。记录缓存以不可知的方式实现,不会干扰用于访问存储引擎 API 的代码。开发者不需要做任何事情来利用记录缓存,因为它是在 API 的层中实现的。

密钥缓存

键缓存是常用索引数据的缓冲区。在这种情况下,它是索引文件(B 树)的一个数据块,专门用于 MyISAM 表。磁盘上的 MYI 文件)。索引本身作为链表存储在键缓存结构中。第一次打开 MyISAM 表时,会创建一个键缓存。每次索引读取时都会访问键缓存。如果在缓存中找到一个索引,就从那里读取它;否则,必须从磁盘中读取新的索引块,并将其放入缓存中。缓存大小有限,可通过更改 key_cache_block_size 配置变量进行调整。因此,并非索引文件的所有块都适合内存。那么,系统如何跟踪哪些块已经被使用了呢?

缓存实现了一个监控系统来跟踪索引块的使用频率。已经实现了键缓存来跟踪索引块有多“热”。在这种情况下,Warm 指的是一段时间内索引块被访问的次数。暖值包括块 _ 冷块 _ 暖块 _ 热。随着块冷却,新块变热,冷块被清除,热块被添加。这种策略是最近最少使用的(LRU)页面替换策略,与操作系统中用于虚拟内存管理和磁盘缓冲的算法相同,即使面对复杂得多的页面替换算法,这种策略也被证明是非常有效的。以类似的方式,键缓存跟踪已经改变的索引块(称为“变脏”)。清除脏块时,其数据会在被替换之前写回磁盘上的索引文件。相反,当清除干净的块时,数据只是从内存中删除。

image 实践表明,LRU 算法的表现在最佳算法的 80%以内。在这个时间宝贵、简单确保可靠的世界里,80%的解决方案是双赢的。

特权缓存

特权缓存用于存储用户帐户上的授权数据。这些数据以与访问控制列表(ACL)相同的方式存储,ACL 列出了用户对系统中某个对象拥有的所有权限。特权高速缓存被实现为存储在先进后出(FILO)哈希表中的结构。在用户身份验证和初始化期间,当读取授权表时,收集高速缓存的数据。将这些数据存储在内存中很重要,因为这样可以节省大量读取授权表的时间。

主机名缓存

主机名缓存是另一个助手缓存,类似于特权缓存。它也是作为结构的堆栈来实现的。它包含到服务器的所有连接的主机名。这似乎令人惊讶,但这种数据经常被请求,因此需求量很大,是专用缓存的候选对象。

多方面的

MySQL 源代码中还实现了许多其他小型缓存机制。一个是连接缓冲区缓存,在复杂的连接操作中使用。例如,一些连接操作需要将一个元组与第二个表中的所有元组进行比较。在这种情况下,缓存可以存储读取的元组,以便可以实现连接,而不必多次将第二个表重新读取到内存中。

通过可插拔存储引擎访问文件

MySQL 最好的特性之一是支持不同存储引擎或文件类型的能力。这允许数据库专业人员通过选择最能满足其应用需求的存储引擎来调整其数据库性能。示例包括使用存储引擎为需要事务处理的高度活跃的数据库提供事务控制,或者每当表被多次读取但很少被更新时使用内存存储引擎(例如,查找表)。

Oracle 在 5.1 版中添加了新的体系结构设计,使添加新的存储类型变得更加容易。这种新机制被称为 MySQL 可插拔存储引擎。Oracle 通过可插拔存储引擎努力使服务器具有可扩展性。可插拔存储引擎是作为文件访问层的抽象而创建的,并作为一个 API 而构建,Oracle(或任何人)可以使用它来构建称为存储引擎的专用文件访问机制。API 提供了一组用于读写数据的方法和访问实用程序。这些方法结合起来形成了一个标准化的模块化体系结构,允许存储引擎对每个存储引擎使用相同的方法(这就是为什么它被称为可插拔的本质,所有存储引擎都使用相同的 API 插入到服务器中)。它还支持使用存储引擎插件。

可插拔 VS 插件

可插拔是指一个公共接口可能有几种不同的实现,允许实现的交换,而系统的其他部分保持不变。插件是模块(编译模块)的二进制形式,它实现了可插入的接口。因此,插件是可以在运行时改变的东西。InnoDB 存储引擎也是一个插件,MySQL 中的几个模块也是如此,它们计划将服务器的其他部分变成插件。

要启用插件存储引擎,使用安装插件命令。例如,要加载示例插件存储引擎,发出以下命令(示例适用于 Linux 操作系统):

mysql >安装插件示例 SONAME ' ha _ example.so

同样,要拔出存储引擎,请使用卸载插件命令:

mysql >卸载插件示例;

或者,您可以使用 mysql_plugin 客户端工具来启用和禁用存储引擎插件。

也许最有趣的是数据库实现者(您!)可以为给定数据库中的每个表分配不同的存储引擎,甚至可以在创建表后更改存储引擎。这种灵活性和模块化允许您根据需要创建新的存储引擎。要更改表的存储引擎,请发出如下命令:

ALTER TABLE MyTable
ENGINE = InnoDB;

可插拔存储引擎可能是 MySQL 最不寻常的特性。对于该体系结构的文件访问层,没有任何其他数据库系统能够达到这种级别的灵活性和可扩展性。以下部分描述了服务器中可用的所有存储引擎,并简要概述了如何创建自己的存储引擎。我将在第 10 章中向你展示如何创建你自己的存储引擎。

存储引擎的优势和劣势多种多样。例如,MySQL 中提供的一些存储引擎支持并发。从版本 5.6 开始,MySQL 的默认存储引擎是 InnoDB。InnoDB 表支持用于并发控制的记录锁定(有时称为行级锁定);当更新正在进行时,在操作完成之前,任何其他进程都不能访问表中的该行。因此,InnoDB 表类型为在预期有许多并发更新的情况下使用提供了优势。但是,这些存储引擎中的任何一个都可以在只读环境中很好地运行。例如 web 服务器或 kiosk 应用。

image 提示您可以通过设置 STORAGE_ENGINE 配置服务器变量来更改默认存储引擎。

MySQL 的早期版本默认使用 MyISAM 存储引擎。MyISAM 支持用于并发控制的表级锁定。也就是说,当更新正在进行时,在操作完成之前,任何其他进程都不能访问同一表中的任何数据。由于使用索引顺序访问方法(ISAM)原则进行了优化,MyISAM 存储引擎也是最快的可用类型。Berkeley Database (BDB) 表支持页级锁定进行并发控制;当更新正在进行时,在操作完成之前,任何其他进程都不能从与被修改的数据相同的页面访问任何数据。

我们讨论过的并发操作是在数据库系统中使用专门的命令实现的,这些命令构成了一个事务子系统。目前,列出的存储引擎中只有三个支持事务:InnoDB 和 NDB。事务提供了一种机制,允许一组操作作为单个原子操作执行。例如,如果为一个银行机构建立一个数据库,将资金从一个账户转移到另一个账户的宏操作将优选地被完整地执行(资金从一个账户转移到另一个账户),而不会中断。事务允许将这些操作封装在一个原子操作中,如果在所有操作完成之前发生错误,该原子操作将取消任何更改,从而避免数据从一个表中删除,并且永远不会进入下一个表。包含在事务命令中的 SQL 语句形式的一组示例操作是:

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

实际上,如果需要更快的访问速度,大多数数据库专业人员会指定 MyISAM 表类型,如果需要事务支持,则指定 InnoDB。幸运的是,MySQL 提供了为数据库中的每个表指定表类型的工具。事实上,数据库中的表不必是同一类型。这种多样的存储引擎允许为各种应用调整数据库。

有趣的是,您可以通过编写自己的表处理程序来扩展这个存储引擎列表。MySQL 提供了示例和代码存根,使系统开发者可以访问这个特性。扩展存储引擎列表的能力使得 MySQL 支持复杂的专有数据格式和访问层成为可能。

innodbt1 版

InnoDB 是一个 Oracle 存储引擎,早于 Oracle 通过收购 Sun MicroSystems 收购 MySQL 。InnoDB 最初是由 inno base(www.innodb.com)授权的第三方存储引擎,并在 GNU 公共许可(GPL)协议下发布。Innobase 被 Oracle 收购,在收购 MySQL 之前,它被授权给 MySQL 使用。因此,现在 Oracle 同时拥有 MySQL 和 InnoDB,它消除了任何许可限制,并使两个产品开发团队能够协调开发。从这种关系中获得的好处最近在 MySQL 的最新版本中得到了显著的性能改进。

当您需要使用事务时,最常使用 InnoDB。InnoDB 支持传统的 ACID 事务(参见附带的侧栏)和外键约束。InnoDB 中的所有索引都是 B 树,索引记录存储在树的叶页面中。InnoDB 通过提供行级锁定改进了 MyISAM 的并发控制。InnoDB 是高可靠性和事务处理环境的首选存储引擎。

酸是什么?

酸代表原子性、一致性、隔离性和持久性。也许是数据库理论中最重要的概念之一,它定义了数据库系统必须表现出的行为,才能被认为是可靠的事务处理。

  • 原子性意味着对于包含多个命令的事务,数据库必须允许在“全有或全无”的基础上修改数据。也就是说,每个事务都是原子的。如果命令失败,则整个事务失败,并且事务中到该点为止的所有更改都将被丢弃。这对于在高事务环境(如金融市场)中运行的系统尤其重要。考虑一下资金转移的后果。通常,借记一个账户和贷记另一个账户需要多个步骤。如果在借记步骤后事务失败,并且没有将钱贷记回第一个帐户,该帐户的所有者将会非常生气。在这种情况下,从借方到贷方的整个事务必须成功,否则都不会成功。
  • 一致性意味着只有有效的数据才会存储在数据库中。也就是说,如果事务中的命令违反了一致性规则之一,则整个事务将被丢弃,数据将返回到事务开始之前的状态。相反,如果事务成功完成,它将以遵守数据库一致性规则的方式更改数据。
  • 隔离意味着同时执行的多个事务不会相互干扰。这是并发性的真正挑战最明显的地方。数据库系统必须处理事务不能违反数据的情况(更改、删除等)。)正在另一个事务中使用。有很多方法可以解决这个问题。大多数系统使用一种叫做锁定的机制,在第一个事务完成之前防止数据被另一个事务使用。尽管隔离属性没有规定先执行哪个事务,但它确实确保了它们不会相互干扰。
  • 持久性意味着事务不会导致数据丢失,也不会丢失事务期间创建或更改的任何数据。耐用性通常由强大的备份和恢复维护功能提供。一些数据库系统使用日志记录来确保任何未提交的数据可以在重启时恢复。

我的天啊

大多数 LAMP 堆栈、数据仓库、电子商务和企业应用都使用 MyISAM 存储引擎。MyISAM 文件是 ISAM 的扩展,通过额外的优化构建,如高级缓存和索引机制。这些表是使用压缩特性和索引优化来提高速度的。此外,MyISAM 存储引擎通过提供表级锁定来支持并发操作。MyISAM 存储机制为各种应用提供可靠的存储,同时提供快速的数据检索。当考虑读取性能时,MyISAM 是首选的存储引擎。

ISAM

ISAM 文件访问方法已经存在很长时间了。ISAM 最初由 IBM 创建,后来用于 System R,IBM 的实验性 RDBMS,被许多人认为是开创性的工作,是今天所有 RDBMS 的祖先。(有些人引用 Ingres 作为原始 RDBMS。)

ISAM 文件通过将数据组织成固定长度属性的元组来存储数据。元组以给定的顺序存储。这样做是为了加快从磁带访问的速度。是的,在过去,这是数据库实现者唯一的存储选择,当然,穿孔卡除外!(通常就是在这一点上,我会通过显示我的年龄来让自己尴尬。如果你也记得打孔卡,那么你和我可能会有一个很少有人会再次经历的经历——扔掉一副没有编号或打印的卡片(在卡片顶部打印数据过去需要很长时间,并且经常被跳过)。

ISAM 文件还有一个外部索引机制,通常实现为包含指针(磁带块数和计数)的哈希表,允许您将磁带快进到所需位置。这允许快速访问存储在磁带上的数据,就像磁带驱动器快进一样快。

虽然 ISAM 机制是为磁带创建的,但它也可以(而且经常)用于磁盘文件系统。ISAM 机制的最大优点是索引通常非常小,并且可以快速搜索,因为可以使用内存中的搜索机制来搜索它。一些较新版本的 ISAM 机制允许创建替代索引,从而允许通过多种搜索机制访问文件(表)。这种外部索引机制已经成为所有现代数据库存储引擎的标准。

MySQL 包含了一个 ISAM 存储引擎(当时称为表类型),但是 ISAM 存储引擎已经被 MyISAM 存储引擎所取代。未来的计划包括用更现代的事务存储引擎替换 MyISAM 存储引擎。

image 注意MySQL 的旧版本支持 ISAM 存储引擎。随着 MyISAM 的推出,Oracle 不再支持 ISAM 存储引擎。

存储器

内存存储引擎(有时称为堆表)是一个内存中的表,它使用哈希机制来快速检索经常使用的数据。因此,这些表比那些从磁盘存储和引用的表要快得多。它们的访问方式与其他存储引擎相同,但是数据存储在内存中,并且仅在 MySQL 会话期间有效。数据在关机(或崩溃)时被刷新和删除。内存存储引擎通常用于静态数据被频繁访问且很少被更改的情况。这种情况的例子包括邮政编码、州、县、类别和其他查找表。堆表也可以用于利用快照技术进行分布式或历史数据访问的数据库中。

image 提示您可以使用- init-file = file 启动选项自动创建基于内存的表。在这种情况下,指定的文件应该包含重新创建表的 SQL 语句。由于表是一次性创建的,所以可以省略 CREATE 语句,因为在系统重新启动时不会删除表定义。

合并

合并存储引擎是使用一组具有相同结构(元组布局或模式)的 MyISAM 表构建的,这些表可以作为单个表引用。因此,表是根据各个表的位置进行分区的,但是没有使用额外的分区机制。所有表必须驻留在同一台机器上(由同一台服务器访问)。使用单一操作或语句访问数据,例如选择更新插入删除。幸运的是,当在合并表上发出 DROP 时,只有合并规范被删除。原始表格没有改变。

这种表类型最大的好处就是速度。可以将一个大表分割成不同磁盘上的几个小表,使用合并表规范将它们组合起来,并同时访问它们。搜索和排序将执行得更快,因为每个表中需要操作的数据更少。例如,如果按谓词划分数据,则可以只搜索包含要搜索的类别的特定部分。同样,对表的修复更有效,因为修复几个较小的单个文件比修复单个大表更快更容易。据推测,大多数错误将局限于一个或两个文件内的区域,因此不需要重建和修复所有数据。不幸的是,这种配置有几个缺点:

  • 您只能使用相同的 MyISAM 表或架构来形成一个合并表。这限制了合并存储引擎在 MyISAM 表中的应用。如果合并存储引擎接受任何存储引擎,合并存储引擎将更加通用。
  • 不允许替换操作。
  • 已经证明索引访问比单个表的效率低。
  • 合并存储机制最适合用在超大型数据库(VLDB)应用中,例如数据驻留在一个或多个数据库的多个表中的数据仓库。

存档

档案存储引擎设计用于以压缩格式存储大量数据。存档存储机制最适合用于存储和检索大量很少访问的存档或历史数据。这种数据包括安全访问数据日志。虽然这不是您想要搜索甚至日常使用的东西,但是如果发生安全事故,关心安全的数据库专业人员会希望拥有它。

没有为归档存储机制提供索引,唯一的访问方法是通过表扫描。因此,归档存储引擎不应用于正常的数据库存储和检索。

联盟

联邦存储引擎被设计为从多个数据库系统创建单个表引用。因此,联邦存储引擎的工作方式类似于合并存储引擎,但是它允许您跨数据库服务器将数据(表)链接在一起。这种机制在目的上类似于其他数据库系统中可用的链接数据表。联邦存储机制最适合在分布式或数据集市环境中使用。

联邦存储引擎最有趣的方面是它不移动数据,也不要求远程表是同一个存储引擎。这说明了可插拔存储引擎层的真正威力。数据在存储和检索过程中被转换。

集群/NDB

集群存储引擎(称为 NDB,以区别于集群产品 14 )被创建来处理 MySQL 的集群服务器功能。当在高可用性和高性能环境中集群多个 MySQL 服务器时,几乎只使用集群存储机制。群集存储引擎不存储任何数据。相反,它将数据的存储和检索委托给集群中数据库使用的存储引擎。它管理跨集群分发数据的控制,从而提供冗余和性能增强。NDB 存储引擎还提供了用于创建可扩展集群解决方案的 API。

CSV〔??〕

CSV 存储引擎被设计为以表格形式创建、读取和写入逗号分隔值(CSV)文件。虽然 CSV 存储引擎不会将数据复制为另一种格式,但图纸布局或元数据会与服务器上指定的文件名一起存储在数据库文件夹中。这允许数据库专业人员快速导出存储在电子表格中的结构化业务数据。CSV 存储引擎不提供任何索引机制。

由于 CSV 存储引擎的简单性,其源代码为想要或需要开发自己的存储引擎的开发者提供了一个极好的起点。您可以在源代码树的/storage/csv/ha_tina.h 和/storage/csv/ha_tina.cc 文件中找到 CSV 存储引擎的源代码。

蒂娜是谁?

关于 CSV 存储引擎的源代码,一个有趣的事实是,它是以原作者的一个朋友的名字命名的,旨在成为一个特殊的,而不是通用的解决方案。幸运的是,存储引擎已被证明对更广泛的受众有用。不幸的是,一旦源文件被引入,就没有任何改变文件名的动机。

黑洞〔??〕

黑洞存储引擎,一个有着惊人效用的有趣特性,被设计成允许系统写数据,但是数据永远不会被保存。如果启用了二进制日志记录,则 SQL 语句将被写入日志。这允许数据库专业人员通过切换表类型来临时禁用数据库中的数据接收。在您想要测试应用以确保它正在写入您不想存储的数据的情况下,这可能会很方便,例如在出于过滤复制的目的而创建中继从属时。

自定义

自定义存储引擎代表您为增强数据库服务器而创建的任何存储引擎。例如,您可能希望创建一个读取 XML 文件的存储引擎。虽然您可以将 XML 文件转换成表格,但是如果您需要访问大量文件,您可能不希望这样做。下面是如何创建这样一个引擎的概述。

如果您考虑使用 XML 存储引擎来读取一组特定的相似 XML 文件,那么您要做的第一件事就是分析 XML 文件的格式或模式,并确定您希望如何解析 XML 文件的自描述性质。假设所有文件都包含相同的基本数据类型,但是具有不同的标签和标签顺序。在这种情况下,您决定使用样式表将文件转换成一致的格式。

一旦决定了格式,就可以开始开发新的存储引擎,方法是检查 MySQL 源代码中包含的示例存储引擎,该示例存储引擎位于名为的文件夹中。主源代码树上的\storage\example 。您会发现一个 makefile 和两个源代码文件( ha_example.h ,ha_example.cc),其中包含一组允许引擎工作的代码,但这些代码并不真正有趣,因为它不做任何事情。但是,您可以阅读程序员留下的注释,这些注释描述了您需要为自己的存储引擎实现的特性。比如打开文件的方法叫做 ha_example::open 。当您检查示例存储引擎文件时,可以在 ha_example.cpp 文件中找到此方法。清单 2-2 显示了一个打开方法的例子。

清单 2-2。 开表法

/**
  @brief
  Used for opening tables. The name will be the name of the file.

  @details
  A table is opened when it needs to be opened; e.g. when a request comes in
  for a SELECT on the table (tables are not open and closed for each request,
  they are cached).

  Called from handler.cc by handler::ha_open(). The server opens all tables by
  calling ha_open() which then calls the handler specific open().

  @see
  handler::ha_open() in handler.cc
*/
int ha_example::open(const char *name, int mode, uint test_if_locked)
{
  DBUG_ENTER("ha_example::open");

  if (!(share = get_share()))
    DBUG_RETURN(1);
  thr_lock_data_init(&share->lock,&lock,NULL);

  DBUG_RETURN(0);
}

image 提示您也可以在 Microsoft Windows 环境中创建存储引擎。在这种情况下,文件位于 Visual Studio 项目中。

清单 2-2 中的例子解释了 ha_example::open 方法是做什么的,并让你知道它是如何被调用的以及预期的返回。虽然源代码现在对你来说可能看起来很陌生,但是越读越清楚,对 MySQL 编码风格越熟悉。

image 注意MySQL 的早期版本(5.1 版之前)允许创建定制的存储引擎,但需要您重新编译服务器可执行文件才能获得更改。借助新的 5.1 版可插拔架构,模块化 API 允许存储引擎具有不同的实现和特性,并允许它们独立于 MySQL 系统代码进行构建。因此,您不需要直接修改 MySQL 源代码。您的新存储引擎项目允许您创建自己的定制引擎,然后编译并将其与现有的运行服务器相链接。

一旦熟悉了示例存储引擎及其工作方式,就可以复制文件并将其重命名为更适合新引擎的名称,然后开始修改文件以从 XML 文件中读取。像所有优秀的程序员一样,你从一次实现一个方法开始,测试你的代码,直到你满意它能正常工作。一旦您拥有了想要的所有功能,并且编译了存储引擎并将其链接到生产服务器,您的新存储引擎就可供任何人使用了。

虽然这听起来像是一个困难的任务,但实际上并不是,这是开始学习 MySQL 源代码的一个好方法。在第 7 章的中,我将返回创建一个定制存储引擎的详细步骤说明。

摘要

在这一章中,我介绍了典型的 RDBMS 的体系结构。虽然这不是一堂完整的数据库理论课,但本章让您了解了关系数据库体系结构的内部,现在您应该对机器内部发生的事情有所了解。我还研究了 MySQL 服务器架构,并解释了构成 MySQL 服务器架构的所有部分在源代码中的位置。

RDBMS 如何工作的知识和 MySQL 服务器体系结构的研究将为您扩展 MySQL 数据库系统的密集旅程做好准备。有了 MySQL 架构的知识,你现在就武装起来了(但不是很危险)。

在下一章中,我将带领您浏览 MySQL 源代码,这将使您能够开始扩展 MySQL 系统以满足您自己的需求。所以卷起你的袖子,带上你的极客; 15 我们要进入源代码了!

1 有一些值得注意的例外,但这是普遍的事实。

2 当在填充的数据存储中修改对象类型时尤其如此。根据所做的更改,对象的行为可能已经改变,因此可能不具有相同的含义。尽管这可能是一种有意的改变,但这种改变的影响可能比典型的关系系统更严重。

3 C. J. Date,《数据库关系模型:回顾与分析》(雷丁,马:Addison-Wesley,2001)。

4 C. J. Date 和 H. Darwen,《未来数据库系统的基础:第三宣言》(雷丁,马:艾迪生-卫斯理,2000 年)。

数据库系统处理空值的一些方式从荒谬到不直观。

6 有时定义为对象数据库连通性或在线数据库连通性,但公认的定义是开放数据库连通性。

7 M. Stonebraker 和 J. L. Hellerstein,数据库系统中的读数,第 3 版。,由迈克尔·斯通布雷克编辑(摩根·考夫曼出版社,1998)。

8 C. J. Date,《数据库关系模型:回顾与分析》(雷丁,马:Addison-Wesley,2001)。

9 R. Elmasri 和 S. B. Navathe,数据库系统基础,第 4 版。(波士顿:艾迪森-韦斯利出版社,2003 年)。

10 A. B .塔克,计算机科学手册,第二版。(佛罗里达州博卡拉顿:CRC 出版社,2004 年)。

11 统计在数据库中的使用源于最早的基于成本的优化器。事实上,商业数据库中的许多实用程序允许数据库专业人员检查和生成这些统计数据,以调整他们的数据库,从而更有效地优化查询。

12 M .克鲁肯伯格和 j .皮波斯。 Pro MySQL 。(加州伯克利:阿普莱斯出版社,2005 年)。

13 克鲁肯伯格和派普斯把这种体验比作心灵的融化。撇开轻浮不谈,对任何不熟悉 YACC 的人来说,这都是一个挑战。

14 关于 NDB API 的更多信息,参见 http://dev.mysql.com/doc/ndbapi/en/overview-ndb-api.html.

众所周知,我们中的许多人在编写代码时,会摆出斜躺在电脑椅上、喝着含咖啡因的现成饮料、听着震耳欲聋的音乐、手按键盘的姿势。

三、MySQL 源代码之旅

本章对 MySQL 源代码进行了完整的介绍,并解释了如何获取和构建该系统。我向您介绍了源代码的机制,以及如何维护代码的编码指南和最佳实践。我将重点放在处理查询的代码部分;这将为第 11 章及以后介绍的主题做好准备。我也给你一个简短的概述,介绍动态加载包含特性的库的插件系统。

入门指南

在这一节中,我将研究修改 MySQL 源代码背后的原则,以及如何获得源代码。让我们先回顾一下可用的许可选项。

了解许可选项

当计划对开源软件进行修改时,要考虑如何使用这些修改。更具体地说,您将如何获取源代码并使用它?根据你对修改的意图,你的选择会和其他人非常不同。您可能希望通过三种主要方式来修改源代码:

  • 深入了解 MySQL 是如何构建的;因此,你是在遵循本书中的例子,还是在进行自己的实验。
  • 为您或您的组织开发一项不会在组织外分发的功能。
  • 构建计划与其他人共享或推广的应用或扩展。

在第一章中,我讨论了在开源许可下修改软件的开源开发者的责任。既然 MySQL 是在 GPLv2 下发布的,也是在商业许可下发布的(一个双许可,我们必须考虑在双许可下源代码的使用。我将从 GPLv2 开始我们的讨论。

根据 GPL,在纯学术会议中修改源代码是允许的,这显然给了你修改和试验源代码的自由。您贡献的价值可能决定您的代码是否在 GPL 下发布。例如,如果您的代码修改被认为是单一的(它们只适用于有限的一组特殊用途的用户),那么该代码可能不包含在源代码库中。类似地,如果你的代码专注于学术实践的探索,那么除了你自己,代码可能对任何人都没有价值。很少有人会将测试源代码中实现的选项和特性的学术练习视为对 MySQL 系统的增值。另一方面,如果你的实验为系统带来了成功和有意义的贡献,大多数人会同意你有义务分享你的发现。出于本书的目的,您将继续修改源代码,就好像您不会共享您的修改一样。虽然我希望你会发现本书中的实验具有启发性和娱乐性,但我不认为在没有进一步开发的情况下,它们会被考虑采用到 MySQL 系统中。如果你采用这些例子,并从中做出一些精彩的东西,我祝福你。一定要告诉所有人你的想法是从哪里来的。

image 警告如果您正在规划一个计划以任何方式与任何人共享的项目,请联系 Oracle 的 MySQL 销售人员,了解您当前的许可以及支持您的目标的许可选项的可用性。

如果您正在修改供您或您的组织使用的 MySQL 源代码,并且您不想共享您的修改,您应该购买适当的 MySQL 商业许可证。MySQL 的商业许可条款允许您进行修改(甚至让 Oracle 来帮助您)并保留这些修改。

类似地,如果你正在修改源代码并打算发布修改,GPL 要求你免费发布修改后的源代码(但你可能会收取媒体费)。在这样做之前,您应该咨询 Oracle。

此外,您的更改不能成为专有的,并且您不能拥有 GPL 下的修改权利。如果您选择不自己发布您的更改,您应该将代码提交给 Oracle 考虑。如果被接受,它将成为甲骨文的财产。另一方面,如果您想对 MySQL 进行专有修改,以便在嵌入式系统或类似安装中使用,请在启动项目之前联系 Oracle 并讨论您的计划。

获取源代码

您可以从 MySQL 开发者网站(http://dev.mysql.com/downloads)下载 MySQL 源代码。在该网站上,您会看到下载所有 MySQL 开源产品的链接。(要使用这本书,您需要 MySQL 社区版。)你还会看到几个下载不同版本服务器的链接,包括:

  • 生产使用的当前版本(也称为普遍可用版本或 GA)
  • 软件的旧版本
  • 每个版本的文档

如果向下滚动,您会看到一个下拉框,允许您选择您的平台。这将下载服务器的二进制版本,包括在您的系统上安装和运行它所需的一切。您还会看到名为“源代码”的条目这是您将用来下载源代码的链接。

您还可以下载新版本服务器的源代码,称为“开发版本”您可以点击选项卡,看到一个类似的列表,用于选择平台或源代码。提醒一下,开发版本是最先进的功能预览,可能包含也可能不包含最终的生产代码,因此,不应考虑在生产环境中使用。出于本书的目的,您可以使用开发发布版本 5.6.5 或更高版本。

要了解本书中的示例,请从网站下载版本 5.6.5 或更高版本。我将在下一节提供安装 MySQL 的说明。该站点包含所有受支持环境的所有二进制文件和源代码。支持许多不同的平台。你会在页面底部找到源代码。为您的平台下载源代码和二进制文件(两次下载)。在本书中,我将使用 Ubuntu 和微软 Windows 7 中的例子。

image 提示如果你用的是 Windows,下载 MSI 安装程序。事实上,可以考虑下载 MySQL Windows installer。它包含了所有的 MySQL 组件,使得在 Windows 上安装 MySQL 成为一个简单快速的过程。在你的 Windows 系统上安装 MySQL 是最好的方法。

旧平台支持

如果您没有看到您选择的二进制发行版中列出您的平台,很可能您的平台太新,不再受支持,或者尚未包括在内。如果出现这种情况,您仍然可以下载源代码并自己构建。

image 除非另有说明,本书中的例子均取自 Linux 源代码发行版(mysql-5.6.5)。虽然 Linux 和 Windows 发行版的大部分代码都是相同的,但我强调了它们之间的差异。最值得注意的是,Windows 平台有一个稍微不同的vio实现。

MySQL 源代码

下载源代码后,将文件解压到系统上的一个文件夹中。如果愿意,您可以将它们解压缩到同一个目录中。当您这样做时,请注意有许多文件夹和许多源文件。您需要引用的主文件夹是/sql文件夹。这包含服务器的主要源文件。表 3-1 列出了最常访问的文件夹及其内容。

表 3-1。 MySQL 源文件夹

文件夹 内容
/BUILD 支持所有平台的编译配置和 make 文件。
/client MySQL 命令行客户端工具。
/cmake CMake 跨平台构建系统的配置文件。
/dbug 调试中使用的实用程序(详见第 5 章。
/include 基本系统包括文件和头文件。
/libmysql 用于 MySQL 客户端应用以及创建嵌入式系统的 C 客户端 API。(更多详情见第 6 章。)
/libmysqld 核心服务器 API 文件。也用于创建嵌入式系统。(更多详情见第 6 章。)
/mysql-test MySQL 系统测试套件。(更多详情请参见第 4 章。)
/mysys 大多数核心操作系统 API 包装器和助手函数。
/plugin 包含所有提供的插件的源代码的文件夹。
/regex 正则表达式库。在查询优化器和执行中用于解析表达式。
/scripts 一组基于 shell 脚本的实用程序。
/sql 主系统代码。你应该从这个文件夹开始你的探索。
/sql-bench 一套基准测试工具。
/storage MySQL 可插拔存储引擎源代码位于该文件夹中。还包括存储引擎示例代码。(详见第 7 章。)
/strings 核心字符串处理包装器。使用这些来满足您所有的字符串处理需求。
/support-files 一组预配置的配置文件,用于使用不同的选项进行编译。
/tests 一组测试程序和测试文件。
/vio 网络和套接字层代码。
/zlib 数据压缩工具。

我建议你现在花些时间仔细阅读一些文件夹,熟悉文件的位置。您会发现许多类型的文件和各种 Perl 脚本分散在文件夹中。虽然没有过分简单化,但 MySQL 源代码在逻辑上是围绕源代码的功能而不是核心子系统组织的。一些子系统,如存储引擎和插件,位于文件夹层次结构中,但大多数位于文件夹结构中的几个位置。对于检查源代码时讨论的每个子系统,我列出了相关的源文件及其位置。

入门指南

理解 MySQL 系统的流程和控制的最佳方式是从典型查询的角度来看源代码。我在第 2 章的中展示了每个主要 MySQL 子系统的高级视图。我现在使用相同的子系统视图,向您展示典型的 SQL 语句是如何执行的。我使用的示例 SQL 语句是:

SELECT lname, fname, DOB FROM Employees WHERE Employees.department = 'EGR'

该查询选择工程部门每个人的姓名和出生日期。虽然不是很有趣,但这个查询在演示 MySQL 系统中的几乎所有子系统时会很有用。让我们从到达服务器进行处理的查询开始。

图 3-1 显示了示例查询通过 MySQL 源代码的路径。我已经抽出了主要的代码行,你应该把它们与第二章中确定的子系统联系起来。我还简化和省略了一些参数列表,使图形更容易阅读。虽然不是特定子系统的一部分,但是mysqld_main()函数负责初始化服务器和设置连接监听器。mysqld_main()函数在文件/sql/mysqld.cc中。

image Windows 系统执行win_main()方法 ,也位于mysqld.cc

9781430246596_Fig03-01.jpg

图 3-1。查询路径概述

查询的路径一旦到达服务器,就从 SQL 接口子系统开始(像大多数 MySQL 子系统一样,SQL 接口函数分布在一组松散关联的源文件上)。在您阅读这一部分和后面的部分时,我会告诉您这些方法在哪个文件中。handle_connections_socket()方法(位于in /sql/mysqld.cc)实现监听器循环,为每个检测到的连接创建一个线程。一旦线程被创建,控制就流向do_handle_one_connection()函数。do_handle_one_connection()功能识别命令,然后将控制传递给do_command开关(位于/sql/sql_parse.cc)。do_command开关将控制路由到适当的网络读取调用,以从连接中读取查询,并通过dispatch_command()函数(位于in /sql/sql_parse.cc)将查询传递给解析器。

查询传递到查询解析器子系统,在那里查询被解析并路由到优化器的正确部分。查询解析器内置了 Lex 和 YACC。Lex 用于标识语言的标记和文字以及语法。YACC 用于构建与 MySQL 源代码交互的代码。它捕获 SQL 命令,将命令的部分存储在内部查询表示中,并将命令路由到名为mysql_execute_command()(有点名不副实)的命令处理器。然后,该方法将查询路由到适当的子功能,在本例中是mysql_select()。这些方法位于/sql/sql_parse.cc/sql/sql_select.cc。这部分代码进入 SELECT-PROJECT-JOIN 查询优化器的 SELECT-PROJECT 部分。

image 提示项目或投影是一个关系数据库术语,描述将结果集限制为 SQL 命令的列列表中定义的那些列的查询操作。例如,SQL 命令SELECT fname, lname FROM employee只将雇员表中的fnamelname列“投影”到结果集中。

此时,查询优化器被调用,通过位于/sql/sql_resolver.cc的函数join->prepare()和位于/sql/sql_optimizer.cc的函数join->optimize()来优化查询的执行。接下来在位于/sql/sql_executor.cc的 join- > exec()中执行查询,控制传递给位于/sql/sql_executor.cc中的较低级别的do_select()函数,该函数执行限制和投影操作。最后,sub _select()函数调用存储引擎读取元组,对其进行处理,并将结果返回给客户端。这些方法位于/sql/sql_executor.cc中。在结果被写入网络后,控制返回到handle_connections_sockets循环(位于in /sql/mysqld.cc)。

image 提示类、结构、类、结构——这都是关于类和结构的!在研究 MySQL 源代码时,请记住这一点。对于服务器中的任何操作,至少有一个类或结构管理数据或驱动执行。学习常用的 MySQL 类和结构是理解源代码的关键,你会在本章后面的“重要的类和结构”中看到。

您可能认为代码并不像您听到的那样糟糕。对于简单的SELECT 语句,比如我正在使用的例子,这在很大程度上是正确的,但是正如你很快会看到的,它可以变得比这更复杂。既然您已经看到了这条路径,并且已经了解了一些主要函数在查询和子系统路径中的位置,那么打开源代码并寻找这些函数。您可以在/sql/mysqld.cc中开始搜索。

好的,这是一个旋风般的介绍,对吗?从这一点开始,我放慢了一点速度(好吧,慢了很多),更详细地浏览源代码。我还在每一节的末尾以表格的形式列出了示例所在的特定源文件。所以系紧安全带,我们要进去了!

我省略了与我们旅行无关的部分。这些可能包括条件编译指令、辅助代码和其他系统级调用。我对缺失的部分做了如下注释:...。我保留了许多原始注释,因为我相信它们将帮助您了解源代码,并让您对开发世界一流的数据库系统有所了解。最后,我用粗体突出显示了代码的重要部分,以便您在阅读时可以更容易地找到它们。

函数的作用是

服务器开始执行的mysqld_main()函数位于/sql/mysqld.cc中。它是服务器可执行文件加载到内存中时调用的第一个函数。这个函数中有数百行代码专门用于特定于操作系统的启动任务,还有大量的系统级初始化代码。清单 3-1 显示了代码的压缩视图,要点用粗体显示。

清单 3-1 。main()函数

int mysqld_main(int argc, char **argv)
{
  ...

  if (init_common_variables())

  ...

  if (init_server_components())

  ...
  /*
   Initialize my_str_malloc() and my_str_free()
  */
  my_str_malloc= &my_str_malloc_mysqld;
  my_str_free= &my_str_free_mysqld;

  ...

  if (mysql_rm_tmp_tables() || acl_init(opt_noacl) ||
      my_tz_init((THD *)0, default_tz_name, opt_bootstrap))

  ...

  create_shutdown_thread();

  ...

  handle_connections_sockets();

  ...

  (void) mysql_mutex_lock(&LOCK_thread_count);

  ...

  (void) mysql_mutex_unlock(&LOCK_thread_count);

  ...
}

第一个有趣的函数是init_common_variables()。这使用命令行参数来控制服务器的运行方式;它是服务器解释参数并以各种模式启动服务器的地方。该函数负责设置系统变量,并将服务器置于所需的模式。init-server-components()函数初始化数据库日志,供任何子系统使用。这些是您看到的事件、语句执行等的典型日志。

两个最重要的my_库函数是my_str_malloc()my_str_free()。这两个函数指针就是在服务器启动代码中的这一点(靠近开头)被设置的。您应该总是使用这些函数来代替传统的 C/C++ malloc()函数,因为 MySQL 函数有额外的错误处理,因此比基本方法更安全。acl_init()函数的工作是启动认证和访问控制子系统。这个密钥系统出现在服务器启动代码的早期。

现在你开始了解 MySQL 的成功之处:线程。创建了两个重要的助手线程。函数创建一个线程,它的任务是在收到信号时关闭服务器。我将在“进程与线程”侧栏中更详细地讨论线程。

在启动代码的这一点上,系统已经准备好接受来自客户端的连接。为了做到这一点,handle-connections-sockets()函数实现了一个监听器,它循环遍历等待连接的代码。接下来我将更详细地讨论这个函数。

我想在代码中指出的最后一点是多线程期间互斥访问的临界区保护代码的一个例子。临界区是必须作为一个集合执行的代码块,一次只能由一个线程访问。临界区通常是写入共享内存变量的区域,因此它们必须在另一个线程试图读取内存之前完成。Oracle 创建了一种常见并发保护机制的抽象,称为互斥(互斥的缩写)。如果您在代码中找到一个需要在并发执行期间保护的区域,请使用以下函数来保护代码。

你应该调用的第一个函数是mysql_mutex_lock([resource reference]。这会在代码的这一点锁定代码的执行。它不允许另一个线程访问指定的内存位置,直到您的代码调用解锁函数mysql_mutex_unlock([resource reference])。在来自mysqld_main()函数的例子中,互斥调用锁定了线程计数全局变量。

这是你第一次在引擎盖下潜水。感觉如何?你想要更多吗?继续读——你才刚刚开始。事实上,您还没有看到我们的示例查询是从哪里进入系统的。让我们接下来做那件事。

进程与线程

术语进程线程经常互换使用。这是不正确的,因为进程是一组有组织的计算机指令,它有自己的内存和执行路径。一个线程也是一组计算机指令,但是线程在一个主机的执行路径中执行,没有自己的内存。(有些人称线程为轻量级进程。虽然这是一个很好的描述,但对它们的称呼无助于区分。)它们存储状态(在 MySQL 中,是通过THD类)。因此,当谈到支持进程的大型系统时,我指的是允许系统的各个部分作为独立的进程执行并拥有自己的内存的系统。当谈到支持线程的大型系统时,我指的是允许系统的某些部分与系统的其他部分并发执行的系统,它们都与主机共享相同的内存空间。

大多数数据库系统使用进程模型来管理并发连接和助手功能。MySQL 使用多线程模型。与进程相比,使用线程有很多优点。最值得注意的是,线程更容易创建和管理(没有内存分配和隔离的开销)。线程也允许非常快速的切换,因为没有上下文切换发生。然而,线程确实有一个严重的缺点。如果事情变得不稳定(这是一个高度技术性的术语,用来描述奇怪的、无法解释的行为;在线程的情况下,它们通常是非常奇怪和有害的事件),如果问题很严重,很可能会影响整个系统。幸运的是,Oracle 和全球开发者社区已经非常努力地让 MySQL 的线程子系统变得健壮和可靠。这就是为什么你的修改必须是线程安全的。

处理连接并创建线程

在上一节中,您看到了系统是如何启动的,以及控制是如何流向等待用户连接的侦听器循环的。连接始于客户端,并被分解成数据包,由客户端软件放在网络上,然后流经网络通信路径,由服务器的网络子系统接收,并在服务器上重新形成数据。(关于通信包的完整描述可以在 MySQL 内部手册中找到。)这个流程可以在图 3-2 中看到。我将在下一章展示更多关于网络通信方法的细节。我还提供了一些例子,说明如何编写代码,使用这些函数向客户机返回结果。

9781430246596_Fig03-02.jpg

图 3-2。从客户端到服务器的网络通信

此时,系统处于 SQL 接口子系统中。也就是说,数据包(包含查询)已经到达服务器,并通过handle_connections_sockets()函数被检测到。该函数进入一个循环,等待变量abort_loop被设置为TRUE表 3-2 显示了管理连接和线程的文件位置。

表 3-2。连接和线程管理

源文件 描述
/sql/net_serv.cc 包含所有网络通信功能。有关如何通过网络与客户端或服务器通信的信息,请查看此处。
/include/mysql_com.h 包含通信中使用的大多数结构。
/sql/sql_parse.cc 包含除词法分析器之外的大多数查询路由和分析功能。
/sql/mysqld.cc 除了 mysqld_main 和服务器启动函数之外,这个文件还包含创建线程的方法。

清单 3-2 提供了连接处理代码的浓缩视图。当检测到一个连接时(我已经隐藏了这部分代码,因为它对了解系统如何工作没有帮助),函数创建一个新的线程,调用恰当命名的create_new_thread()函数。正是在这个功能中,第一个主要结构被创建。THD类负责维护线程的所有信息。虽然没有在私有内存空间中分配给线程,但是THD类允许系统在执行过程中控制线程。我将在后面的部分公开一些THD类。

清单 3-2 句柄-连接-套接字功能

void handle_connections_sockets()
{

  ...

  DBUG_PRINT("general",("Waiting for connections."));

  ...

  while (!abort_loop)
  {

  ...

    /*
    ** Don't allow too many connections
    */

    if (!(thd= new THD))

  ...

    create_new_thread(thd);
  }

  ...
}

好了,客户端已经连接到服务器了。接下来会发生什么?让我们看看create_new_thread()函数内部发生了什么。清单 3-3 显示了这个函数的一个浓缩视图。首先看到的是锁定线程数的互斥调用。正如您在mysqld_main()函数中看到的,这对于防止其他线程竞争对变量的写访问是必要的。创建线程时,会调用相关的解锁互斥体来解锁资源。

清单 3-3T5【create _ new _ thread()函数

static void create_new_thread(THD *thd)
{

...

  /*
    Don't allow too many connections. We roughly check here that we allow
    only (max_connections + 1) connections.
  */
  mysql_mutex_lock(&LOCK_connection_count);
  if (connection_count >= max_connections + 1 || abort_loop)
  {
    mysql_mutex_unlock(&LOCK_connection_count);
...
    close_connection(thd, ER_CON_COUNT_ERROR);
    delete thd;
...
  }

  ++connection_count;

  if (connection_count > max_used_connections)
    max_used_connections= connection_count;
  mysql_mutex_unlock(&LOCK_connection_count);
  /* Start a new thread to handle connection. */
  mysql_mutex_lock(&LOCK_thread_count);
...

  thd->thread_id= thd->variables.pseudo_thread_id= thread_id++;
  MYSQL_CALLBACK(thread_scheduler, add_connection, (thd));
...

}

一件非常有趣的事情发生在函数的早期。注意MYSQL_CALLBACK()宏。该宏旨在重用驻留在连接池中的线程。这有助于加快速度,因为创建线程虽然比创建进程快,但会花费一些时间。让线程准备就绪是一种连接缓存机制。为以后使用而保存的线程被称为连接池

如果没有可供重用的连接(线程),系统会通过调用pthread_create()函数创建一个。这里发生了非常奇怪的事情。注意这个函数调用的第三个参数。看似变量的东西实际上是函数的起始地址(函数指针)。pthread_create()使用这个函数指针来关联服务器中线程开始执行的位置。

既然查询已经从客户机发送到服务器,并且已经创建了一个线程来管理执行,那么控制就传递给 do_ handle_one_connection()函数。清单 3-4 显示了 do_ handle_one_connection()函数的浓缩视图。在这个视图中,我注释掉了处理初始化THD类以供使用的一大段代码。如果您感兴趣,稍后可以仔细看看代码(位于/sql/sql_connect.cc)。现在,让我们看看这个函数内部的基本工作。

清单 3-4do _ handle _ one _ connection()函数

void do_handle_one_connection(THD *thd_arg)
{
  THD *thd= thd_arg;

...

    while (thd_is_connection_alive(thd))
    {
      mysql_audit_release(thd);
      if (do_command(thd))
        break;
    }
    end_connection(thd);

...

}

在这种情况下,我们研究的唯一感兴趣的函数调用是do_command(thd)函数。它位于一个循环中,对于从网络通信代码中读取的每个命令,该循环都循环一次。虽然在这一点上有点神秘,但这是我们这些输入过堆叠 SQL 命令(同一行中有多个命令)的人感兴趣的。正如您在这里看到的,这是 MySQL 处理这种可能性的地方。对于每个命令读取,该函数将控制传递给开始从网络读取查询的函数。

此时,系统从网络中读取查询,并将其放入THD类进行解析。这发生在do_command()函数中。清单 3-5 显示了do_command()函数的浓缩视图。我留下了一些更有趣的注释和代码来演示 MySQL 源代码的健壮性。

清单 3-5。do _ command()函数

bool do_command(THD *thd)
{
  bool return_value;
  char *packet = 0;
  ulong packet_length;
  NET *net= &thd->net;
  enum enum_server_command command;

...

  net_new_transaction(net);

...

  packet_length= my_net_read(net);

...

  if (packet_length == packet_error)
  {
    DBUG_PRINT("info",("Got error %d reading command from socket %s",
         net->error,
         vio_description(net->vio)));

...

  command= (enum enum_server_command) (uchar) packet[0];

  if (command >= COM_END)
    command= COM_END; // Wrong command

  DBUG_PRINT("info",("Command on %s = %d (%s)",
                     vio_description(net->vio), command,
                     command_name[command].str));

  ...

  my_net_set_read_timeout(net, thd->variables.net_read_timeout);

  DBUG_ASSERT(packet_length);

  return_value= dispatch_command(command, thd, packet+1, 
                                    (uint) (packet_length-1));

...

}

首先要注意的是一个包缓冲区和一个NET结构的创建。这个包缓冲区是一个字符数组,存储从网络上读取的原始查询字符串,并存储在NET结构中。下一个创建的项目是一个命令结构,它将用于将控制传递给适当的解析器函数。my_net_read()功能从网络中读取数据包,并将它们存储在NET结构中。数据包的长度也存储在NET结构的packet_length变量中。您在这个函数中看到的最后一件事是对dispatch_command()的调用,从这里您可以开始看到命令是如何通过服务器代码路由的。

好吧,你开始有所进展了。dispatch_command()功能的工作是将控制路由到服务器中能够最好地处理输入命令的部分。由于您正在进行一个普通的SELECT查询,系统通过将command变量设置为COM_QUERY.将它识别为一个查询,其他命令类型用于识别语句、更改用户、生成统计数据和许多其他服务器功能。对于这一章,我将只看查询命令(COM_QUERY)。清单 3-6 显示了该函数的一个浓缩视图。为了简洁起见,我省略了 switch 中所有其他命令的代码(我也省略了注释分隔符),但是我保留了大多数命令的 case 语句。花点时间浏览列表。大多数名字都是不言自明的。如果您要对另一种类型的查询进行这种探索,您可以通过在这个函数中查找所标识的类型并按照 case 语句中的代码来找到自己的方法。我还包括了出现在函数代码前的大函数注释块。花点时间看看这个。在这一章的后面,我会更深入地探讨这一点。

清单 3-6。dispatch _ command()函数

/**
  Perform one connection-level (COM_XXXX) command.

  @param command         type of command to perform
  @param thd             connection handle
  @param packet          data for the command, packet is always null-terminated
  @param packet_length   length of packet + 1 (to show that data is
                         null-terminated) except for COM_SLEEP, where it
                         can be zero.

...

  @retval
    0   ok
  @retval
    1   request of thread shutdown, i. e. if command is
        COM_QUIT/COM_SHUTDOWN
*/
bool dispatch_command(enum enum_server_command command, THD *thd,
        char* packet, uint packet_length)
{

  ...
  switch (command) {
    case COM_INIT_DB:
    ...
    case COM_REGISTER_SLAVE:
    ...
    case COM_TABLE_DUMP:
    ...
    case COM_CHANGE_USER:
    ...
    case COM_STMT_EXECUTE:
    ...
    case COM_STMT_FETCH:
    ...
    case COM_STMT_SEND_LONG_DATA:
    ...
    case COM_STMT_PREPARE:
    ...
    case COM_STMT_CLOSE:
    ...
    case COM_STMT_RESET:
    ...
    case COM_QUERY:
    {
      if (alloc_query(thd, packet, packet_length))
        break;          // fatal error is set

    ...

      if (opt_log_raw)
        general_log_write(thd, command, thd->query(), thd->query_length());

    ...

      mysql_parse(thd, thd->query(), thd->query_length(), &parser_state);

    ...
    }
    case COM_FIELD_LIST:        // This isn't actually needed
    ...
    case COM_QUIT:
    ...
    case COM_BINLOG_DUMP_GTID;
    ...
    case COM_BINLOG_DUMP:
    ...
    case COM_REFRESH:
    ...
    case COM_SHUTDOWN:
    ...
    case COM_STATISTICS:
    ...
    case COM_PING:
    ...
    case COM_PROCESS_INFO:
    ...
    case COM_PROCESS_KILL:
    ...
    case COM_SET_OPTION:
    ...
    case COM_DEBUG:
    ...
    case COM_SLEEP:
    ...
    case COM_DELAYED_INSERT:
    ...
    case COM_CONNECT;
    case COM_TIME;
    ...
    case COM_END:
    ...
    default:
    ...
}

当控制权传递给COM_QUERY处理程序时发生的第一件事是,通过alloc _query()函数将查询从packet数组复制到thd->query成员变量。通过这种方式,线程现在拥有了查询的副本,该副本将在整个执行过程中一直伴随着它。还要注意,代码将命令写入常规日志。这将有助于稍后调试系统问题和查询问题。清单 3-6 中感兴趣的最后一个函数调用是mysql_parse()函数调用。至此,代码可以正式从 SQL 接口子系统转移到查询解析器子系统。如您所见,这种区别是语义上的,而不是语法上的。

解析查询

最后,解析开始。这是服务器处理查询时内部运行的核心。解析器代码和系统的其他部分一样,位于几个地方。如果你意识到虽然它是高度组织的,但是代码的结构与架构不匹配,那么这就不难理解了。

您现在正在检查的函数是mysql_parse()函数(位于/sql/sql_parse.cc)。它的工作是在查询缓存中检查先前执行的具有相同结果集的查询的结果,然后将控制传递给词法分析器(parse_sql()),最后将命令传递给查询优化器。清单 3-7 显示了mysql_parse()函数的浓缩视图。

清单 3-7MySQL _ parse()函数

/**
  Parse a query.

  @param       thd     Current thread
  @param       rawbuf  Begining of the query text
  @param       length  Length of the query text
  @param[out]  found_semicolon For multi queries, position of the character of
                               the next query in the query text.
*/

void mysql_parse(THD *thd, char *rawbuf, uint length,
                 Parser_state *parser_state)
{
  int error __attribute__((unused));

  ...

  if (query_cache_send_result_to_client(thd, rawbuf, length) <= 0)
  {
    LEX *lex= thd->lex;

  ...

    bool err= parse_sql(thd, parser_state, NULL);

  ...

      error= mysql_execute_command(thd);

  ...

    }

  ...

  }
  else
  {
    /*
      Query cache hit. We need to write the general log here.
      Right now, we only cache SELECT results; if the cache ever
      becomes more generic, we should also cache the rewritten
      query string together with the original query string (which
      we'd still use for the matching) when we first execute the
      query, and then use the obfuscated query string for logging
      here when the query is given again.
    */
    thd->m_statement_psi= MYSQL_REFINE_STATEMENT(thd->m_statement_psi,
                                                sql_statement_info[SQLCOM_SELECT].m_key);
    if (!opt_log_raw)
      general_log_write(thd, COM_QUERY, thd->query(), thd->query_length());
    parser_state->m_lip.found_semicolon= NULL;
  }
  ...
}

首先要注意的是检查查询缓存的调用。查询缓存存储所有最频繁请求的查询,包括结果。如果查询已经在查询缓存中,我们跳到 else,这样就完成了!剩下的工作就是将结果返回给客户机。不需要解析、优化甚至执行。这有多酷?

为了便于研究,我们假设查询缓存不包含示例查询的副本。在这种情况下,该函数创建一个新的LEX结构来包含查询的内部表示。这个结构由 Lex/YACC 解析器填充,如清单 3-8 所示。这段代码在 sql/sql_yacc.yy 中。

清单 3-8 选择 Lex/YACC 解析代码摘录

/*
  Select : retrieve data from table
*/

select:
          select_init
          {
            LEX *lex= Lex;
            lex->sql_command= SQLCOM_SELECT;
          }
        ;

/* Need select_init2 for subselects. */
select_init:
          SELECT_SYM select_init2
        | '(' select_paren ')' union_opt
        ;

select_paren:
          SELECT_SYM select_part2
          {
            if (setup_select_in_parentheses(Lex))
              MYSQL_YYABORT;
          }
        | '(' select_paren ')'
        ;

/* The equivalent of select_paren for nested queries. */
select_paren_derived:
          SELECT_SYM select_part2_derived
          {
            if (setup_select_in_parentheses(Lex))
              MYSQL_YYABORT;
          }
        | '(' select_paren_derived ')'
        ;

select_init2:
          select_part2
          {
            LEX *lex= Lex;
            SELECT_LEX * sel= lex->current_select;
            if (lex->current_select->set_braces(0))
            {
              my_parse_error(ER(ER_SYNTAX_ERROR));
              MYSQL_YYABORT;
            }
            if (sel->linkage == UNION_TYPE &&
                sel->master_unit()->first_select()->braces)
            {
              my_parse_error(ER(ER_SYNTAX_ERROR));
              MYSQL_YYABORT;
            }
          }
          union_clause
        ;

select_part2:
          {
            LEX *lex= Lex;
            SELECT_LEX *sel= lex->current_select;
            if (sel->linkage != UNION_TYPE)
              mysql_init_select(lex);
            lex->current_select->parsing_place= SELECT_LIST;
          }
          select_options select_item_list
          {
            Select->parsing_place= NO_MATTER;
          }
          select_into select_lock_type
        ;

select_into:
          opt_order_clause opt_limit_clause {}
        | into
        | select_from
        | into select_from
        | select_from into
        ;

select_from:
          FROM join_table_list where_clause group_clause having_clause
          opt_order_clause opt_limit_clause procedure_analyse_clause
          {
            Select->context.table_list=
              Select->context.first_name_resolution_table=
                Select->table_list.first;
          }
        | FROM DUAL_SYM where_clause opt_limit_clause
          /* oracle compatibility: oracle always requires FROM clause,
             and DUAL is system table without fields.
             Is "SELECT 1 FROM DUAL" any better than "SELECT 1" ?
          Hmmm :) */
        ;

select_options:
          /* empty*/
        | select_option_list
          {
            if (Select->options & SELECT_DISTINCT && Select->options & SELECT_ALL)
            {
              my_error(ER_WRONG_USAGE, MYF(0), "ALL", "DISTINCT");
              MYSQL_YYABORT;
            }
          }
        ;

select_option_list:
          select_option_list select_option
        | select_option
        ;

select_option:
          query_expression_option
        | SQL_NO_CACHE_SYM
          {
            /* 
              Allow this flag only on the first top-level SELECT statement, if
              SQL_CACHE wasn't specified, and only once per query.
             */
            if (Lex->current_select != &Lex->select_lex)
            {
              my_error(ER_CANT_USE_OPTION_HERE, MYF(0), "SQL_NO_CACHE");
              MYSQL_YYABORT;
            }
            else if (Lex->select_lex.sql_cache == SELECT_LEX::SQL_CACHE)
            {
              my_error(ER_WRONG_USAGE, MYF(0), "SQL_CACHE", "SQL_NO_CACHE");
              MYSQL_YYABORT;
            }
            else if (Lex->select_lex.sql_cache == SELECT_LEX::SQL_NO_CACHE)
            {
              my_error(ER_DUP_ARGUMENT, MYF(0), "SQL_NO_CACHE");
              MYSQL_YYABORT;
            }
            else
            {
              Lex->safe_to_cache_query=0;
              Lex->select_lex.options&= ∼OPTION_TO_QUERY_CACHE;
              Lex->select_lex.sql_cache= SELECT_LEX::SQL_NO_CACHE;
            }
          }
        | SQL_CACHE_SYM
          {
            /* 
              Allow this flag only on the first top-level SELECT statement, if
              SQL_NO_CACHE wasn't specified, and only once per query.
             */
            if (Lex->current_select != &Lex->select_lex)
            {
              my_error(ER_CANT_USE_OPTION_HERE, MYF(0), "SQL_CACHE");
              MYSQL_YYABORT;
            } 
            else if (Lex->select_lex.sql_cache == SELECT_LEX::SQL_NO_CACHE)
            {
              my_error(ER_WRONG_USAGE, MYF(0), "SQL_NO_CACHE", "SQL_CACHE");
              MYSQL_YYABORT;
            }
            else if (Lex->select_lex.sql_cache == SELECT_LEX::SQL_CACHE)
            {
              my_error(ER_DUP_ARGUMENT, MYF(0), "SQL_CACHE");
              MYSQL_YYABORT;
            }
            else
            {
              Lex->safe_to_cache_query=1;
              Lex->select_lex.options|= OPTION_TO_QUERY_CACHE;
              Lex->select_lex.sql_cache= SELECT_LEX::SQL_CACHE;
            }
          }
        ;

select_lock_type:
          /* empty */
        | FOR_SYM UPDATE_SYM
          {
            LEX *lex=Lex;
            lex->current_select->set_lock_for_tables(TL_WRITE);
            lex->safe_to_cache_query=0;
          }
        | LOCK_SYM IN_SYM SHARE_SYM MODE_SYM
          {
            LEX *lex=Lex;
            lex->current_select->
              set_lock_for_tables(TL_READ_WITH_SHARED_LOCKS);
            lex->safe_to_cache_query=0;
          }
        ;

select_item_list:
          select_item_list ',' select_item
        | select_item
        | '*'
          {
            THD *thd= YYTHD;
            Item *item= new (thd->mem_root)
                          Item_field(&thd->lex->current_select->context,
                                     NULL, NULL, "*");
            if (item == NULL)
              MYSQL_YYABORT;
            if (add_item_to_list(thd, item))
              MYSQL_YYABORT;
            (thd->lex->current_select->with_wild)++;
          }
        ;

select_item:
          remember_name table_wild remember_end
          {
            THD *thd= YYTHD;

            if (add_item_to_list(thd, $2))
              MYSQL_YYABORT;
          }
        | remember_name expr remember_end select_alias
          {
            THD *thd= YYTHD;
            DBUG_ASSERT($1 < $3);

            if (add_item_to_list(thd, $2))
              MYSQL_YYABORT;
            if ($4.str)
            {
              if (Lex->sql_command == SQLCOM_CREATE_VIEW &&
                  check_column_name($4.str))
              {
                my_error(ER_WRONG_COLUMN_NAME, MYF(0), $4.str);
                MYSQL_YYABORT;
              }
              $2->item_name.copy($4.str, $4.length, system_charset_info, false);
            }
            else if (!$2->item_name.is_set())
            {
              $2->item_name.copy($1, (uint) ($3 - $1), thd->charset());
            }
          }
        ;

我包含了来自 Lex/YACC 解析器的摘录,展示了如何识别SELECT标记并通过 YACC 代码进行解析。要阅读这段代码(以防你不知道莱克斯或 YACC),注意代码中的关键词(或标记)(它们位于冒号的左下方,如select:)。这些关键字用于指导解析器的流程。这些关键字右侧的标记定义了解析查询时必须出现的顺序。例如,看看select:关键词。在它的右边,你会看到一个select_init2关键字,它并没有提供太多的信息。然而,如果你向下看代码,你会在左边看到select_init:关键字。这允许 Lex/YACC 作者以一种类似宏的形式指定某些行为。另外,请注意在select_init关键字下有花括号。这是解析器将查询分成几部分并将条目放入LEX结构的地方。直接符号,如SELECT,在头文件(/sql/lex.h)中定义,在解析器中显示为SELECT_SYM。现在花点时间浏览一下代码。你可能需要反复练习几次。如果你没有学习过编译器构造或文本解析,这可能会令人困惑。

如果你在想,“真是个怪物”,那么你可以放心,你是正常的。莱克斯/YACC 代码对大多数开发者来说都是一个挑战。我强调了一些重要的代码语句,它们应该有助于解释代码是如何工作的。让我们过一遍。为了方便起见,我在这里再次重复了示例SELECT语句:

SELECT lname, fname, DOB FROM Employees WHERE Employees.department = 'EGR'

再看第一个关键词。注意select_init代码块如何将LEX结构的sql_command设置为SQLCOM_SELECT。这很重要,因为查询路径中的下一个函数在一个大型 switch 语句中使用它来进一步控制通过服务器的查询流。示例SELECT语句在字段列表中有三个字段。让我们试着在解析器代码中找到它们。寻找add_item_to_list()函数调用。这就是解析器检测字段并将它们放入LEX结构的地方。您还会看到调用解析器代码的几行代码,这些代码标识了字段列表的*选项。现在您已经得到了sql_command成员变量集和标识的字段。那么,FROM子句在哪里被检测到呢?寻找以FROM join_table_list where_clause开头的代码语句。这段代码是解析器的一部分,用于识别FROMWHERE子句(以及其他)。处理这些子句的解析器的代码没有包含在清单 3-8 中,但是我想你已经明白了。如果您打开sql_yacc.yy源文件(位于/sql),您现在应该能够找到所有这些语句,并看到LEX结构的其余部分是如何用FROM子句中的表列表和WHERE子句中的表达式填充的。

我希望这次解析器代码之旅有助于减轻通常伴随着检查 MySQL 系统这一部分的震惊和恐惧。稍后当我演示如何添加你自己的命令 MySQL SQL lexicon 时,我将回到系统的这一部分(更多细节见第 8 章)。表 3-3 列出了与 MySQL 解析器相关的源文件。

表 3-3。MySQL 解析器

源文件 描述
/sql/lex.h 解析器支持的所有关键字和标记的符号表
/sql/lex_symbol.h 符号表的类型定义
/sql/sql_lex.h 法律结构的定义
/sql/sql_lex.cc Lex 类的定义
/sql/sql_yacc.yy 莱克斯/YACC 解析器代码
/sql/sql_parse.cc 包含除词法分析器之外的大多数查询路由和分析功能

image 注意不要编辑文件sql_yacc.ccsql_yacc.hlex_hash.h。这些文件由其他实用程序生成。详见第 7 章。

为优化准备查询

尽管从 MySQL 文档来看,解析器结束和优化器开始的界限并不清楚(存在矛盾),但是从优化器的定义来看,源代码的路由和控制部分可以被视为优化器的一部分。为了避免混淆,我将把下一组函数称为优化器的准备阶段。

这些准备功能中的第一个是mysql_execute_command()功能(位于/sql/sql_parse.cc)。这个名称使您相信您实际上正在执行查询,但事实并非如此。该函数执行优化查询所需的许多设置步骤。复制了LEX结构,并设置了几个变量来帮助查询优化和后期执行。你可以在清单 3-9 中的函数的浓缩视图中看到这些操作。

清单 3-9 。函数的作用是

/**
  Execute command saved in thd and lex->sql_command.

  @param thd                       Thread handle

...

  @retval
    FALSE       OK
  @retval
    TRUE        Error
*/

int
mysql_execute_command(THD *thd)
{
  int res= FALSE;
  int  up_result= 0;
  LEX  *lex= thd->lex;
  /* first SELECT_LEX (have special meaning for many of non-SELECTcommands) */
  SELECT_LEX *select_lex= &lex->select_lex;
  /* first table of first SELECT_LEX */
  TABLE_LIST *first_table= select_lex->table_list.first;
  /* list of all tables in query */
  TABLE_LIST *all_tables;
  /* most outer SELECT_LEX_UNIT of query */
  SELECT_LEX_UNIT *unit= &lex->unit;
#ifdef HAVE_REPLICATION
  /* have table map for update for multi-update statement (BUG#37051) */
  bool have_table_map_for_update= FALSE;
#endif
  DBUG_ENTER("mysql_execute_command");

  ...

switch (lex->sql_command) {

...

  case SQLCOM_SHOW_STATUS_PROC:
  case SQLCOM_SHOW_STATUS_FUNC:
  case SQLCOM_SHOW_DATABASES:
  case SQLCOM_SHOW_TABLES:
  case SQLCOM_SHOW_TRIGGERS:
  case SQLCOM_SHOW_TABLE_STATUS:
  case SQLCOM_SHOW_OPEN_TABLES:
  case SQLCOM_SHOW_PLUGINS:
  case SQLCOM_SHOW_FIELDS:
  case SQLCOM_SHOW_KEYS:
  case SQLCOM_SHOW_VARIABLES:
  case SQLCOM_SHOW_CHARSETS:
  case SQLCOM_SHOW_COLLATIONS:
  case SQLCOM_SHOW_STORAGE_ENGINES:
  case SQLCOM_SHOW_PROFILE:
  case SQLCOM_SELECT:
  {
    thd->status_var.last_query_cost= 0.0;
    thd->status_var.last_query_partial_plans= 0;

    if ((res= select_precheck(thd, lex, all_tables, first_table)))
      break;

    res= execute_sqlcom_select(thd, all_tables);
    break;
  }

...

在这个函数中会发生许多有趣的事情。您将会注意到另一个 switch 语句,它使用了SQLCOM关键字。在示例查询中,您看到解析器将成员变量lex->sql_command设置为SQLCOM_SELECT。我已经在的清单 3-9 中为您提供了该案例陈述的精简视图。我没有包括许多其他的SQLCOM案例陈述。这是一个非常大的函数。因为它是查询处理的中心路由功能,所以它包含每个可能命令的案例。因此,源代码有几十页长。

让我们看看这个 case 语句的作用。注意 select_precheck()方法调用。该方法执行权限检查,查看用户是否可以使用表列表来执行命令,以验证访问权限。如果用户有访问权,处理继续到 execute_sqlcom_select()方法,如清单 3-10 所示。我将有关DESCRIBE ( EXPLAIN)命令的部分代码留给您来检查并弄清楚它是如何工作的。

清单 3-10 。execute_sqlcom_command()函数

static bool execute_sqlcom_select(THD *thd, TABLE_LIST *all_tables)
{
  LEX       *lex= thd->lex;
  select_result *result= lex->result;
  bool res;
  /* assign global limit variable if limit is not given */
  {
    SELECT_LEX *param= lex->unit.global_parameters;
    if (!param->explicit_limit)
      param->select_limit=
        new Item_int((ulonglong) thd->variables.select_limit);
  }
  if (!(res= open_and_lock_tables(thd, all_tables, 0)))
  {
    if (lex->describe)
    {
      /*
        We always use select_send for EXPLAIN, even if it's an EXPLAIN
        for SELECT ... INTO OUTFILE: a user application should be able
        to prepend EXPLAIN to any query and receive output for it,
        even if the query itself redirects the output.
      */
      if (!(result= new select_send()))
        return 1;                               /* purecov: inspected */
      res= explain_query_expression(thd, result);
      delete result;
    }
    else
    {
      if (!result && !(result= new select_send()))
        return 1;                               /* purecov: inspected */
      select_result *save_result= result;
      select_result *analyse_result= NULL;
      if (lex->proc_analyse)
      {
        if ((result= analyse_result=
               new select_analyse(result, lex->proc_analyse)) == NULL)
          return true;
      }
      res= handle_select(thd, result, 0);
      delete analyse_result;
      if (save_result != lex->result)
        delete save_result;
    }
  }
  return res;
}

image 注意有一次当我修改代码时,我需要找到所有EXPLAIN调用的位置,这样我就可以根据特定的需要修改它们。我到处寻找,直到在解析器中找到它们。在莱克斯/YACC 代码的中间,有一个注释,大意是DESCRIBE是早期 Oracle 兼容性问题遗留下来的,正确的术语是EXPLAIN。注释是有用的,如果你能找到它们的话。

下一个有趣的函数调用是对handle_select()的调用。你可能在想,“我们不是刚做了手柄的事情吗?”handle_select()是另一个函数mysql_select()的包装器。清单 3-11 显示了handle_select()功能的完整代码。清单顶部附近是select_lex->next_select()操作,它检查将多个SELECT结果追加到一组结果中的UNION命令。除此之外,代码只调用链中的下一个函数mysql_select()。此时,您终于可以过渡到查询优化器子系统了。表 3-4 列出了与查询优化器相关的源文件。

image 注意这可能是代码中最容易受到未定义子系统影响的部分。虽然代码仍然非常有组织,但是在源代码的这一点上,子系统的边界是模糊的。

清单 3-11handle _ select()函数

bool handle_select(THD *thd, select_result *result,
                   ulong setup_tables_done_option)
{
  bool res;
  LEX *lex= thd->lex;
  register SELECT_LEX *select_lex = &lex->select_lex;
  DBUG_ENTER("handle_select");
  MYSQL_SELECT_START(thd->query());

  if (lex->proc_analyse && lex->sql_command != SQLCOM_SELECT)
  {
    my_error(ER_WRONG_USAGE, MYF(0), "PROCEDURE", "non-SELECT");
    DBUG_RETURN(true);
  }

  if (select_lex->master_unit()->is_union() || 
      select_lex->master_unit()->fake_select_lex)
    res= mysql_union(thd, lex, result, &lex->unit, setup_tables_done_option);
  else
  {
    SELECT_LEX_UNIT *unit= &lex->unit;
    unit->set_limit(unit->global_parameters);
    /*
      'options' of mysql_select will be set in JOIN, as far as JOIN for
      every PS/SP execution new, we will not need reset this flag if 
      setup_tables_done_option changed for next rexecution
    */
    res= mysql_select(thd,
        select_lex->table_list.first,
        select_lex->with_wild, select_lex->item_list,
        select_lex->where,
        &select_lex->order_list,
        &select_lex->group_list,
        select_lex->having,
        select_lex->options | thd->variables.option_bits |
                      setup_tables_done_option,
        result, unit, select_lex);
  }
  DBUG_PRINT("info",("res: %d  report_error: %d", res,
       thd->is_error()));
  res|= thd->is_error();
  if (unlikely(res))
    result->abort_result_set();

  MYSQL_SELECT_DONE((int) res, (ulong) thd->limit_found_rows);
  DBUG_RETURN(res);
}

表 3-4。查询优化器

源文件 描述
/sql/sql_parse.cc 大部分解析器代码都在这个文件中
/sql/sql_select.cc 包含一些优化功能和选择功能的实现
/sql/sql_prepare.cc 包含优化程序的准备方法。
/sql/sql_executor.cc 包含优化程序的执行方法。

优化查询

终于!你在优化器那里。然而,如果您去寻找那个名字的源文件或类,您将找不到它。虽然JOIN类包含一个名为optimize()的方法,但是优化器实际上是一个流控制和子功能的集合,旨在找到执行查询的最短路径。花哨的算法、查询路径和编译后的查询发生了什么变化?回想一下我们在第 2 章的架构讨论,MySQL 查询优化器是一个非传统的混合优化器,它结合了已知的最佳实践和基于成本的路径选择。在代码的这一点上,最佳实践部分开始发挥作用。

其中一个最佳实践的例子是标准化WHERE子句表达式中的参数。示例查询使用带有表达式Employees.department = 'EGR'WHERE子句,但是该子句可以写成'EGR' = Employees.department,仍然是正确的(它返回相同的结果)。这是一个传统的基于成本的优化器可以生成多个计划的例子——每个表达式变量一个计划。MySQL 使用的许多最佳实践的几个例子是:

  • 常数传播——使用常数移除传递连接词。比如你有 a=b='c'传递性定律陈述 a='c'这种优化消除了那些内部等式,从而减少了求值的次数。例如,SQL 命令SELECT * FROM table 1 WHERE column 1 = 12 AND NOT(column 3 = 17 OR column 2 = column 1)将被简化为**SELECT * FROM table 1 WHERE column 1 = 12 AND column 3<>17 AND column 2<>12
  • 死代码消除——消除始终为真的条件。例如,如果您有 a=b 和 1=1和 1=1* 条件被移除。对于 always- false 条件也是如此,在这种情况下,可以删除 false 表达式,而不会影响子句的其余部分。例如,SQL 命令 SELECT * FROM table1,其中 column1 = 12,column2 = 13,column1 < column2 将被简化为 SELECT * FROM table1,其中 column1 = 12,column2 = 13 。*
    ** 范围查询——将 子句中的 转换为析取列表。例如,如果在(1,2,3)中有一个变换将是 a = 1 或 a = 2 或 a = 3 。*这有助于简化表达式的计算。例如,SQL 命令
    SELECT * FROM table 1 WHERE column 1 = 12 或 column1 = 17 或 column1 = 21* 将简化为**SELECT * FROM table 1 WHERE column 1 IN(12,17,21) 。*

*我希望这一小部分例子能够让您对世界上最成功的非传统查询优化器的内部工作原理有所了解。简而言之,它对于数量惊人的查询非常有效。

嗯,我说得太快了。在优化领域,mysql_select()函数也没有太多变化。似乎mysql_select()函数只是锁定表,然后调用mysql_execute_select()函数。你又一次处于另一个模糊的边界。清单 3-12 显示了mysql_select()函数的摘录。

清单 3-12MySQL _ select()函数

/**
  An entry point to single-unit select (a select without UNION).

  @param thd                  thread handler
  @param tables               list of all tables used in this query.
                              The tables have been pre-opened.
  @param wild_num             number of wildcards used in the top level 
                              select of this query.
                              For example statement
                              SELECT *, t1.*, catalog.t2.* FROM t0, t1, t2;
                              has 3 wildcards.
  @param fields               list of items in SELECT list of the top-level
                              select
                              e.g. SELECT a, b, c FROM t1 will have Item_field
                              for a, b and c in this list.
  @param conds                top level item of an expression representing
                              WHERE clause of the top level select
  @param order                linked list of ORDER BY agruments
  @param group                linked list of GROUP BY arguments
  @param having               top level item of HAVING expression
  @param select_options       select options (BIG_RESULT, etc)
  @param result               an instance of result set handling class.
                              This object is responsible for send result
                              set rows to the client or inserting them
                              into a table.
  @param unit                 top-level UNIT of this query
                              UNIT is an artificial object created by the
                              parser for every SELECT clause.
                              e.g.
                              SELECT * FROM t1 WHERE a1 IN (SELECT * FROM t2)
                              has 2 unions.
  @param select_lex           the only SELECT_LEX of this query

  @retval
    false  success
  @retval
    true   an error
*/

bool
mysql_select(THD *thd,
             TABLE_LIST *tables, uint wild_num, List<Item> &fields,
             Item *conds, SQL_I_List<ORDER> *order, SQL_I_List<ORDER> *group,
             Item *having, ulonglong select_options,
             select_result *result, SELECT_LEX_UNIT *unit,
             SELECT_LEX *select_lex)
{
  bool free_join= true;
  uint og_num= 0;
  ORDER *first_order= NULL;
  ORDER *first_group= NULL;
  DBUG_ENTER("mysql_select");

  if (order)
  {
    og_num= order->elements;
    first_order= order->first;
  }
  if (group)
  {
    og_num+= group->elements;
    first_group= group->first;
  }

  if (mysql_prepare_select(thd, tables, wild_num, fields,
                       conds, og_num, first_order, first_group, having,
                       select_options, result, unit,
                       select_lex, &free_join))
  {
    if (free_join)
    {
      THD_STAGE_INFO(thd, stage_end);
      (void) select_lex->cleanup();
    }
    DBUG_RETURN(true);
  }

  if (! thd->lex->is_query_tables_locked())
  {
    /*
      If tables are not locked at this point, it means that we have delayed
      this step until after the prepare stage (i.e. this moment). This allows us to
      do better partition pruning and avoid locking unused partitions.
      As a consequence, in such a case, the prepare stage can rely only on
      metadata about tables used and not data from them.
      We need to lock tables now in order to proceed with the remaining
      stages of query optimization and execution.
    */
    if (lock_tables(thd, thd->lex->query_tables, thd->lex->table_count, 0))
    {
      if (free_join)
      {
        THD_STAGE_INFO(thd, stage_end);
        (void) select_lex->cleanup();
      }
      DBUG_RETURN(true);
    }

    /*
      Only register query in cache if it tables were locked above.

      Tables must be locked before storing the query in the query cache.
      Transactional engines must have been signalled that the statement started,
      which external_lock signals.
    */
    query_cache_store_query(thd, thd->lex->query_tables);
  }

  DBUG_RETURN(mysql_execute_select(thd, select_lex, free_join));
}

所有这些最佳实践在哪里?他们在JOIN班!对JOIN类中的优化器源代码的详细检查将花费比这本书更多的页面来呈现任何有意义的深度。简单地说,优化器很复杂,也很难检查。幸运的是,很少有人需要深入探究 MySQL。但是,欢迎您这样做!我将集中精力对optimizer from the mysql_execute_select() function进行更高层次的审查。

该函数中的下一个主要函数调用是join->exec()方法。不过,首先让我们看看在清单 3-13 中的mysql_execute_select()方法中发生了什么。

清单 3-13。MySQL _ execute _ select()函数

/**
  Execute stage of mysql_select.

  @param thd                  thread handler
  @param select_lex           the only SELECT_LEX of this query
  @param free_join            if join should be freed

  @return Operation status
    @retval false  success
    @retval true   an error

  @note tables must be opened and locked before calling mysql_execute_select.
*/

static bool
mysql_execute_select(THD *thd, SELECT_LEX *select_lex, bool free_join)
{
  bool err;
  JOIN* join= select_lex->join;

  DBUG_ENTER("mysql_execute_select");
  DBUG_ASSERT(join);

  if ((err= join->optimize()))
  {
    goto err; // 1
  }

  if (thd->is_error())
    goto err;

  if (join->select_options & SELECT_DESCRIBE)
  {
    join->explain();
    free_join= false;
  }
  else
    join->exec();

err:
  if (free_join)
  {
    THD_STAGE_INFO(thd, stage_end);
    err|= select_lex->cleanup();
    DBUG_RETURN(err || thd->is_error());
  }
  DBUG_RETURN(join->error);
}

现在我们可以在mysql_execute_select()函数中看到优化器代码的入口。我们看到了对在 prepare 方法中创建的现有 JOIN 类的引用。在代码中再往下一点,我们看到了我们期望的方法——optimize()调用。此后不久,我们看到了通过 JOIN 类执行查询的exec()方法。表 3-5 列出了与查询优化相关的更重要的源文件。

表 3-5。查询优化

源文件 描述
/sql/abstract_query_plan.cc 实现了一个抽象查询计划接口,用于检查查询计划的某些方面,而无需访问 mysqld 内部类(JOIN_TAB、SQL_SELECT 等)。)直接。
/sql/sql_optimizer.cc 包含优化器核心功能
/sql/sql_planner.cc 包含帮助优化器确定检索连接行的表顺序的类。
/sql/sql_select.h 选择功能中用于支持the SELECT命令的结构定义
/sql/sql_select.cc 包含一些优化功能和选择功能的实现
/sql/sql_union.cc 用于执行联合操作的代码。

执行查询

与优化器一样,查询执行使用一组最佳实践来执行查询。例如,查询执行子系统检测特殊子句,如ORDER BYDISTINCT,,并将这些操作的控制路由到为快速排序和元组消除而设计的方法。

这种活动大部分发生在JOIN类的方法中。清单 3-14 展示了一个join::exec()方法的浓缩视图。请注意,还有另一个函数调用,调用的函数名称中包含了select。果然,还有一个调用需要调用一个名为do_select()的函数。看看这个函数调用的参数。您现在开始看到字段列表等内容。这是否意味着你正在接近读取数据?没错。事实上,do_select()函数正是为此而设计的高级包装器。

清单 3-14join::exec()函数

void
JOIN::exec()
{
  Opt_trace_context * const trace= &thd->opt_trace;
  Opt_trace_object trace_wrapper(trace);
  Opt_trace_object trace_exec(trace, "join_execution");
  trace_exec.add_select_number(select_lex->select_number);
  Opt_trace_array trace_steps(trace, "steps");
  List<Item> *columns_list= &fields_list;
  DBUG_ENTER("JOIN::exec");

...

  THD_STAGE_INFO(thd, stage_sending_data);
  DBUG_PRINT("info", ("%s", thd->proc_info));
  result->send_result_set_metadata(*fields,
                                Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF);
  error= do_select(this);
  /* Accumulate the counts from all join iterations of all join parts. */
  thd->inc_examined_row_count(examined_rows);
  DBUG_PRINT("counts", ("thd->examined_row_count: %lu",
                        (ulong) thd->get_examined_row_count()));

  DBUG_VOID_RETURN;
}

还有一个函数调用看起来很有意思。注意代码语句result->send_result_set_metadata ()。这个函数如其名所示。这个函数将字段头发送给客户端。正如您所猜测的,还有其他方法可以将结果发送给客户端。我将在第四章的中讨论这些方法。请注意thd->inc_examined_row_count=任务。这将记录计数值保存在THD类中。我们来看看那个do_select()功能。

你可以在清单 3-15 所示的do_select()方法中看到,一些重要的事情正在发生。请注意最后突出显示的代码语句。语句join->result->send_eof()看起来好像代码正在某处发送一个文件结束标志。它确实向客户端发送了一个文件结束信号。那么结果在哪里呢?它们在first_select()函数中生成(映射到sub_select()))。接下来让我们看看这个函数。

清单 3-15do _ select()函数

static int
do_select(JOIN *join)
{
  int rc= 0;
  enum_nested_loop_state error= NESTED_LOOP_OK;
  DBUG_ENTER("do_select");

...

  else
  {
    JOIN_TAB *join_tab= join->join_tab + join->const_tables;
    DBUG_ASSERT(join->tables);
    error= join->first_select(join,join_tab,0);
    if (error >= NESTED_LOOP_OK)
      error= join->first_select(join,join_tab,1);
  }

  join->thd->limit_found_rows= join->send_records;
  /* Use info provided by filesort. */
  if (join->order)
  {
    // Save # of found records prior to cleanup
    JOIN_TAB *sort_tab;
    JOIN_TAB *join_tab= join->join_tab;
    uint const_tables= join->const_tables;

    // Take record count from first non constant table or from last tmp table
    if (join->tmp_tables > 0)
      sort_tab= join_tab + join->tables + join->tmp_tables - 1;
    else
    {
      DBUG_ASSERT(join->tables > const_tables);
      sort_tab= join_tab + const_tables;
    }
    if (sort_tab->filesort &&
        sort_tab->filesort->sortorder)
    {
      join->thd->limit_found_rows= sort_tab->records;
    }
  }

  {
    /*
      The following will unlock all cursors if the command wasn't an
      update command
    */
    join->join_free(); // Unlock all cursors
  }
  if (error == NESTED_LOOP_OK)
  {
    /*
      Sic: this branch works even if rc != 0, e.g. when
      send_data above returns an error.
    */
    if (join->result->send_eof())
      rc= 1;                                  // Don't send error
    DBUG_PRINT("info",("%ld records output", (long) join->send_records));
  }

...

}

现在你有所进展了!花点时间浏览一下清单 3-16 。这个清单显示了sub_select()函数的一个精简视图。注意,代码以一个名为READ_RECORD的结构的初始化开始。READ_RECORD结构包含从表中读取的元组。系统初始化表,开始顺序读取记录,然后一次读取一条记录,直到所有记录都被读取。

清单 3-16sub _ select()函数

enum_nested_loop_state
sub_select(JOIN *join,JOIN_TAB *join_tab,bool end_of_records)
{
  DBUG_ENTER("sub_select");

  join_tab->table->null_row=0;
  if (end_of_records)
  {
    enum_nested_loop_state nls=
      (*join_tab->next_select)(join,join_tab+1,end_of_records);
    DBUG_RETURN(nls);
  }
  READ_RECORD *info= &join_tab->read_record;

...

  join->thd->get_stmt_da()->reset_current_row_for_warning();

  enum_nested_loop_state rc= NESTED_LOOP_OK;
  bool in_first_read= true;
  while (rc == NESTED_LOOP_OK && join->return_tab >= join_tab)
  {
    int error;
    if (in_first_read)
    {
      in_first_read= false;
      error= (*join_tab->read_first_record)(join_tab);
    }
    else
      error= info->read_record(info);

    DBUG_EXECUTE_IF("bug13822652_1", join->thd->killed= THD::KILL_QUERY;);

    if (error > 0 || (join->thd->is_error()))   // Fatal error
      rc= NESTED_LOOP_ERROR;
    else if (error < 0)
      break;
    else if (join->thd->killed) // Aborted by user
    {
      join->thd->send_kill_message();
      rc= NESTED_LOOP_KILLED;
    }
    else
    {
      if (join_tab->keep_current_rowid)
        join_tab->table->file->position(join_tab->table->record[0]);
      rc= evaluate_join_record(join, join_tab);
    }
  }

  if (rc == NESTED_LOOP_OK && join_tab->last_inner && !join_tab->found)
    rc= evaluate_null_complemented_join_record(join, join_tab);

  DBUG_RETURN(rc);
}

image 注意清单 3-16 中的代码比我展示的其他例子更加简洁。主要原因是这段代码使用了相当多的高级编程技术,比如递归和函数指针重定向。然而,所提出的概念对于示例查询是准确的。

控制返回到JOIN类,用于计算表达式和执行关系运算符。在结果被处理之后,它们被传输到客户端,然后控制返回到sub_select()函数,在这里发送文件结束标志来告诉客户端没有更多的结果。我希望这篇文章满足了您的好奇心,如果没有别的,也希望它提高了您对真实世界数据库系统复杂性的理解。请随意再次回顾此教程,直到您对基本流程感到满意为止。我将在下一节讨论一些更重要的类和结构。

支持库

MySQL 源代码树中有许多附加的库。Oracle 长期以来一直致力于封装和优化许多用于访问受支持的操作系统和硬件的常用例程。这些库中的大部分被设计成使代码与操作系统和硬件无关。这些库使得编写代码成为可能,这样特定的平台特性就不会迫使您编写专门的代码。这些库包括用于管理有效的字符串处理、哈希表、链表、内存分配等的库。表 3-6 列出了一些比较常见的库的用途和位置。

image 提示发现你正在使用的例程是否有库的最好方法是使用文本搜索工具查看/mysys目录下的源代码文件。大多数包装函数都有一个类似于它们原始函数的名字。例如,my_alloc.c实现了malloc包装器。

表 3-6。支持库

源文件 公用事业
/mysys/array.c 数组运算
/include/hash.h/mysys/hash.c 散列表
/mysys/list.c 合框架
/mysys/my_alloc.c 存储器分配
/strings/*.c 基本内存和字符串操作例程
/mysys/string.c 字符串操作
/mysys/my_pthread.c 穿线

重要的类和结构

MySQL 源代码中相当多的类和结构被认为是系统成功的关键因素。要全面了解 MySQL 源代码,需要学习系统中使用的所有关键类和结构的基础知识。了解什么存储在哪个类中或者结构包含什么可以帮助您很好地集成您的修改。以下部分描述了这些关键的类和结构。

项目 _ 类别

渗透到整个子系统的一个类是ITEM_ class。我称之为ITEM_是因为许多类都是从基类ITEM中派生出来的,甚至还有从基类中派生出来的类。这些衍生工具用于存储和操作系统中的大量数据(项目)。这些包括参数(如在WHERE子句中)、标识符、时间、字段、函数、数量、字符串和许多其他内容。清单 3-17 显示了一个ITEM基类的浓缩视图。该结构在/sql/item.h源文件中定义,在/sql/item.cc源文件中实现。附加的子类在以其封装的数据命名的文件中定义和实现。比如 function 子类在/sql/item_func.h中定义,在/sql/item_func.cc中实现。

清单 3-17ITEM _ Class

class Item
{
  Item(const Item &); /* Prevent use of these */
  void operator=(Item &);
  /* Cache of the result of is_expensive(). */
  int8 is_expensive_cache;
  virtual bool is_expensive_processor(uchar *arg) { return 0; }

public:
  static void *operator new(size_t size) throw ()
  { return sql_alloc(size); }
  static void *operator new(size_t size, MEM_ROOT *mem_root) throw ()
  { return alloc_root(mem_root, size); }
  static void operator delete(void *ptr,size_t size) { TRASH(ptr, size); }
  static void operator delete(void *ptr, MEM_ROOT *mem_root) {}

  enum Type {FIELD_ITEM= 0, FUNC_ITEM, SUM_FUNC_ITEM, STRING_ITEM,
      INT_ITEM, REAL_ITEM, NULL_ITEM, VARBIN_ITEM,
      COPY_STR_ITEM, FIELD_AVG_ITEM, DEFAULT_VALUE_ITEM,
      PROC_ITEM,COND_ITEM, REF_ITEM, FIELD_STD_ITEM,
      FIELD_VARIANCE_ITEM, INSERT_VALUE_ITEM,
             SUBSELECT_ITEM, ROW_ITEM, CACHE_ITEM, TYPE_HOLDER,
             PARAM_ITEM, TRIGGER_FIELD_ITEM, DECIMAL_ITEM,
             XPATH_NODESET, XPATH_NODESET_CMP,
             VIEW_FIXER_ITEM};

  enum cond_result { COND_UNDEF,COND_OK,COND_TRUE,COND_FALSE };

  enum traverse_order { POSTFIX, PREFIX };

  /* Reuse size, only used by SP local variable assignment, otherwize 0 */
  uint rsize;

  /*
    str_values's main purpose is to be used to cache the value in
    save_in_field
  */
  String str_value;

  Item_name_string item_name;  /* Name from select */
  Item_name_string orig_name;  /* Original item name (if it was renamed)*/

  /**
     Intrusive list pointer for free list. If not null, points to the next
     Item on some Query_arena's free list. For instance, stored procedures
     have their own Query_arena's.

     @see Query_arena::free_list
   */
  Item *next;
  uint32 max_length;                    /* Maximum length, in bytes */
  /**
     This member has several successive meanings, depending on the phase we're
     in:
     - during field resolution: it contains the index, in the "all_fields"
     list, of the expression to which this field belongs; or a special
     constant UNDEF_POS; see st_select_lex::cur_pos_in_all_fields and
     match_exprs_for_only_full_group_by().
     - when attaching conditions to tables: it says whether some condition
     needs to be attached or can be omitted (for example because it is already
     implemented by 'ref' access)
     - when pushing index conditions: it says whether a condition uses only
     indexed columns
     - when creating an internal temporary table: it says how to store BIT
     fields
     - when we change DISTINCT to GROUP BY: it is used for book-keeping of
     fields.
  */
  int marker;
  uint8 decimals;
  my_bool maybe_null; /* If item may be null */
  my_bool null_value; /* if item is null */
  my_bool unsigned_flag;
  my_bool with_sum_func;
  my_bool fixed;                        /* If item fixed with fix_fields */
  DTCollation collation;
  Item_result cmp_context;              /* Comparison context */
 protected:
  my_bool with_subselect;               /* If this item is a subselect or some
                                           of its arguments is or contains a
                                           subselect. Computed by fix_fields
                                           and updated by update_used_tables. */
  my_bool with_stored_program;          /* If this item is a stored program
                                           or some of its arguments is or
                                           contains a stored program.
                                           Computed by fix_fields and updated
                                           by update_used_tables. */

  /**
    This variable is a cache of 'Needed tables are locked'. True if either
    'No tables locks is needed' or 'Needed tables are locked'.
    If tables are used, then it will be set to
    current_thd->lex->is_query_tables_locked().

    It is used when checking const_item()/can_be_evaluated_now().
  */
  bool tables_locked_cache;
 public:
  // alloc & destruct is done as start of select using sql_alloc
  Item();
  /*
     Constructor used by Item_field, Item_ref & aggregate (sum) functions.
     Used for duplicating lists in processing queries with temporary
     tables
     Also it used for Item_cond_and/Item_cond_or for creating
     top AND/OR structure of WHERE clause to protect it from
     optimization changes in prepared statements
  */
  Item(THD *thd, Item *item);
  virtual ∼Item()
  {
#ifdef EXTRA_DEBUG
    item_name.set(0);
#endif
  } /*lint -e1509 */
  void rename(char *new_name);
  void init_make_field(Send_field *tmp_field,enum enum_field_types type);
  virtual void cleanup();
  virtual void make_field(Send_field *field);
  virtual Field *make_string_field(TABLE *table);
  virtual bool fix_fields(THD *, Item **);
  ...

};

LEX 结构

LEX结构负责查询及其部分的内部表示(内存存储)。然而,不仅仅如此。LEX结构用于以有组织的方式存储查询的所有部分。有字段、表、表达式以及构成任何查询的所有部分的列表。

当解析器发现查询的各个部分时,LEX结构由解析器填充。因此,当解析器完成时,LEX结构包含了优化和执行查询所需的一切。清单 3-18 显示了LEX结构的浓缩视图。该结构在/sql/sql_lex.h源文件中定义。

清单 3-18LEX 结构

struct LEX: public Query_tables_list
{
  SELECT_LEX_UNIT unit;                         /* most upper unit */
  SELECT_LEX select_lex;                        /* first SELECT_LEX */
  /* current SELECT_LEX in parsing */
  SELECT_LEX *current_select;
  /* list of all SELECT_LEX */
  SELECT_LEX *all_selects_list;

  char *length,*dec,*change;
  LEX_STRING name;
  char *help_arg;
  char* to_log;                                 /* For PURGE MASTER LOGS TO */
  char* x509_subject,*x509_issuer,*ssl_cipher;
  String *wild;
  sql_exchange *exchange;
  select_result *result;
  Item *default_value, *on_update_value;
  LEX_STRING comment, ident;
  LEX_USER *grant_user;
  XID *xid;
  THD *thd;

  /* maintain a list of used plugins for this LEX */

  DYNAMIC_ARRAY plugins;
  plugin_ref plugins_static_buffer[INITIAL_LEX_PLUGIN_LIST_SIZE];

  const CHARSET_INFO *charset;

...

};

网状结构

NET结构负责存储与客户端通信相关的所有信息。清单 3-19 显示了NET结构的浓缩视图。buff成员变量用于存储原始的通信数据包(当它们组合起来形成 SQL 语句时)。正如您将在后面的章节中看到的,帮助器函数填充、读取和传输来自客户端的数据包。两个例子是:

  • my_net_write(),将数据包从NET结构写入网络协议
  • my_net_read(),将数据包从网络协议读入NET结构

您可以在/include/mysql_com.h中找到整套网络通信功能。

清单 3-19 网络结构

typedef struct st_net {
#if !defined(CHECK_EMBEDDED_DIFFERENCES) || !defined(EMBEDDED_LIBRARY)
  Vio *vio;
  unsigned char *buff,*buff_end,*write_pos,*read_pos;
  my_socket fd; /* For Perl DBI/dbd */
  /*
    The following variable is set if we are doing several queries in one
    command ( as in LOAD TABLE ... FROM MASTER ),
    and do not want to confuse the client with OK at the wrong time
  */
  unsigned long remain_in_buf,length, buf_length, where_b;
  unsigned long max_packet,max_packet_size;
  unsigned int pkt_nr,compress_pkt_nr;
  unsigned int write_timeout, read_timeout, retry_count;
  int fcntl;
  unsigned int *return_status;
  unsigned char reading_or_writing;
  char save_char;
  my_bool unused1; /* Please remove with the next incompatible ABI change */
  my_bool unused2; /* Please remove with the next incompatible ABI change */
  my_bool compress;
  my_bool unused3; /* Please remove with the next incompatible ABI change. */
  /*
    Pointer to query object in query cache, do not equal NULL (0) for
    queries in cache that have not stored its results yet
  */
#endif
  /*
    Unused, please remove with the next incompatible ABI change.
  */
  unsigned char *unused;
  unsigned int last_errno;
  unsigned char error; 
  my_bool unused4; /* Please remove with the next incompatible ABI change. */
  my_bool unused5; /* Please remove with the next incompatible ABI change. */
  /** Client library error message buffer. Actually belongs to struct MYSQL. */
  char last_error[MYSQL_ERRMSG_SIZE];
  /** Client library sqlstate buffer. Set along with the error message. */
  char sqlstate[SQLSTATE_LENGTH+1];
  /**
    Extension pointer, for the caller private use.
    Any program linking with the networking library can use this pointer,
    which is handy when private connection specific data needs to be
    maintained.
    The mysqld server process uses this pointer internally,
    to maintain the server internal instrumentation for the connection.
  */
  void *extension;
} NET;

THD 类

在前面的源代码之旅中,您看到了许多对THD类的引用。事实上,对于每个连接,正好有一个THD对象。thread 类对于成功的线程执行至关重要,它参与了从实现访问控制到向客户端返回结果的所有操作。结果,THD类出现在服务器中运行的几乎每个子系统或功能中。清单 3-20 显示了一个THD类的浓缩视图。花点时间浏览一些成员变量和方法。如您所见,这是一个很大的类(我已经省略了很多方法)。该类在/sql/sql_class.h源文件中定义,并在/sql/sql_class.cc源文件中实现。

清单 3-20 。THD 类

class THD :public MDL_context_owner,
           public Statement,
           public Open_tables_state
{
private:

  ...

  String  packet; // dynamic buffer for network I/O
  String  convert_buffer;               // buffer for charset conversions
  struct  rand_struct rand; // used for authentication
  struct  system_variables variables;       // Changeable local variables
  struct  system_status_var status_var; // Per thread statistic vars
  struct  system_status_var *initial_status_var; /* used by show status */
  THR_LOCK_INFO lock_info;              // Locking info of this thread
  /**
    Protects THD data accessed from other threads:
    - thd->query and thd->query_length (used by SHOW ENGINE
      INNODB STATUS and SHOW PROCESSLIST
    - thd->mysys_var (used by KILL statement and shutdown).
    Is locked when THD is deleted.
  */
  mysql_mutex_t LOCK_thd_data;

  ...

};

读取记录结构

正如我们前面看到的,READ_RECORD结构用于包含来自存储引擎的元组,一旦优化器将它标识为要返回给用户的行。我们将存储引擎的讨论留到第 10 章中。清单 3-21 显示了READ_RECORD的结构。请注意,这里有回调到JOIN类方法的函数指针、引用THD类的变量、记录长度以及指向记录缓冲区本身的指针。如果您对了解行在系统中的存储方式感兴趣,可以研究一下这个类中的许多方法。

清单 3-21 。读取记录结构

struct READ_RECORD
{
  typedef int (*Read_func)(READ_RECORD*);
  typedef void (*Unlock_row_func)(st_join_table *);
  typedef int (*Setup_func)(JOIN_TAB*);

  TABLE *table;                                 /* Head-form */
  TABLE **forms;                                /* head and ref forms */
  Unlock_row_func unlock_row;
  Read_func read_record;
  THD *thd;
  SQL_SELECT *select;
  uint cache_records;
  uint ref_length,struct_length,reclength,rec_cache_size,error_offset;
  uint index;
  uchar *ref_pos; /* pointer to form->refpos */
  uchar *record;
  uchar *rec_buf;                /* to read field values  after filesort */
  uchar       *cache,*cache_pos,*cache_end,*read_positions;
  struct st_io_cache *io_cache;
  bool print_error, ignore_not_found_rows;

...

  Copy_field *copy_field;
  Copy_field *copy_field_end;
public:
  READ_RECORD() {}
};

MySQL 外挂程式

不提到架构中最重要和最新的创新之一,MySQL 系统之旅是不完整的。MySQL 现在支持一个插件工具,允许动态加载系统特性。这不仅意味着用户可以通过只加载她需要的东西来定制她的系统,还意味着 MySQL 的开发者可以以更加模块化的设计来开发功能。存储引擎子系统是为使用新的插件机制而重新设计的子系统的一个例子。还有很多其他的。我们将在后面的章节中更详细地看到插件是如何工作的。现在,让我们讨论插件是如何加载和卸载的,以及如何确定插件的状态。

mysql数据库中的plugins表用于在启动时加载插件。该表只包含两列,namedl,它们存储插件名和库名。启动时,除非用户关闭了插件,否则系统会加载在dl栏中指定的每个库,并为其各自的库启动在name栏中指定的每个插件。然后,可以手动修改这个表来管理插件,但是不推荐这样做,因为有些库可以包含多个插件。稍后,当我们检查mysql_plugin客户端应用时,您将看到这个概念的实际应用。

有些插件被认为是“内置”的,在默认情况下是可用的,在某些情况下是自动加载(安装)的。这包括许多存储引擎以及标准身份验证机制、二进制日志等。其他插件可以通过安装来使用,同样,也可以通过卸载来禁用。您可以在在线参考手册的“服务器插件”部分找到关于管理插件的完整文档。

安装和卸载插件

插件可以使用特殊的 SQL 命令作为启动选项来加载和卸载,也可以通过mysql_plugin客户端应用来加载和卸载。要加载一个插件,首先需要将正确的库放入由系统变量plugin_dir中的路径指定的插件目录中。您可以从 MySQL 中找到该变量的当前值,如下所示:

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

可以看到路径是/usr/local/mysql/lib/plugin/。当你构建你的插件或者安装一个现存的插件时,你必须首先把你的库放到插件目录中。然后,您可以执行类似于以下内容的安装插件命令:

mysql> INSTALL PLUGIN something_cool SONAME some_cool_feature.so;

在这里,我们正在加载一个名为something_cool的插件,它包含在名为some_cool_feature.so的已编译库模块中。

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

mysql> UNINSTALL PLUGIN something_cool;

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

mysqld ... --plugin-load=something_cool=some_cool_feature.so   
mysqld ... --plugin-load=something_cool=some_cool_feature.so;something_even_better=even_better.so

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

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

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

#
# 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
#
libdaemon_example
daemon_example

要使用mysql_plugin应用安装(启用)或卸载(禁用)插件,请至少指定插件的名称:ENABLEDISABLEbasedirdatadirplugin-dirplugin-ini选项。如果 mysql_plugin 应用不在您的路径上,您可能还需要指定my-print-defaults选项。应用以静默方式运行,但是您可以打开详细度来查看应用的运行情况。(使用选项:-vvv)。下面展示了如何使用 mysql_plugin 客户端应用加载daemon_example插件。该示例从 MySQL 安装的 bin 文件夹中运行。

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表中的任何行。类似地,将发出删除查询来禁用插件。

发现可用插件的状态

您可以通过检查INFORMATION_SCHEMA PLUGINS视图来发现系统上可用的插件。清单 3-22 是该视图输出的摘录。请注意,每个存储引擎以及每个插件的版本和状态都有条目。该视图还包含用于存储插件类型版本(创建插件时的系统版本)和作者的字段。通过使用EXPLAIN命令,您可以看到这个视图的所有字段。

清单 3-22 。信息 _ 模式。插件视图

mysql> SELECT plugin_name, plugin_version, plugin_status, plugin_type 
       FROM INFORMATION_SCHEMA.PLUGINS;
+-----------------------+----------------+---------------+--------------------+
| plugin_name           | plugin_version | plugin_status | plugin_type        |
+-----------------------+----------------+---------------+--------------------+
| binlog                | 1.0            | ACTIVE        | STORAGE ENGINE     |
| mysql_native_password | 1.0            | ACTIVE        | AUTHENTICATION     |
| mysql_old_password    | 1.0            | ACTIVE        | AUTHENTICATION     |
| CSV                   | 1.0            | ACTIVE        | STORAGE ENGINE     |
| MEMORY                | 1.0            | ACTIVE        | STORAGE ENGINE     |
| MyISAM                | 1.0            | ACTIVE        | STORAGE ENGINE     |
| MRG_MYISAM            | 1.0            | ACTIVE        | STORAGE ENGINE     |
| ARCHIVE               | 3.0            | ACTIVE        | STORAGE ENGINE     |
| BLACKHOLE             | 1.0            | ACTIVE        | STORAGE ENGINE     |
| FEDERATED             | 1.0            | DISABLED      | STORAGE ENGINE     |
| InnoDB                | 1.1            | ACTIVE        | STORAGE ENGINE     |
| INNODB_TRX            | 1.1            | ACTIVE        | INFORMATION SCHEMA |
| INNODB_LOCKS          | 1.1            | ACTIVE        | INFORMATION SCHEMA |
| INNODB_LOCK_WAITS     | 1.1            | ACTIVE        | INFORMATION SCHEMA |
| INNODB_CMP            | 1.1            | ACTIVE        | INFORMATION SCHEMA |
| INNODB_CMP_RESET      | 1.1            | ACTIVE        | INFORMATION SCHEMA |
| INNODB_CMPMEM         | 1.1            | ACTIVE        | INFORMATION SCHEMA |
| INNODB_CMPMEM_RESET   | 1.1            | ACTIVE        | INFORMATION SCHEMA |
| PERFORMANCE_SCHEMA    | 0.1            | ACTIVE        | STORAGE ENGINE     |
| partition             | 1.0            | ACTIVE        | STORAGE ENGINE     |
+-----------------------+----------------+---------------+--------------------+
20 rows in set (0.00 sec)

现在,您已经浏览了源代码,并研究了系统中使用的一些重要的类和结构,我将重点转移到有助于您实现自己对 MySQL 系统的修改的项目上。让我们暂时离开源代码,考虑软件开发的编码指南和文档方面。

编码指南

如果我描述的源代码看起来有一种奇怪的格式,那可能是因为您的风格与源代码作者的风格不同。考虑这样一种情况,有许多开发者编写一个大型软件程序,如 MySQL,每个人都有自己的风格。可以想象,代码很快就会变得像一大堆杂乱的语句。为了避免这种情况,Oracle 发布了各种形式的编码指南。当您自己开始探索代码时,您将会看到,似乎有一些开发者没有遵循编码指南。唯一合理的解释是指导方针随着时间的推移而改变,这可能发生在大型项目的整个生命周期中。因此,代码的某些部分是使用一组规则编写的,而其他部分可能使用不同版本的规则。不管这个结果如何,开发者确实努力遵循了指导方针。

编码指南有一个巨大的列表,包含了为 MySQL 服务器编写 C/C++代码的注意事项。我已经捕捉到了最重要的指导方针,并在下面的段落中为您进行了总结。

一般准则

指南中最强调的一个方面是,您应该编写尽可能优化的代码。这个目标与敏捷开发方法相反,在敏捷开发方法中,您只编写您需要的代码,而将细化和优化留给重构。如果您使用敏捷方法进行开发,您可能希望等到重构之后再签入代码。

另一个非常重要的总体目标是避免使用直接的 API 或操作系统调用。您应该总是在相关的库中寻找包装函数。这些函数中有许多都经过优化,可以快速安全地执行。比如,千万不要用 C malloc()函数。而是使用sql_alloc ()my _alloc()功能。

所有代码行的长度必须少于 80 个字符。如果需要将一行代码延续到另一行,请对齐代码,以便参数垂直对齐,或者延续代码与缩进空间计数对齐。

注释是使用标准 C 风格的注释编写的,例如,/* this is a comment */。您应该在代码中自由地使用注释。

image 提示抵制使用 C++ // comment选项的冲动。MySQL 编码指南明确反对这种技术。

文件

源代码选择的语言是英语。这包括所有变量、函数名、常量和注释。编写和维护 MySQL 源代码的开发者遍布欧洲和美国。在源代码中选择英语作为默认语言很大程度上是由于美国计算机科学发展的影响。在许多欧洲国家,英语也作为第二语言在许多小学和中学教育项目中教授。

编写函数时,使用注释块来描述函数、其参数和预期的返回值。注释块的内容应该分成几个部分,每个部分的名称都要大写。您应该在注释后的第一行包含一个简短的函数描述名,并且至少要包含部分、概要、描述和返回值。您还可以包括可选部分,如警告、注释、另请参见、待办事项、错误和 REFERENCED_BY。各部分和内容描述如下:

  • 简介(必需)—简要概述该功能中的流程和控制机制。它应该允许读者理解函数的基本算法。这有助于读者理解该功能,并对其功能有一个大致的了解。该部分还包括所有参数的描述(用 IN 表示输入,用 OUT 表示输出,用 IN/OUT 表示其值可以更改的引用参数)。
  • 描述(必选)——功能的描述。它应该包括函数的目的和它的使用的简要描述。
  • 返回值(必选)—显示所有可能的返回值以及它们对调用者的意义。
  • 警告—包括这一部分来描述呼叫者应该注意的任何不寻常的副作用。
  • 注释—包括这一部分,为读者提供您认为重要的任何信息。
  • 参见—当您正在编写一个与另一个函数相关联的函数,或者需要另一个函数的特定输出,或者打算由另一个函数以特定的调用顺序使用的函数时,请包括这一节。
  • TODO—包含此部分,以传达该功能的任何未完成功能。完成这些项目后,请将其从本节中删除。我倾向于忘记做这件事,这经常会让我有点挠头,因为我已经完成了待办事项。
  • 错误—包含这一部分来记录您的函数所具有的任何异常错误处理。
  • REFERENCED _ BY—包含此部分以传达此函数与其他函数或对象之间关系的特定方面——例如,无论何时您的函数被另一个函数调用,该函数都是另一个函数的原语,或者该函数是友元方法甚至是虚方法。

image 提示 Oracle 建议没有必要为只有几行代码的短函数提供注释块,但是我建议为您创建的所有函数编写一个注释块。当您探索源代码并遇到许多很少或没有文档的小(和一些大)函数时,您会喜欢这个建议。

清单 3-23 中显示了一个函数注释块的例子。

清单 3-23 示例函数注释块

/**
  Find tuples by key.

  SYNOPSIS
    find_by_key()
    string key            IN     A string containing the key to find.
    Handler_class *handle IN     The class containing the table to be searched.
    Tuple *               OUT    The tuple class containing the key passed.

    Uses B Tree index contained in the Handler_class. Calls Index::find()
    method then returns a pointer to the tuple found.

  DESCRIPTION
    This function implements a search of the Handler_class index class to find
    a key passed.

  RETURN VALUE
    SUCCESS (TRUE)                 Tuple found.
    != SUCCESS (FALES)             Tuple not found.

  WARNING
    Function can return an empty tuple when a key hit occurs on the index but
    the tuple has been marked for deletion.

  NOTES
    This method has been tested for empty keys and keys that are greater or
    less than the keys in the index.

  SEE ALSO
    Query:;execute(), Tuple.h

  TODO
    * Change code to include error handler to detect when key passed in exceeds
    the maximum length of the key in the index.

  ERRORS
    -1                              Table not found.
    1                               Table locked.

  REFERENCED_BY
    This function is called by the Query::execute() method.
*/

功能和参数

我想特别指出这些项目,因为源代码中存在一些不一致的地方。如果您使用源代码作为格式化的指南,您可能会偏离编码指南。函数及其参数应该对齐,以便参数垂直对齐。这适用于定义函数和从其他代码调用它。同样,在声明变量时,变量也应该对齐。对齐的间距不是这些项目垂直外观的问题。您还应该添加关于每个变量的行注释。行注释应该从第 49 列开始,并且不能超过最大 80 列的规则。如果变量的注释超过 80 列,请将该注释放在单独的一行。清单 3-24 显示了函数、变量和参数的对齐类型的例子。

清单 3-24 变量、函数和参数对齐示例

int     var1;                                 /* comment goes here */
long    var2;                                 /* comment goes here too */
/* variable controls something of extreme interest and is documented well */
bool    var3;

return_value *classname::classmethod(int  var1,
                                    int  var2
                                    bool var3);

if (classname->classmethod(myreallylongvariablename1,
                          myreallylongvariablename2,
                          myreallylongvariablename3) == -1)
{
  /* do something */
}

image 警告如果你在 Windows 上开发,你的编辑器的换行符可能设置不正确。当您在文件中放置一个换行符时,Windows 中的大多数编辑器都会发出一个 CRLF ( /r/n)。Oracle 要求您使用单个 LF ( /n),而不是 CRLF。这是在 Windows 上创建的文件与在 UNIX 或 Linux 上创建的文件之间常见的不兼容。如果您使用的是 Windows,请检查您的编辑器,并对其配置进行适当的更改。

命名惯例

Oracle 更喜欢使用带下划线的小写字母而不是大写字母为变量指定有意义的名称。例外情况是类名的使用,它需要有首字母大写。枚举应该以短语enum_为前缀。所有的结构和定义都应该用大写字母书写。命名约定的例子如清单 3-25 所示。

清单 3-25 示例命名约定

class My_classname;
int   my_integer_counter;
bool  is_saved;

#define CONSTANT_NAME 12;

int my_function_name_goes_here(int variable1);

间距和缩进

MySQL 编码指南规定每个缩进层次的间距应该总是两个字符。千万不要用制表符。如果您的编辑器允许,请更改编辑器的默认行为以关闭自动格式,并用两个空格替换所有制表符。当使用 Doxygen(我稍后将讨论)或行解析工具等文档工具在文本中定位字符串时,这一点尤其重要。

当标识符和运算符之间有空格时,变量和运算符之间不包含空格,运算符和操作数之间只包含一个空格(运算符的右侧)。同样,函数中的左括号后面也不能有空格,但是参数之间要有一个空格,最后一个参数名和右括号之间不能有空格。最后,包含一个空行来描述变量声明与控制代码,控制代码与方法调用,块注释与其他代码,函数与其他声明。清单 3-26 描述了一段格式正确的代码摘录,包含一个赋值语句、一个函数调用和一个控制语句。

清单 3-26 间距和缩进

return_value= do_something_cool(i, max_limit, is_found);
if (return_value)
{
  int var1;
  int var2;

  var1= do_something_else(i);

  if (var1)
  {
    do_it_again();
  }
}

在源代码的某些部分,花括号的对齐方式也不一致。MySQL 编码指南规定花括号应该和它上面的控制代码对齐,正如我在所有例子中展示的那样。如果您需要缩进另一个级别,请使用与花括号中的代码相同的列对齐方式(两个空格)。如果在代码块中执行一行代码,也没有必要使用花括号。

花括号区域中的一个奇怪之处是 switch 语句。应该编写一个 switch 语句,将 switch 条件后面的左花括号对齐,并将右花括号与 switch 关键字对齐。case 语句应该与 switch 关键字在同一列中对齐。清单 3-27 说明了这一准则。

清单 3-27 开关语句示例

switch (some_var) {
case 1:
   do_something_here();
   do_something_else();
   break;
case 2:
   do_it_again();
   break;
}

image 注意前面代码中的最后一个break不需要。为了完整起见,我通常将它包含在我的代码中。

文档实用程序

检查源代码的另一个有用的方法是使用自动文档生成器,它读取源代码并生成基于函数和基于类的方法列表。这些程序列出了所使用的结构,并提供了它们在源代码中的使用方式和位置的线索。这对于研究 MySQL 很重要,因为源代码依赖于许多关键结构来操作和操纵数据。

一个这样的项目叫做 Doxygen。Doxygen 的好处是,它也是开源的,由 GPL 管理。当您调用 Doxygen 时,它读取源代码并生成一组可读性很高的 HTML 文件,这些文件从函数之前的源代码中提取注释,它还列出了函数原语。Doxygen 可以阅读 C、C++和 Java 等编程语言。Doxygen 是研究复杂系统(如 MySQL)的有用工具——尤其是当您考虑到代码中有数百个位置调用了基本库函数时。

Doxygen 可用于 UNIX 和 Windows 平台。要在 Linux 上使用该程序,请从 Doxygen 网站http://www.doxygen.com下载源代码。

下载安装后,按照安装说明(也在网站上)进行操作。Doxygen 使用配置文件来生成输出的外观以及输入中包含的内容。要生成默认配置文件,请发出以下命令:

doxygen -s -g /path_to_new_file/doxygen_config_filename

指定的路径应该是您想要存储文档的路径。一旦有了默认配置文件,就可以编辑该文件并更改参数以满足您的特定需求。有关选项及其参数的更多信息,请参见 Doxygen 文档。您通常会指定要处理的文件夹、项目名称以及其他与项目相关的设置。一旦设置了所需的配置,就可以通过发出以下命令为 MySQL 生成文档:

doxygen </path_to_new_file/Doxygen_config_filename>

image 注意根据您的设置,Doxygen 可能会运行很长时间。如果您希望 Doxygen 在合理的时间内生成文档,请避免使用高级绘图命令。

最新版本的 Doxygen 可以使用提供的 GUI 在 Windows 上运行。GUI 允许您使用向导创建配置文件,该向导将引导您逐步完成该过程,并创建基本配置文件、允许您设置自己的参数的专家模式,以及加载配置文件的能力。我发现使用向导界面生成的输出足以满足偶然到深入的查看。

我建议在深入研究源代码之前,花一些时间运行 Doxygen 并检查输出文件。这将节省你大量的查找时间。光是这些结构就值得贴在你显示器旁边的墙上,或者贴在你的工程日志上。Doxygen 可以生成的文件类型示例如图 3-3 所示。

9781430246596_Fig03-03.jpg

图 3-3。示例 MySQL Doxygen 输出

保存工程日志

许多开发者记录他们的项目。一些比另一些更详细,但大多数在会议和电话交谈中做笔记,从而为口头交流提供书面记录。如果你没有记录工程日志的习惯,你应该考虑这样做。我发现日志是我工作中的一个重要工具。是的,写东西确实需要更多的努力,如果你试图包含所有你认为重要的各种图纸和电子邮件,日志可能会变得混乱(我的日志经常塞满了从重要文件上剪下的剪报,就像某种工程师的剪贴簿)。然而,回报可能是巨大的。

当你在研究 MySQL 源代码的时候做一些调查工作时,尤其如此。把你的每一个发现都记录下来。写下每一个顿悟、重要的设计决策、重要纸质文档的片段,甚至偶尔的啊哈!随着时间的推移,你会建立起你的发现的纸质记录(我的一个前老板称之为她的纸质大脑!)这将证明对于评论和您自己的文档工作是非常宝贵的。如果你真的使用日志,做日志条目或粘贴重要的文件片段,你很快就会发现各种日志并不适合被很好地组织。大多数工程师(比如我)更喜欢无法重组的有线条的精装期刊(除非你用很多剪刀和胶水)。其他人更喜欢便于重组的活页日志。如果你打算使用精装期刊,可以考虑在使用过程中建立一个“活”索引。

image 提示如果你的日记本没有编号,花几分钟时间在每一页上都标上页码。

有许多方法可以建立生活索引。你可以在页面顶部或页边空白处写下任何有趣的关键词。这可以让你快速浏览日志,找到感兴趣的项目。活索引的特点是能够随着时间的推移添加引用。我发现创建 living index 的最好方法是使用电子表格列出你写在日志页上的所有术语,并在旁边写上页码。我大约每周更新一次电子表格,打印出来,贴在我的日志前面。我见过一些期刊前面有一个口袋,但磁带的方法也很有效。随着时间的推移,您可以对索引项和参考页码进行重新排序,以使列表更易于阅读;您也可以在日志的前面放置一个更新的列表,这样您可以更容易地找到页面。

考虑使用工程日志。到了向上级汇报进展的时候,你不会后悔的。当你被要求报告六个月或更久以前做的事情时,它也可以为你节省大量的重复工作。

跟踪您的更改

当您创建的代码对读者来说不直观时,请始终使用注释。例如,代码语句if (found)非常简单明了。如果变量评估为TRUE,将执行控制语句后的代码。然而,代码if (func_call_17(i, x, lp))需要一些解释。当然,您希望编写的所有代码都是不言自明的,但有时这是不可能的。当您访问支持库函数时尤其如此。有些名称不直观,参数列表可能会令人困惑。当你编码时,记录下这些情况,你的生活将会得到改善。

编写注释时,可以使用行内注释、单行注释或多行注释。行内注释从第 49 列开始写,不能超过 80 列。单行注释应该与它所引用的代码对齐(缩进标记),并且不应该超过 80 列。同样,多行注释应该与它们解释的代码对齐,并且不应该超过 80 列,但是它们应该将开始和结束注释标记放在单独的行上。清单 3-28 说明了这些概念。

清单 3-28 。注释位置和间距示例

if (return_value)
{
  int     var1;                                 /* comment goes here */
  long    var2;                                 /* comment goes here too */

  /* this call does something else based on i */
  var1= do_something_else(i);

  if (var1)
  {
    /*
      This comment explains
      some really interesting thing
      about the following statement(s).
    */
    do_it_again();
  }
}

image 提示永远不要使用重复的*来强调代码的某些部分。它分散了读者对代码的注意力,看起来杂乱无章。此外,将所有这些东西排列起来太麻烦了——尤其是当你以后编辑评论的时候。

如果您正在使用诸如 bazaar 这样的源代码控制应用修改 MySQL 源代码,您不必担心跟踪您的更改。Bazaar 为您提供了几种方法来检测和报告哪些变化是您自己的,哪些是别人的。如果您没有使用源代码管理应用,您可能会忘记哪些更改是您自己的,特别是当您直接对现有的系统函数进行更改时。在这种情况下,很难区分你写的和已经存在的。保持工程日志对这个问题有很大帮助,但是有一个更好的方法。

您可以在更改前后添加注释,以指示哪些代码行是您修改的。例如,您可以在代码前放置一个像/* BEGIN CAB MODIFICATION */这样的注释,在代码后放置一个像/* END CAB MODIFICATION */这样的注释。这允许您将更改括起来,并帮助您使用许多文本和行解析实用程序轻松地搜索更改。这种技术的一个例子显示在清单 3-29 中。

清单 3-29 评论您对 MySQL 源代码的修改

/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* This section adds my revision note to the MySQL version number. */
   /* original code:   */
   /*strmov(end, "."); */
   strmov(end, "-CAB Modifications");
/* END CAB MODIFICATION */

请注意,我还包括了修改的原因和原始代码的注释行(这个例子是虚构的)。使用这种技术将帮助您快速访问您的更改,并增强您以后诊断问题的能力。

如果您进行了修改以便在您的组织中使用,并且不打算与 Oracle 共享这些更改,这种技术也会很有帮助。如果您不共享这些更改,每次 Oracle 发布您想要使用的新版本系统时,您都将被迫对源代码进行修改。在源代码中使用注释标记将帮助您快速确定哪些文件需要更改,以及这些更改应该是什么。如果你创造了一些新的功能,你可能最终会想要分享这些功能,如果没有别的原因,只是为了避免每次 MySQL 新版本发布时都进行修改。

image 注意虽然在使用配置控制下的源代码(BitKeeper)时这种技术没有被禁止,但通常是不鼓励的。事实上,开发者以后可能会完全删除你的评论。仅当您做出不打算与任何人共享的更改时,才使用此技巧。

首次构建系统

既然您已经看到了 MySQL 源代码的内部工作方式,并遵循了源代码中的典型查询路径,那么是时候开始了。如果您已经在使用 MySQL 源代码,并且您正在阅读本书以了解更多关于源代码以及如何修改它的信息,您可以跳过这一节。

在开始之前,我建议您下载源代码(如果您还没有下载的话),然后下载并安装适用于您选择的平台的可执行文件。将编译好的二进制文件放在手边很重要,以防在实验过程中出错。试图在没有参考点的情况下诊断一个修改过的 MySQL 源代码版本的问题是相当具有挑战性的。如果在遇到困难的调试问题时可以恢复到基本编译的二进制文件,您将会节省很多时间。我将在第 5 章中更详细地介绍调试。如果你发现自己遇到了系统问题,你可以重新安装二进制文件,让你的 MySQL 系统恢复正常。

编译源代码很容易。如果您使用的是 Linux,打开一个命令 shell,切换到源代码树的根目录,运行cmake和 make 命令。cmake 脚本将检查系统的依赖性并创建适当的 makefiles。下面概述了首次在 Linux 上构建源代码的典型构建过程:

$ cmake .

-- The C compiler identification is GNU
-- The CXX compiler identification is GNU
-- Check for working C compiler: /usr/bin/gcc
-- Check for working C compiler: /usr/bin/gcc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Looking for SHM_HUGETLB
-- Looking for SHM_HUGETLB - found
-- Looking for sys/types.h
-- Looking for sys/types.h - found
-- Looking for stdint.h
-- Looking for stdint.h - found
-- Looking for stddef.h
-- Looking for stddef.h - found

...

-- Configuring done
-- Generating done
-- Build files have been written to: /source/mysql-5.6.6

$ make

[  0%] Built target INFO_BIN
[  0%] Built target INFO_SRC
[  0%] Built target abi_check
[  0%] Building C object zlib/CMakeFiles/zlib.dir/adler32.c.o
[  0%] Building C object zlib/CMakeFiles/zlib.dir/compress.c.o
[  0%] Building C object zlib/CMakeFiles/zlib.dir/crc32.c.o
[  0%] Building C object zlib/CMakeFiles/zlib.dir/deflate.c.o
[  0%] Building C object zlib/CMakeFiles/zlib.dir/gzio.c.o
[  0%] Building C object zlib/CMakeFiles/zlib.dir/infback.c.o
[  0%] Building C object zlib/CMakeFiles/zlib.dir/inffast.c.o
[  0%] Building C object zlib/CMakeFiles/zlib.dir/inflate.c.o
[  0%] Building C object zlib/CMakeFiles/zlib.dir/inftrees.c.o
[  0%] Building C object zlib/CMakeFiles/zlib.dir/trees.c.o
[  0%] Building C object zlib/CMakeFiles/zlib.dir/uncompr.c.o
[  1%] Building C object zlib/CMakeFiles/zlib.dir/zutil.c.o
Linking C static library libzlib.a
[  1%] Built target zlib
[  1%] Building CXX object extra/yassl/CMakeFiles/yassl.dir/src/buffer.cpp.o

...

Linking CXX executable my_safe_process
[100%] Built target my_safe_process

$

image 提示关于设置复杂条件的更多信息,参见http://www.cmake.org/cmake/help/documentation.html。您还可以使用cmake-gui应用通过图形界面设置选项。你可以从 cmake.org 网站下载cmakecmake-gui。在线参考手册还包含一些使用条件编译 MySQL 的好例子。

您可以使用 Microsoft Visual Studio 编译 Windows 平台源代码。要在 Windows 上编译系统,请打开 Visual Studio 命令窗口(确保运行 vcvarsall.bat 批处理文件以加载所需的路径),然后发出cmake .命令,后跟devenv mysql.sln /build debug

image 注意您必须在当前目录中包含告知 cmake 开始工作的点。

这将构建服务器的调试版本。如果您希望从源代码树安装服务器,请参见 MySQL 参考手册http://dev.mysql.com/doc/refman/5.6/en/source-installation.html中的“源代码安装概述”一节。

运行 cmake 命令后,您还可以从 Visual Studio 的源代码分发树的根目录中打开mysql.sln项目工作区。在那里,您将活动项目设置为mysqld类,并将项目配置设置为mysqld - Win32 nt。当您单击 Build mysqld 时,该项目将编译任何必要的库,并将它们链接到您指定的项目。带一杯新鲜的饮料来娱乐自己,因为第一次建立所有的库可能需要一段时间。不管你使用哪个平台,编译后的可执行文件都会被放在client_releaseclient_debug文件夹中,这取决于你选择的编译选项。

image 注意大多数编译问题都可以追溯到开发工具配置不当或缺少库。有关如何解决最常见编译问题的详细信息,请参考 MySQL 论坛。

关于新编译的二进制文件(除非有问题),您首先会注意到的是,您无法判断该二进制文件是否是您编译的!您可以检查文件的日期,以查看可执行文件是否是您刚刚创建的文件,但是没有办法从客户端知道这一点。尽管 Oracle 不推荐这种方法,其他人也可能会回避这种方法,但是您可以更改 MySQL 编译的版本号,以表明它是您编译的版本号。

让我们假设您想一眼就识别出您的修改。例如,您希望在客户端窗口中看到一些指示,表明服务器是您的修改版本。您可以更改版本号来显示这一点。图 3-4 是这种修改的一个例子。

9781430246596_Fig03-04.jpg

图 3-4。带有版本修改的 MySQL 命令示例

请注意,在头和发出命令SELECT Version();的结果中,返回的版本号是您编译的服务器的相同版本号加上我放在字符串中的附加标签。要自己做这个改变,只需编辑mysqld.cpp文件中的set_server_version()函数,如清单 3-30 中的所示。在示例中,我加粗了一行代码,您可以添加它来创建这种效果。

清单 3-30 修改 set_server_version 函数

/*
  Create version name for running mysqld version
  We automaticly add suffixes -debug, -embedded and -log to the version
  name to make the version more descriptive.
  (MYSQL_SERVER_SUFFIX is set by the compilation environment)
*/

static void set_server_version(void)
{
  char *end= strxmov(server_version, MYSQL_SERVER_VERSION,
                     MYSQL_SERVER_SUFFIX_STR, NullS);
#ifdef EMBEDDED_LIBRARY
  end= strmov(end, "-embedded");
#endif
#ifndef DBUG_OFF
  if (!strstr(MYSQL_SERVER_SUFFIX_STR, "-debug"))
    end= strmov(end, "-debug");
#endif
  if (opt_log || opt_slow_log || opt_bin_log)
    strmov(end, "-log");                        // This may slow down system
  /* BEGIN CAB MODIFICATION */
  /* Reason for modification: */
  /* This section adds my revision note to the MySQL version number. */
  strmov(end, "-CAB MODIFICATION");
  /* END CAB MODIFICATION */
}

还要注意,我已经包含了我前面提到的修改意见。这将帮助您确定您更改了哪些代码行。这一变化还有一个好处,即新的版本号将显示在其他 MySQL 工具中,如 MySQL Workbench。图 3-5 显示了在 MySQL Workbench 中运行SELECT @@version查询的结果,该查询针对的是修改后编译的代码。

9781430246596_Fig03-05.jpg

图 3-5。使用 MySQL Workbench 访问修改后的 MySQL 服务器

image 警告我有没有说过这是不被认可的方法?如果您正在使用 MySQL 进行您自己的实验,或者您正在修改源代码供您自己使用,那么您可以按照我的建议去做。然而,如果您正在创建将在以后添加到基本源代码中的修改,您应该而不是实现这种技术。

摘要

在本章中,你学习了几种获取源代码的方法。无论您选择下载源代码树的快照或 GA 版本源代码的副本,还是使用开发者里程碑版本来获得最新和最好的版本,您都可以获得并开始使用源代码。这就是开源的美妙之处!

也许这一章最吸引人的地方是你对 MySQL 源代码的引导之旅。我希望通过一个简单的系统查询,您在理解 MySQL 源代码的探索中取得了很大的进展。我还希望,如果你在编译源代码时遇到了问题,你不会沮丧地扔掉这本书。一个优秀的开源开发者的主要素质是她系统地诊断和调整环境以适应当前项目需求的能力。如果你遇到问题,不要绝望。解决问题是学习周期的自然组成部分。

您还研究了 MySQL 编码指南文档中的主要元素,并看到了一些代码格式和文档指南的示例。虽然不完整,但我提供的编码指南足以让您感受到 Oracle 希望您如何编写修改的源代码。如果你遵循这些简单的指导方针,你以后就不会被要求遵守。

在接下来的两章中,我将带您了解软件开发中经常被忽视的两个非常重要的概念。下一章将向您展示如何应用测试驱动的开发方法来探索和扩展 MySQL 系统,下一章将讨论 MySQL 源代码的调试。*

四、测试驱动的 MySQL 开发

系统集成商必须克服他们正在集成的系统的局限性,但有时系统缺少集成所需的某些功能或命令。Oracle 认识到了这一点,并在 MySQL 服务器中包含了添加新功能和命令的灵活选项。本章介绍了为 MySQL 系统生成高质量扩展的一个关键要素。我讨论了软件测试,并解释了测试大型系统的一些常见实践,使用具体的例子来说明测试 MySQL 系统的公认实践。

背景

为什么在书中这么早就包含一章关于测试的内容呢?您需要了解可用的测试功能,以便您可以通过首先计划如何测试它们来计划您自己的修改。这是测试驱动开发的前提:从需求出发开发并实现测试,编写代码,然后立即执行测试。对于不熟悉这个概念的人来说,这听起来可能有点违反直觉——毕竟,如何为尚未编写的代码编写测试呢?

在接下来的部分中,我将通过提供一些关于这个越来越流行的概念的背景信息来澄清。

为什么要测试?

当我讲授软件质量问题时,我经常被问到,“为什么要测试?”一些学生想知道多少测试是足够的。我给那些觉得测试在很大程度上是浪费时间的学生,或者觉得测试被高估的学生,提供了一个机会,让他们使用最少测试或者不测试的策略来完成他们的课堂项目 1 。结果通常很有趣,也很有启发性。22

这些学生经常讨论他们如何编写他们的模块和类,以及他们如何小心地使用好的建模实践。许多人使用统一建模语言(UML)图表来帮助他们的软件开发。虽然这些都是好的实践,但是测试不仅仅是确保你的源代码与你的模型相匹配。那些坚持认为他们的高度磨练的编码技能已经足够的学生经常产生具有特性和功能问题的项目可交付物。

尽管这些项目中的大部分不会遭受致命的错误或崩溃(这在开发过程中经常发现),但是它们经常在集成和软件如何工作方面存在问题。也就是说,学生们无法确保他们的软件按照客户期望的方式工作。

如果这发生在你身上,你现在知道软件测试的价值了。当时,选择哪种技术使用是软件测试科学的真正本质。

image 提示专业软件测试人员(有时被称为质量控制工程师或质量保证工程师)对软件有着非常不同的看法。如果你曾经和一个专业的软件测试人员一起工作,花时间学习理解他们如何进行软件测试。他们通常对软件如何工作有着难以置信的洞察力——很少有开发者能够成功地磨练这种洞察力。如果他们破坏了你的代码,不要感到尴尬——那是他们的工作,大多数人都很擅长!

测试与调试

虽然他们通常有相同的目标——识别缺陷,但是调试和测试并不相同。调试是一个交互式过程,旨在通过揭示源代码的内部工作方式来定位源代码逻辑中的缺陷。另一方面,测试在不检查源代码内部工作的情况下识别源代码执行中的缺陷。

测试驱动开发

测试驱动开发通常与敏捷编程联系在一起,并且经常被采用极限编程(XP)方法的组织所使用。这听起来可能很可怕,但是这里有一个关于 XP 的秘密:你不必采用 XP 来使用敏捷实践!

我经常遇到这样的开发者,他们对采用敏捷实践深感忧虑,因为不知情的人抛出了所有负面的宣传。那些认为传统的软件工程过程一成不变的人认为敏捷实践是为了用更少的资源做更多的事情而设计的,因此,他们是劣等的。其他人认为敏捷实践“剔除了需求分析和设计的混乱”,从而“专注于代码”这些都不是真的。

敏捷实践旨在简化软件开发,重新吸引客户,只生产需要的和需要的,并专注于手头的工作(客户想要的)。敏捷方法的焦点是客户,而不是过程。显然,重点是在分析和设计上。

此外,敏捷实践被设计成既可以作为一个集合使用,也可以有选择地在应用中使用。也就是说,组织被鼓励采用他们认为合适的敏捷实践,而不是双脚跳进去,把他们工程师的世界颠倒过来。这是负面炒作背后的一个原因——以及那些试图在太短时间内做太多事情的组织所报告的失败。 3 如果你想了解更多关于敏捷方法与传统方法之争的信息,请浏览敏捷联盟网站,http://www.agilealliance.org

一个非常有用的敏捷实践是测试驱动开发。测试驱动开发的哲学很简单:从解决方案的基本模型开始,编写测试,运行测试(这将失败),编码解决方案,并用测试验证它(当未更改的测试通过时)。虽然这听起来很直观,但它的复杂程度令人惊讶。在代码听起来倒退之前创建测试。你怎么能测试一个不存在的东西?这有什么帮助?

首先开发测试可以让你专注于软件的设计,而不是代码。我将解释一个典型的测试驱动的敏捷开发过程,以便您可以看到测试驱动的开发是如何补充设计并实际驱动源代码的。这听起来很奇怪,但给它一个机会,它会有意义。

测试驱动开发从一个简单的系统模型开始,通常是系统中基本类的简单类图。类图只设置了空的类块,只标注了类的建议的名称。我说提议是因为这通常是习惯于传统方法的开发者被难住的地方。在敏捷实践中,没有什么是一成不变的,任何事情都可能是变化的候选(这种实践被称为重构)——它只是必须有意义,并推进生产客户想要的软件的最终目标。

什么是重构?

重构是一个批判性分析的过程,它会问:“如何才能做得更好?”例如,当检查代码的缺陷修复或添加新特性时,实践重构的开发者会寻找更好的方法来重新组织代码,以使其更高效、更易于维护。因此,重构的目标是改进软件的非功能方面。

虽然重构通常与源代码相关联,但前提适用于软件开发的所有领域,从精炼源代码到精炼测试以获得更大的覆盖范围。

一旦一个初始的类图被创建,它就被复制、搁置,并被称为域模型,因为它描述了你的类的初始布局。从那里,用例图和补充的用例场景(用例的文本描述和可选的执行顺序)被创建。每一个用例都被一个序列图所扩充,这个序列图描绘了被引用的类所需要的功能。

随着每个类开始成形,您开始编写测试。即使这些类不存在,您仍然必须编写测试——它们形成了集成、系统和接口测试的混合体(都是白盒技术),其中每个测试测试领域模型中的一个类。

image 注意 白盒测试是在不知道系统如何构造的情况下进行的测试。黑盒测试是在已知系统内部结构的情况下测试系统的行为。

对于大多数敏捷实践来说,在这一点上,从这个序列的第一次迭代中获得的经验被合并到设计的适当部分(用例,序列图,等等)。)并进行适当的更改。

image 注意一些敏捷实践者通过使用健壮性图在流程中增加了另一个建模步骤。这种适应非常类似于 ICONIX 过程。关于 ICONIX 流程的更多信息,请参见“使用 ICONIX 流程的敏捷开发4

有时这些变化包括新类的发现、现有类的重组,甚至是类的方法和属性的制定。换句话说,在编写代码之前编写测试有助于验证设计。这真的很酷,因为一旦您完成了您想要的迭代设计级别,并开始编写源代码,您的测试就完成了!您可以简单地运行它们,并证明您的代码按设计运行。当然,如果你需要改变测试,从而改变设计——这就是敏捷开发的美妙之处。

标杆

基准测试 旨在建立软件的性能特征。您可以使用基准测试来建立一个已知的性能水平(称为基线,然后在软件执行的环境发生变化后再次运行基准测试,以确定这些变化的影响。这是基准测试最常见的用途。其他包括识别负载下的性能限制,管理系统或环境的变化,以及识别可能导致性能问题的条件。

通过运行一组测试系统的测试来执行基准测试,并存储性能计数器的结果,称为基准测试。它们通常被存储或存档,并用系统环境的描述进行注释。例如,精明的数据库专业人员经常在他们的档案中包括系统配置和环境的基准和转储。这允许他们将系统过去的表现与当前的表现进行比较,并识别系统或其环境的任何变化。

测试通常是功能性的,目标是测试系统的特定特性或功能。基准测试工具包括广泛的测试,这些测试在轻、中和重负载下检查系统的一切,从普通到最复杂的操作。

尽管大多数开发者只在发生一些奇怪的事情时才考虑运行基准测试,但是以固定的时间间隔或者甚至在重大事件(比如系统或环境的变化)之前和之后运行基准测试是非常有用的。记得在第一次创建基线时运行您的基准。在没有基线的情况下,在事件之后进行的基准测试不会有很大帮助。

良好基准的指导方针

许多良好的实践都与基准测试相关联。在这一节中,我将带您了解一些我发现的有助于从基准测试中获得最大收益的方法。

首先,始终考虑前后快照的概念。不要等到对服务器进行了更改之后才看它与六个月前的基线相比如何——六个月内会发生很多事情。相反,在改变之前测量系统,进行改变,然后再次测量系统。这将为您提供三个指标来进行比较:系统的预期性能、变更前的性能以及变更后的性能。你可能会发现发生了一些事情,使你的变化或多或少的重要。例如,假设您的基准包括一个查询时间指标。六个月前为给定的测试查询建立的基线被设置为 4.25 秒。您决定修改正在测试的表的索引。运行 before 基准测试得到的值是 15.50,after 基准测试得到的值是 4.5 秒。如果你没有拍下之前的照片,你不会知道你的改变极大地提高了性能。相反,您可能会得出结论,该更改导致查询执行速度稍慢,这可能会导致您撤消该更改,从而导致返回到较慢的查询。

这个例子暴露了我想警告你的几个方面。如果您正在对预计存储的数据量会增长的系统进行数据检索性能基准测试,那么您需要更频繁地运行基准测试,以便您可以将数据增长的影响与系统性能对应起来。在前面的示例中,您可能认为 before 值对于系统条件(如数据负载)来说是“正常”的。

此外,要小心确保你的测试对你所测量的是有效的。如果您正在对一个表的查询性能进行基准测试,那么您的基准测试是针对应用级别的,对于预测一般意义上的系统性能没有用处。将应用级别的基准与更一般的指标分开,以避免得出错误的结论。

与前后概念相关的另一个好的实践是,在一段受限的活动期间(在一致的负载下)运行几次基准,以确保它们不会受到局部活动的影响,例如流氓流程或资源密集型任务。我发现运行基准测试多达几十次允许我确定结果的平均值。您可以使用许多技术来创建这些聚合。例如,您可以使用统计软件包来创建基本的统计数据,或者使用您最喜欢的统计友好的电子表格应用。 5

image 注意一些基准测试工具为你提供了这个特性。唉,MySQL 基准套件没有。

也许最有用的实践是一次改变一件事。不要对你的服务器进行大范围的修改,并期望从结果中得出任何有意义的结论。在这种情况下经常发生的是,六个左右的变化中的一个会对其他几个变化的增益产生负面影响,而其余的变化对性能的影响很小或没有影响。除非你一次做一个改变,否则你不知道哪个改变对系统有负面、正面或中性的影响。

尽可能使用真实数据。有时制造的数据包含恰好落入指定字段范围的数据,因此从不测试系统的某些特性(域和范围检查等)。).如果您的数据经常变化,您可能希望在某个时候对数据进行快照,并且每次都使用相同的数据集来构建您的测试。虽然这将确保您使用真实数据测试性能,但是,它可能无法测试性能随时间的增长而下降的情况。

最后,当解释你的基准结果和管理你的期望时,设定现实的目标。如果你试图在某些条件下提高系统的性能,在你设定目标之前,要牢牢把握已知的后果。例如,如果您正在检查将网络接口从千兆位连接切换到执行网络通信速度快 100 倍的接口的效果,您的服务器将不会以快 100 倍的速度执行其数据传输。在这种情况下以及类似的情况下,硬件增加的价值应该与硬件的成本以及使用新硬件的预期收益进行权衡。换句话说,您的服务器应该执行得更快一些,从而为您省钱(或增加收入)。

如果您估计您需要将网络性能提高 10%才能达到季度支出和收入目标,从而实现节约,请将该值作为您的目标。如果你的基准显示你已经达到了预期的改进(或者,更好的是,超过了预期),向你的老板要求加薪。如果基准测试显示绩效指标没有达到目标,告诉你的老板你可以通过退回硬件来帮他省钱(然后要求加薪)。无论哪种方式,你都可以用经验数据来支持你的观点:你的基准!

基准数据库系统

您可能同意基准测试是您的武器库中非常强大的工具,但是基准测试与数据库服务器到底有什么关系呢?答案是——很多。了解数据库服务器何时性能最佳,可以让您在繁重的查询处理负载期间设置一个衡量性能下降的标准。更确切地说,您如何判断资源密集型查询是性能问题的原因还是结果?

您可以在许多级别上对数据库服务器进行基准测试。最值得注意的是对数据库模式的基准更改。您可能不会为单个表创建测试(尽管您可以这样做),但是您更可能对数据库模式的更改如何影响性能感兴趣。

对于新的应用和数据库来说尤其如此。您可以创建几个模式,用数据填充它们,并编写旨在模拟提议的系统的基准测试。(又是那个测试驱动的东西。)通过创建备选模式并对它们进行基准测试,甚至可能进行多次迭代的更改,您可以快速确定哪个模式最适合您正在设计的应用。

您还可以针对特殊用途对数据库系统进行基准测试。例如,您可能希望检查数据库系统在各种负载或环境下的性能。要确定新的 RAID 设备是否会提高性能,还有什么比运行前后基准测试更好的方法,可以知道环境的变化会产生多大的影响呢?是的,it 是关于成本的,基准测试将有助于管理您的数据库系统成本。

剖析

有时,除非系统处于负载之下,否则缺陷不会显现出来。在这些情况下,系统可能会变慢,但不会产生任何错误。你如何发现这些类型的问题?您需要一种在系统运行时检查系统的方法。这个过程被称为剖析。一些作者将分析和调试放在一起,但是分析不仅仅是一个调试工具。概要分析允许您在基准测试中检测到性能瓶颈和潜在问题之前就识别它们。分析通常是在检测到问题之后进行的,有时也是确定问题根源的一种手段。使用性能分析,您可以发现或监控内存和磁盘消耗、CPU 使用、I/O 使用、系统响应时间以及许多其他系统参数。

术语剖析图(或剖析图)有时与执行目标系统参数的测量相混淆。性能指标的识别被称为诊断操作技术 (或有时称为跟踪 )。管理这些诊断操作并允许您对系统运行这些操作的系统称为 profiler。因此,分析是使用探查器的诊断操作的应用。

分析器通常会生成报告,其中包括在固定时间段内系统的机器可读记录。这些类型的性能测量通常被称为轨迹,因为它们随着时间的推移跟踪系统的路径。其他分析器被设计用来产生人类可读的打印输出,详细说明系统的哪个部分执行时间最长,或者更常见的是,系统在哪里花费了大部分时间。这种类型的探查器通常用于监控 I/O、内存、CPU 和线程或进程等资源。例如,您可以发现您的线程和进程正在执行什么命令或功能。如果您的系统在线程或进程头中记录了额外的元数据,您可能还会发现线程或进程阻塞和死锁的性能问题。

image 注意死锁的一个例子是当一个进程拥有对一个资源的锁(独占访问),并且正在等待另一个资源,该资源又被另一个正在等待第一个资源的进程锁定。死锁检测是精心设计的数据库系统的一个关键属性。

您还可以使用分析来确定哪些查询的性能最差,甚至哪些线程或进程的执行时间最长。在这些情况下,您可能还会发现某个线程或进程正在消耗大量资源(如 CPU 或内存),因此会采取措施纠正问题。在有大量用户访问中央资源的环境中,这种情况并不少见。

有时,系统的某些请求会导致一个用户的行为(合法的或不合法的——让我们希望是合法的)可能会影响到其他用户。在这种情况下,您可以正确地识别出有问题的线程或进程及其所有者,并采取措施来纠正问题。

在开发系统时,概要分析也是一个强大的诊断工具,因此倾向于称之为调试工具。您可以获得的关于您的系统的报告类型可能会导致您的源代码中出现各种意想不到的低效率。但是,注意不要过度。您可能会花费大量的时间来分析一段需要很长时间执行的源代码,这样您可能永远也不会完全满足识别瓶颈的期望。记住,有些事情需要一段时间来执行。磁盘 I/O 或网络延迟就是这种情况。通常,除了重新设计你的架构,减少对慢速资源的依赖,你不能做太多。当然,如果你正在设计一个嵌入式实时系统,这可能确实是一个有效的努力,但是通常不值得努力去改进你不能控制的东西。

然而,您应该始终努力使您的代码尽可能高效地运行。如果您发现可以使用概要分析来改进您的代码,那么尽一切办法去做。只是不要忘乎所以地试图识别或跟踪这些小事情。先买大件商品。

基准测试还是侧写?

基准测试和剖析之间的区别有时会被混淆。标杆管理建立绩效评级或衡量标准。概要分析根据系统的性能来识别系统的行为。

基准测试用于在给定的配置下建立已知的性能特征,而分析用于识别系统在哪里花费了大部分的执行时间。因此,基准测试用于确保系统的性能达到或优于给定的标准(基线),而概要分析用于确定性能瓶颈。

软件测试简介

软件测试对我们的行业越来越重要,因为很久以来就很清楚,软件系统失败的一个重要原因是缺乏足够的测试或进行测试的时间。

然而,测试进行的方式和测试本身的目标有时会引起争论。例如,一个设计良好的测试的目标是检测缺陷的存在。听起来很对,不是吗?想一想——这意味着成功的测试是发现了缺陷的测试。那么,如果测试没有发现任何缺陷会怎么样呢?测试失败是因为写得不正确,还是没有产生任何错误?这些争论(以及其他许多争论)是软件测试研究人员感兴趣的话题。

一些软件测试人员(让我们简称他们为测试人员)认为一个测试如果没有发现任何缺陷就是成功的,这并不等同于说一个成功的测试就是发现了缺陷。如果你站在这些测试人员的角度来看,一个系统有可能通过测试(所有测试都是成功的),但仍然有缺陷。在这种情况下,重点是测试而不是软件。此外,如果在测试后发现缺陷,很少会被认为是测试失败。

然而,如果你认为成功的测试就是发现缺陷,那么只有当软件没有缺陷时,你的测试才会失败。因此,当没有发现缺陷时,目标就变成了使测试更加健壮,这样他们就可以发现更多的缺陷。

功能测试与缺陷测试

测试人员通常关注于确保系统按照规范(也称为需求文档)的方式执行。他们经常进行测试来验证规范的功能,因此并不试图寻找缺陷。这种类型的测试被称为功能测试,有时也被称为系统测试。测试是在不了解系统内部工作原理的情况下创建的(称为黑盒测试),通常被写成以用户为中心的软件功能的逐步练习。例如,如果系统包括打印功能,可以编写功能测试来使用首选和备选执行场景执行打印功能。在这种情况下,成功的测试将表明打印功能正常工作,并给出正确的输出。功能测试只是众多测试类型中的一种,软件工程师和测试人员可以使用它来确保他们生产出高质量的产品。

第一个观点,缺陷测试,是给定一组有效和无效的输入数据,导致系统失败的有目的的意图。这些测试通常是用软件内部工作的知识编写的(通常被称为白盒测试)。构建缺陷测试是为了在测试软件的所有门和阈值条件的同时,对软件的特定组件的源代码执行所有可能的执行场景(或路径)。例如,如果您要为打印功能示例编写缺陷测试,您将编写不仅测试该功能的正确操作,而且测试每个已知的错误处理程序和异常触发器的测试。也就是说,您将编写测试来有目的地尝试破坏代码。在这种情况下,在没有识别缺陷的情况下完成的缺陷测试可以被认为是一个失败的测试(或者简单的否定——“失败”给人一种有问题的印象,但是实际上没有;简单来说,这种情况下没有发现错误)。66

出于本书的目的,我提出了功能和缺陷测试观点的结合。也就是说,我向您展示了如何进行具有检测缺陷的内置特性的功能测试。我们将使用的测试机制允许您使用执行 SQL 语句的测试对 MySQL 服务器进行功能测试。虽然您可以构建简单测试功能的测试,但是您也可以构建识别缺陷的测试。事实上,我建议您编写所有的测试来测试错误处理程序和异常。如果您的测试未能识别缺陷,或者稍后向您报告了一个 bug,您可以创建一个测试或者修改一个现有的测试来测试该 bug 的存在。这样,您可以在修复之前重复错误,然后显示错误已被修复。

软件测试的类型

软件测试通常在一个受约束的过程中进行,这个过程从分析系统需求和设计开始。然后使用需求和设计来创建测试,以确保质量(正确性、健壮性、可用性等)。)的软件。正如我前面提到的,一些测试是为了识别缺陷而进行的,而另一些测试是用来验证功能而没有错误的(这并不等同于没有缺陷)。一些测试技术的目标是建立对软件的评价或评估。这些测试通常侧重于定性因素,而不是定量结果。

测试是一个更大的软件工程咒语的一部分,它确保软件满足它的需求并交付期望的功能。这个过程有时被称为验证和确认 。这些很容易混淆。验证仅仅意味着你要确保软件是按照它的规格来构建的。验证仅仅意味着您遵循了正确的流程和方法来创建它。换句话说,验证会问,“我们构建了正确的产品吗?”验证会问,“我们是否正确地构建了产品?”

虽然许多软件开发过程包括验证和确认活动,但是大多数开发者将过程中确认满足规范的部分称为软件测试。此外,验证过程通常与测试系统的功能和功能中不存在缺陷而不是软件的正确性相关联。

你可以进行许多类型的软件测试。事实上,在早期的项目计划中,经常会有关于什么类型的测试应该或者不应该被要求的热烈讨论。幸运的是,大多数开发者都认为测试是软件开发的重要组成部分。然而,根据我的经验,很少有人理解不同类型的软件测试的作用。只有你可以选择什么是适合你的项目。我的目标是解释一些比较流行的软件测试类型,这样你就可以应用那些对你的需求最有意义的测试。

下面的章节描述了流行的软件测试技术,它们的目标和应用,以及它们与持续测试驱动开发的关系。正如您将看到的,测试的传统阶段是持续测试工作中的里程碑。

集成测试

集成测试是在系统由基本构件组装而成时进行的。测试通常首先测试单个组件,然后是这个组件和另一个组件,依此类推,直到整个系统被集成。这种形式的测试最常用于使用半独立组件构建的大型开发项目。

组件测试

组件测试是在独立的测试运行中,在系统的半独立部分(或组件)上进行的。也就是说,组件是通过调用它的所有方法和查询它的所有属性来实现的。组件测试通常以测试工具的形式构建,提供测试组件所需的所有外部通信。这包括任何依赖组件,这些组件是使用代码脚手架模拟的(有时称为模拟或存根组件)。这些代码支架提供了所有必要的输入和输出来交流和测试被测试的组件。

接口测试

接口测试是在组件本身的接口上进行的,而不是在组件上。目的是表明该接口提供了所有需要的功能。这种类型的测试通常与组件测试一起完成。

回归测试

回归测试确保软件的任何添加或修正不会影响到软件的其他部分。在这种情况下,将再次运行过去运行过的测试,并将结果与上一次运行的结果进行比较。如果结果是相同的,那么变更不会影响功能(在测试编写的范围内)。这种类型的测试通常使用自动化测试软件,允许开发者(或测试人员)在无人值守的情况下运行测试。在大部分测试完成后,对结果进行比较。自动化测试是敏捷开发哲学中的一个流行概念。

路径测试

路径测试确保所有可能的执行路径都得到执行。测试是在完全了解源代码的情况下编写的(白盒测试),通常不关心是否符合规范,而是关心系统准确遍历所有条件路径的能力。然而,很多时候,这些测试是在考虑功能的情况下进行的。

阿尔法阶段测试

传统上,一旦稳定的开发质量系统准备好了,alpha 阶段测试就开始了。这通常发生在生产软件的早期阶段。这个阶段的测试有时是为了确保系统已经达到了可以使用大部分功能的稳定性水平(可能有小的缺陷)。这可能包括运行部分测试,以验证系统在保护条件下工作。被认为是 alpha 的系统通常是完整的,可能包括一些已知的缺陷问题,范围从轻微到中等。通常,通过 alpha 测试就结束了 alpha 阶段,项目进入 beta 阶段。

在这一点上,系统是足够完整的,因此所有测试都是针对实际代码运行的,并且不需要搭建(存根类)。当测试结果满足被认为是测试版的项目参数时,项目进入测试阶段。

测试阶段测试

一个项目通常被认为是一个稳定的产品质量系统,当它拥有一套完整的功能,但可能包括一些尚未变得有效的特性,或者可能需要额外的健壮性工作(硬化)。在这个阶段运行的测试通常是针对所交付的特性的一整套测试。如果发现缺陷,它们通常是轻微的。这种类型的测试可以包括由目标受众和客户进行的测试。这些小组在他们的测试方法上不太科学,但是他们给开发者提供了一个机会来和客户一起检查他们的系统,并做一些小的修正来改进他们的产品。通过 beta 测试意味着软件已经准备好最终发布。

在测试驱动的开发环境中,beta 测试是持续测试工作中的另一个里程碑。测试驱动开发下的测试版通常是大多数特性相对于测试结果表现良好的点。系统的稳定性水平通常被判断为产生很少的缺陷。

发布、功能和验收测试

发布测试通常是功能测试,通过它来验证系统是否满足其规格,并且该测试在系统交付给客户之前进行。与测试阶段一样,一些组织选择让客户参与这个测试阶段。在这种情况下,测试方法通常被称为验收测试,因为是客户决定软件是否满足他们的规格。测试驱动的开发环境会将这些里程碑视为测试的完成。

可用性测试

可用性测试在系统完成后或接近完成时进行,有时与功能和发布测试并行进行。可用性测试的目标是确定用户与系统的交互有多好。通常没有通过或失败的结果,而是一个喜欢和不喜欢的列表。虽然可用性测试非常主观,并且仅仅基于用户的偏好,但是它有助于创建能够赢得用户忠诚度的软件。

可用性测试最好在实验室里完成,这个实验室被设计用来记录用户的反应和建议,以备日后查阅。这使得用户可以专注于软件而不会分心。然而,大多数可用性测试都是在非正式的环境下进行的,开发者观察用户使用系统的情况,或者给用户一段时间使用软件,然后她的评论被作为调查或采访的一部分。

可靠性测试

可靠性测试通常旨在改变系统上的负载,并用复杂的数据和不同数量的负载(数据)来挑战系统,进行可靠性测试是为了确定系统在一段时间内的持续运行情况。可靠性通常以系统持续运行的小时数和每小时或每次测试的缺陷数来衡量。

性能测试

执行性能测试是为了建立性能行为(基准测试)或确保系统在已建立的指导方针内运行。被检查的系统的方面有时包括可靠性以及性能。极端负载下的性能(称为压力测试)有时会在这种测试中进行检查。

image 注意可用性、可靠性和性能测试是可以在传统测试或测试驱动的开发环境中进行的测试形式。

测试设计

既然您已经简要介绍了软件测试以及您可以在自己的项目中进行的测试类型,那么让我们将注意力转向测试是如何构造的。所有不同的构建测试的哲学最终都是为了锻炼、确认或验证软件或其过程的某个方面。让我们看看三个最突出的基本哲学。

基于规范的测试

基于规格的测试(有时称为功能测试 )测试软件需求和设计。重点是验证软件是否符合其规格。这些测试通常是基于一个给定的需求或者一组需求而构建的。测试被组织成功能集(有时被称为测试套件)。当一个系统被构建时,测试集可以在需求完成时或者在过程的任何时候运行,以验证对需求的持续符合性(也称为回归测试 )。

分区测试

分区测试关注系统的输入和输出数据特征。它们测试被测输入或输出数据的外部、边缘和平均值范围。例如,假设系统被设计为接受 1 到 10 范围内的正整数值的输入。您可以通过测试值{0,1,5,10,11}来形成这些数据的分区(称为等价分区或域)。有些可能更进一步,包括负值,如 1。这个想法是,如果系统执行范围检查,边界条件比有效的,甚至是完全无效的数据更有可能出现缺陷。

在我们前面的例子中,没有必要测试大于 11 的值,除非您想测试内部数据收集代码(系统中读取和解释输入的部分)。大多数现代系统使用系统级调用来管理数据输入,这些调用本质上是非常可靠的(例如,Microsoft Windows Forms)。最有趣的是,您还可以为输出数据形成分区。在这种情况下,测试旨在测试系统如何接受已知数据(好的或坏的)并产生结果(好的或坏的)。在这种情况下,测试试图验证输入数据处理的鲁棒性和准确性。分区测试有助于证明系统满足性能和健壮性方面的要求。

结构测试

结构测试(有时称为架构测试 )确保系统是根据指定的布局(或架构)构建的——也就是说,验证系统符合规定的结构。这种性质的测试旨在确保某些接口可用并正常工作,并且组件能够正常工作。这些测试类别包括所有形式的白盒测试,其目标是测试系统中的每一条路径(被称为路径测试 )。这些测试可以被认为是验证的一种,因为它们确定了架构是否被正确构建,以及它是否遵循了规定的过程。

MySQL 测试

你可以用很多方法测试 MySQL 系统。您可以使用 mysqlshow 命令测试服务器连接和基本功能,使用客户端工具手动运行测试,使用基准测试工具建立性能特征,甚至在服务器上进行分析。大多数数据库专业人员选择的工具是 MySQL 测试套件和 MySQL 基准测试工具。以下部分描述了这些工具和技术。

使用 MySQL 测试套件

Oracle 提供了一个强大的测试工具,名为 MySQL Test Suite,一个名为 mysqltest 的可执行文件,以及一组 Perl 模块和脚本,旨在测试系统并比较结果。表 4-1 列出了一些相关的目录及其内容。测试套件附带了 Unix/Linux 二进制和源代码发行版,尽管它包含在一些 Mac 和 Windows 发行版中。

表 4-1 。mysql-test 目录下的目录

目录 内容
/收藏 在集成和发布测试期间执行的测试组
/r 结果文件的主套件
/std_data 测试套件的测试数据
/套件 也称为“suites”,包含特定功能测试的子文件夹,例如 binlog、(存储)引擎和复制
/t 主套件测试

image 注意MySQL 测试套件目前不能在 Windows 环境下运行。如果您想通过 MySQL 代码贡献计划为 MySQL 的开发做出贡献,这将是一个很好的项目。如果建立了 Perl 环境并且安装了 Perl DBI 模块,它可以在 Cygwin 环境中运行。有关更多详细信息,请参见 MySQL 参考手册中的“Perl 安装说明”。

安装 MySQL 后,您会在安装目录下的 mysql-test 目录中找到 mysql-test-run.pl Perl 脚本。最重要的是,测试套件是可扩展的。您可以编写自己的测试,并针对您的特定应用或需求进行测试。这些测试被设计为回归测试,从这个意义上说,这些测试旨在确保所有功能都像过去一样工作。

这些测试位于 mysql-test 目录下的一个名为 simply /t 的目录中。该目录包含近 740 个测试以及所有套件中的 3400 多个测试。虽然这听起来可能很全面,但是 MySQL 文档指出测试套件并没有涵盖系统的所有特性或细微差别。当前的测试集旨在检测大多数 SQL 命令、操作系统和库交互以及集群和复制功能中的错误。Oracle 希望最终积累足够的测试,为整个系统提供测试覆盖。目标是建立一组测试,测试 MySQL 系统的所有特性。如果您创建的附加测试覆盖了 mysql-test/t 目录中的某个测试尚未覆盖的特性,请随时将您的测试提交给 Oracle。

image 提示您可以通过访问 MySQL Internals 邮件列表找到关于 MySQL 测试套件的更多信息(参见http://lists.mysql.com/了解更多细节并查看可用列表)。您也可以通过向列表发送电子邮件来提交要包含的测试。如果您决定将您的测试发送到 Oracle 以包含在测试套件中,请确保您使用的数据是您可以向世界展示的。每个人都可以参加考试。例如,我确信你的朋友和亲戚不希望他们的电话号码出现在 MySQL 的每个安装中!

对于每个测试,相应的结果文件存储在 mysql-test/r 目录中。结果文件包含测试运行的输出,用于比较(使用 diff 命令)测试运行时的结果。在许多方面,结果文件是测试输出的基准。这使您能够创建测试并保存预期的结果,然后稍后运行测试并确保系统产生相同的输出。

你必须小心使用这个前提。可以使用本质上在两次执行之间会发生变化的数据值,但是它们需要额外的命令来正确处理。不幸的是,这样的数据值会被测试套件忽略,而不是直接比较。因此,如果在测试中使用,时间和日期字段是可能导致一些问题的数据类型。稍后我将详细讨论这个主题和其他命令。

运行测试

使用测试套件运行测试很容易。只需导航到 mysql-test 目录并执行命令。这将启动测试可执行文件,连接到服务器,并运行/t 目录中的所有测试。因为运行所有测试可能需要一些时间,所以 Oracle 编写了测试套件,允许您按顺序执行几个测试。例如,以下命令将只运行名为 t1、t2 和 t3 的测试:

% > ./mysql-test-run.pl t1 t2 t3

测试套件将按顺序运行每个测试,但如果有任何测试失败,就会停止运行。要覆盖此行为,请使用- force 命令行参数来强制测试套件继续进行。

测试套件被设计为执行它自己的 mysqld 可执行文件实例。这可能与您计算机上运行的另一个服务器实例冲突。在运行测试套件之前,您可能需要关闭 MySQL 服务器的其他实例。如果您从源目录使用测试套件,您可以通过编译源代码来创建 mysqld 可执行文件。如果您想测试您在服务器中已经更改的内容,但是不想或者不能关闭现有的服务器来这样做,这将非常方便。

image 注意只要服务器没有使用 13000 及以上的端口,您就可以在现有服务器旁边运行测试套件。如果是,测试套件可能无法正确运行,您可能需要停止服务器或将其更改为使用其他端口。

如果您想要连接到一个特定的服务器实例,您可以使用- extern 命令行参数来告诉测试套件连接到服务器。如果您有额外的启动命令或想要使用特定用户连接到服务器,您也可以添加这些命令。有关 mysql-test-run 脚本的可用命令行参数的更多信息,请输入命令% >。/mysql-test-run.pl - help

请访问http://dev.mysql.com/doc/mysql/en/mysql-test-suite.html了解更多详情。

image 注意使用- extern 命令行参数需要包含您想要执行的测试的名称。一些测试需要服务器的本地实例来执行。例如,以下命令连接到正在运行的服务器,并执行别名和分析测试:perl MySQL-test-run . pl-extern alias analyze。

创建新测试

要创建您自己的测试,请使用标准文本编辑器在/t 目录下名为 mytestname.test 的文件中创建测试。

清单 4-1。 样本测试

#
# Sample test to demonstrate MySQL Test Suite
#
--disable_warnings
SHOW DATABASES;
--enable_warnings
CREATE TABLE characters (ID INTEGER PRIMARY KEY,
                      LastName varchar(40),
                      FirstName varchar(20),
                      Gender varchar(2)) ENGINE = MYISAM;
EXPLAIN characters;
#
INSERT INTO characters (ID, LastName, FirstName, Gender)
             VALUES (3, 'Flintstone', 'Fred', 'M');
INSERT INTO characters (ID, LastName, FirstName, Gender)
             VALUES (5, 'Rubble', 'Barney', 'M');
INSERT INTO characters (ID, LastName, FirstName, Gender)
             VALUES (7, 'Flintstone', 'Wilma', 'F');
INSERT INTO characters (ID, LastName, FirstName, Gender)
             VALUES (9, 'Flintstone', 'Dino', 'M');
INSERT INTO characters (ID, LastName, FirstName, Gender)
             VALUES (4, 'Flintstone', 'Pebbles', 'F');
INSERT INTO characters (ID, LastName, FirstName, Gender)
             VALUES (1, 'Rubble', 'Betty', 'F');
INSERT INTO characters (ID, LastName, FirstName, Gender)
             VALUES (6, 'Rubble', 'Bam-Bam', 'M');
INSERT INTO characters (ID, LastName, FirstName, Gender)
             VALUES (8, 'Jetson', 'George', 'M');
#
SELECT * FROM characters;
#
EXPLAIN (SELECT DISTINCT LASTNAME from characters);
#
SELECT DISTINCT LASTNAME from characters;
#
# Cleanup
#
DROP TABLE characters;
# . . .and we're done.

注意,测试的内容只是创建一个表、插入一些数据,然后进行一些简单的选择的 SQL 命令。大多数测试比这要复杂一点,但是你会明白的。您创建您的测试来练习一些命令集(或数据处理)。注意前六行。前三行是注释行,它们以#符号开始。总是在文件的顶部用最简单的解释来记录你的测试,以表明测试在做什么。在测试体中使用注释来解释任何不容易理解的命令(例如,复杂的连接或用户定义的函数)。第四行和第六行很有趣,因为它们向测试套件发出命令。测试套件命令总是从前面有-的行开始。这些行指示测试套件暂时禁用然后启用来自服务器的任何警告消息。如果表格(字符)不存在,这是必要的。如果我让警告处于启用状态,在这种情况下测试将会失败,因为:

  • 服务器会发出警告,或者
  • 输出将与预期结果不符。

测试的一般布局应该在开始时包括一个清理部分,以删除由于测试失败而可能存在的任何表或视图。测试的主体应该包括完成测试所需的所有语句,测试的结尾应该包括清除测试中创建的任何表或视图的语句。

image 提示在编写自己的测试时,Oracle 要求您使用 ??、??、?? 等表名。和视图名称,如 v1、v2 或 v3 等。,以便您的测试表不会与现有的测试表冲突。

运行新测试

一旦创建了测试,您需要执行测试并创建预期结果的基线。执行以下命令,从 mysql-test 目录运行新创建的名为 cab.test 的测试:

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

第一个命令创建一个空的结果文件。这确保了测试套件有可以比较的东西。下一个命令第一次运行测试。清单 4-2 描述了一个典型的首次运行测试结果。注意,测试套件表明测试失败并生成了一个差异报告。这是因为没有可比较的结果。为了简洁起见,我省略了一些更普通的语句。

清单 4-2。 第一次运行新测试

Logging: ./mysql-test-run.pl  cab.test
120620  9:48:44 [Note] Plugin 'FEDERATED' is disabled.
120620  9:48:44 [Note] Binlog end
120620  9:48:44 [Note] Shutting down plugin 'CSV'
120620  9:48:44 [Note] Shutting down plugin 'MyISAM'
MySQL Version 5.6.6
Checking supported features. . .
 - skipping ndbcluster
 - SSL connections supported
 - binaries are debug compiled
Collecting tests. . .
Checking leftover processes. . .
Removing old var directory. . .
Creating var directory '/source/mysql-5.6/mysql-test/var'. . .
Installing system database. . .
Using server port 49261

==============================================================================

TEST                                      RESULT   TIME (ms) or COMMENT
--------------------------------------------------------------------------

main.cab                                 [ fail ]
        Test ended at 2012-06-20 09:48:49

CURRENT_TEST: main.cab
--- /source/mysql-5.6/mysql-test/r/cab.result       2012-06-20 16:48:40.000000000 +0300
+++ /source/mysql-5.6/mysql-test/r/cab.reject       2012-06-20 16:48:49.000000000 +0300
@@ -0,0 +1,52 @@
+SHOW DATABASES;
+Database
+information_schema

[. . .]

+DROP TABLE characters;

mysqltest: Result length mismatch

 - saving '/source/mysql-5.6/mysql-test/var/log/main.cab/' to '/source/mysql-5.6/mysql-test/var/log/main.cab/'
--------------------------------------------------------------------------
The servers were restarted 0 times
Spent 0.000 of 5 seconds executing testcases

Completed: Failed 1/1 tests, 0.00 % were successful.

Failing test(s): main.cab

The log files in var/log may give you some hint of what went wrong.

If you want to report this error, please read first the documentation
at http://dev.mysql.com/doc/mysql/en/mysql-test-suite.html

mysql-test-run: *** ERROR: there were failing test cases

差异报告显示了整个测试结果,因为我们的结果文件是空的。如果我们运行一个被修改的测试,差异报告将只显示那些不同的部分。这是一个非常强大的工具,每当开发者对代码进行更改时,他们就用它来运行回归测试。它向他们展示了他们的代码是如何影响系统操作的已知结果的。

下一个命令将 cab.reject 文件中的最新结果复制到 cab.result 文件中。只有当您确定测试正确运行并且没有意外错误时,才执行这一步。确保这一点的一种方法是手动运行测试语句,并验证它们是否正常工作。只有这样,您才能将剔除文件复制到结果文件中。清单 4-3 描述了新测试的结果文件。请注意,输出正是您期望从手动执行中看到的,减去了通常漂亮的打印输出和列间距。

清单 4-3。 结果文件

SHOW DATABASES;
Database
information_schema
mtr
mysql
performance_schema
test
CREATE TABLE characters (ID INTEGER PRIMARY KEY,
LastName varchar(40),
FirstName varchar(20),
Gender varchar(2)) ENGINE = MYISAM;
EXPLAIN characters;
Field       Type         Null             Key       Default       Extra
ID          int(11)      NO               PRI       NULL
LastName                 varchar(40)      YES       NULL
FirstName                varchar(20)      YES       NULL
Gender                   varchar(2)       YES       NULL
INSERT INTO characters (ID, LastName, FirstName, Gender)
VALUES (3, 'Flintstone', 'Fred', 'M');
INSERT INTO characters (ID, LastName, FirstName, Gender)
VALUES (5, 'Rubble', 'Barney', 'M');
INSERT INTO characters (ID, LastName, FirstName, Gender)
VALUES (7, 'Flintstone', 'Wilma', 'F');
INSERT INTO characters (ID, LastName, FirstName, Gender)
VALUES (9, 'Flintstone', 'Dino', 'M');
INSERT INTO characters (ID, LastName, FirstName, Gender)
VALUES (4, 'Flintstone', 'Pebbles', 'F');
INSERT INTO characters (ID, LastName, FirstName, Gender)
VALUES (1, 'Rubble', 'Betty', 'F');
INSERT INTO characters (ID, LastName, FirstName, Gender)
VALUES (6, 'Rubble', 'Bam-Bam', 'M');
INSERT INTO characters (ID, LastName, FirstName, Gender)
VALUES (8, 'Jetson', 'George', 'M');
SELECT * FROM characters;
ID      LastName       FirstName       Gender
3       Flintstone     Fred             M
5       Rubble         Barney           M
7       Flintstone     Wilma            F
9       Flintstone     Dino             M
4       Flintstone     Pebbles          F
1       Rubble         Betty            F
6       Rubble         Bam-Bam          M
8       Jetson         George           M
EXPLAIN (SELECT DISTINCT LASTNAME from characters);
id       select_type       table       type       possible_keys       key       key_len       ref       rows       Extra
1       SIMPLE       characters       ALL       NULL       NULL       NULL       NULL       8       Using temporary
SELECT DISTINCT LASTNAME from characters;
LASTNAME
Flintstone
Rubble
Jetson
DROP TABLE characters;

最后,我们使用预期的结果重新运行测试,测试套件报告测试通过了。清单 4-4 描述了一个典型的测试结果。

清单 4-4。 试运行成功

Logging: ./mysql-test-run.pl  cab.test
120620  9:53:19 [Note] Plugin 'FEDERATED' is disabled.
120620  9:53:19 [Note] Binlog end
120620  9:53:19 [Note] Shutting down plugin 'CSV'
120620  9:53:19 [Note] Shutting down plugin 'MyISAM'
MySQL Version 5.6.6
Checking supported features. . .
 - skipping ndbcluster
 - SSL connections supported
 - binaries are debug compiled
Collecting tests. . .
Checking leftover processes. . .
Removing old var directory. . .
Creating var directory '/source/mysql-5.6/mysql-test/var'. . .
Installing system database. . .
Using server port 49273

==============================================================================

TEST                                      RESULT   TIME (ms) or COMMENT
--------------------------------------------------------------------------
main.cab                                 [ pass ]     28
--------------------------------------------------------------------------
The servers were restarted 0 times
Spent 0.028 of 7 seconds executing testcases

Completed: All 1 tests were successful.

创建自己的测试并运行它们很容易。你可以重复我刚才描述的过程,想做多少次测试就做多少次。这个过程遵循测试驱动开发的精神,首先创建测试,在没有结果证明的情况下运行它,创建解决方案(预期的结果),然后执行测试并验证成功的测试完成。我鼓励您在创建自己的 MySQL 应用时,尤其是在扩展 MySQL 服务器时,采用同样的理念。

例如,假设您想要创建一个新的 SHOW 命令。在这种情况下,您应该创建一个新的测试来执行新命令,运行它,并建立测试结果。自然,测试每次都会失败,直到您实际创建新命令。好处是它允许您在实际编写代码之前关注命令的结果以及命令语法应该如何。如果您在所有的开发中采用这种实践,您将不会后悔,并且会看到代码质量的提高。一旦您实现了命令,并且通过再次运行测试并检查拒绝文件(或者手动运行命令)验证了它的工作,您就可以将拒绝文件复制到结果文件中,测试套件将在以后的测试运行中使用该结果文件进行验证(通过/失败)。

高级测试

MySQL 测试套件提供了一组丰富的命令,可以用来创建强大的测试。本节介绍一些更流行和有用的命令。不幸的是,没有全面的文档解释所有可用的命令。以下是我通过研究提供的测试和在线帖子找到的。

image 提示如果您使用高级测试套件命令,您可以使用- record 命令 -line 参数来创建结果文件,以记录正确的结果。例如,您可以运行命令。/mysql-test-run.pl - record cab 记录 cab 测试文件的结果。

如果您期望某个错误发生(假设您正在测试错误的存在,而不是没有检测到它们),您可以使用- error num 命令。这个命令告诉测试套件,您预期会出现指定的错误,并且当该错误出现时,它不应该使测试失败。该命令设计在产生错误的命令之前。您还可以指定附加的错误号,用逗号分隔。例如,- error 1550、1530 表示这些(虚构的)错误对于后面的命令是允许的。

您也可以在测试中使用控制代码流。例如,您可以使用一个循环来执行某件事情固定的次数。以下代码执行一个命令 100 次:

let $1 = 100;
while ($1)
{
  # Insert your commands here
  dec($1)
}

另一个有用的命令是睡眠。休眠命令将执行下一个命令前暂停的秒数作为参数。例如,- real_sleep 3.5 告诉测试套件在执行下一个命令之前暂停 3.5 秒。如果网络中存在意外的延迟,或者如果由于流量过大而导致测试失败,此命令会有所帮助。使用 sleep 命令将允许您减慢测试速度,从而减少由于外部因素导致的不良性能造成的任何干扰。

然而,使用 sleeps 通过克服服务器代码中的计时问题来使测试具有确定性是一种不良的实践。例如,插入休眠来测试从多个连接运行的查询是一个坏主意。鉴于多线程执行的性质,仅使用休眠来协调结果是不够的,尽管使用休眠可以使测试在一台机器上具有确定性(在这种情况下,连接 1 在连接 2 之前返回结果),但它可能无法在另一台机器上工作。

如果您有兴趣查看关于命令的附加信息,请使用- enable_metadata 命令。这将生成并显示内部元数据,这些元数据可以帮助您调试复杂测试的命令。同样,如果您想禁止记录输出,请使用- disable_result_log 关闭记录,使用- enable_result_log 重新打开记录。

如果您的命令会导致数据在运行之间发生变化(比如日期或时间字段),那么告诉测试套件忽略这些值,方法是使用- replace_column 列字符串命令替换另一个字符串。例如,如果您的输出在第二列中产生当前时间(列计数从 1 开始,而不是 0),您可以使用命令- replace_column 2 CURTIME。这告诉测试套件,下一个命令的输出是用字符串“CURTIME”替换列 2。虽然这确实抑制了输出中的实际值,但它提供了一种忽略那些无法预测的值的方法,因为它们在测试运行之间会发生变化。

最后,如果您需要在测试中包含额外的测试命令,您可以使用-source include/filetoinclude . Inc 来包含 mysql-test/include 目录中的文件。这种做法在用一组常用命令组成测试套件的测试中很典型。

报告错误

您可能会在运行某个测试或创建自己的测试时发现一个 bug。Oracle 欢迎对测试套件的反馈,并提供了报告错误的方法。然而,在你发送电子邮件并撰写一份详细的失败报告之前,一定要彻底确认这个错误。

Oracle 要求您自行运行测试,并找出失败背后的确切命令和错误代码。首先确定这些错误是否是您的环境造成的(请参阅 MySQL 参考手册中的“操作系统特定说明”以了解潜在问题;请访问http://dev.mysql.com/doc/refman/5.6/en/ installing.html了解更多详情)通过在全新安装或另一个已知良好的安装上运行测试。您还应该手动运行测试中的命令来确认错误和错误代码。有时,手动运行命令会揭示您无法通过其他方式获得的附加信息。在调试模式下运行服务器也可能有所帮助。最后,如果测试和错误条件可以重复,那么在提交错误报告时,将测试文件、测试结果、测试拒绝文件和任何测试数据都包含到 MySQL 中。

MySQL 基准测试

Oracle 提供了一个强大的基准测试工具,称为 MySQL 基准测试套件。这是 Perl 模块和脚本的集合,旨在测试系统保存性能指标的能力。基准测试套件附带了大多数二进制和源代码发行版,可以在 Windows 上运行。 7 当 MySQL 安装好后,你会在安装目录下的 sql-bench 目录中找到 run-all-tests 脚本。这些测试是在回归测试的意义上设计的,因为它们旨在记录系统在当前条件下的性能。基准测试套件也可以从 MySQL 开发者网站(http://dev.mysql.com)上单独下载,用于大多数操作系统。

image 注意您需要安装 dbi 和 DBD::mysql 模块来使用基准测试工具。你可以从 MySQL 下载页面http://dev.mysql.com/downloads/dbi.html下载这些。

像大多数基准测试工具一样,MySQL 基准测试套件最适合用于确定系统和环境变化的影响。基准测试套件与测试套件有些不同,因为基准测试套件能够针对其他系统运行基准测试——您可以使用基准测试套件针对 MySQL、Oracle 和 Microsoft SQL Server 安装运行相同的基准测试。可以想象,这样做可以帮助您确定 MySQL 在您的环境中的性能比您现有的数据库系统好多少。要针对其他服务器运行基准,请使用- server = 'server '命令行开关。该参数的值包括 MySQL、Oracle、Informix 和 MS-SQL。

许多命令行参数控制着基准测试套件。表 4-2 列出了几个流行的,并解释了每一个。有关命令行参数的更多信息,请参见 sql-bench 目录中的自述文件。

表 4-2 。MySQL 基准测试套件的命令行参数

命令行参数 说明
同-LOGUE 将基准测试的结果保存到文件中。与- dir 选项一起使用,指定存储结果的目录。使用 Unix 命令 uname–a 的相同输出来命名结果文件。
-用户 指定登录到服务器的用户。
-密码 指定用户登录服务器的密码。
主持人 指定服务器的主机名。
-小型测试 指定运行最小基准测试。省略这个参数会执行整个基准测试套件。对于大多数用途,小型测试足以确定更常见的性能指标。

要运行基准测试套件,只需导航到您的安装下的 sql-bench 目录并运行命令 run-all-tests。注意基准测试套件的一个重要特征:所有测试都是连续运行的。因此,测试是一次运行一个。要测试多个进程或线程的性能,请使用第三方基准测试套件,如 Sysbench 和 DBT2。

基准测试套件的另一个限制是它目前不可扩展。也就是说,没有为自己的应用创建自己的测试的工具。然而,源代码是免费分发的,所以精通 Perl 的人可以看一看。如果您创建了自己的测试,请确保与全球开发者社区共享它们。您永远不知道—有人可能需要您创建的测试。

SYSBENCH 和 DBT2

SysBench 和 DBT2 是基准测试套件,用于在负载下测试系统。它们在http://sourceforge.net/projects/sysbench/http://osdldbt.sourceforge.net/#dbt2可用。

image 提示为了获得最佳结果,在运行基准测试之前禁用 MySQL 查询缓存。发出命令 SET GLOBALS query_cache_size = 0,关闭查询缓存;在 MySQL 客户端界面中。这将允许您的基准记录查询的实际时间,而不是系统从缓存中检索查询所需的时间。您将获得更准确的系统性能读数。

如果基本的基准集是您所需要的,那么您可以运行命令 run-all-tests - small-test 并为基本的测试集生成结果。虽然运行所有的测试可以确保对性能进行更彻底的测量,但也可能需要很长时间才能完成。另一方面,如果您确定了您想要测量的系统的特定部分,您可以通过独立地执行测试来运行单独的测试。例如,要测试与服务器的连接,可以运行 test-connect 命令。表 4-3 列出了一些可用的独立测试。

表 4-3 。基准测试的部分列表

试验 描述
test-ATIS.sh 创建多个表格并对其进行若干选择
test-connect.sh 测试与服务器的连接速度
test-create.sh 测试创建表的速度
test-insert.sh 测试表的创建和填充操作
test-wisconsin.sh 运行该基准的 PostgreSQL 版本的一个端口

image 注意基准测试套件在单线程中运行测试。Oracle 计划在未来将多线程测试添加到基准测试套件中。

有关 MySQL 的其他基准测试形式的更多信息,请参见 Michael Kruckenberg 和 Jay Pipes 的 Pro MySQL8 它是 MySQL 所有东西的绝佳参考。

运行小型测试

让我们来看看在您的系统上运行基准测试工具时,您可以期待什么。在这个例子中,我在我的 Linux 系统上使用小测试运行了基准测试套件。清单 4-5 显示了生成的输出文件的顶部。

清单 4-5。 小测试基准摘录

$ ./run-all-tests --small-test --password = XXX --user = root
Benchmark DBD suite: 2.15
Date of test:        2012-06-20 10:39:38
Running tests on:    Linux 2.6.38-15-generic x86_64
Arguments:           --small-test
Comments:
Limits from:
Server version:      MySQL 5.1.63 0ubuntu0.11.04.1
Optimization:        None
Hardware:

alter-table: Total time:  1 wallclock secs ( 0.02 usr  0.01 sys +  0.00 cusr  0.00 csys =  0.03 CPU)
ATIS: Total time:  2 wallclock secs ( 0.75 usr  0.23 sys +  0.00 cusr  0.00 csys =  0.98 CPU)
big-tables: Total time:  0 wallclock secs ( 0.06 usr  0.01 sys +  0.00 cusr  0.00 csys =  0.07 CPU)
connect: Total time:  1 wallclock secs ( 0.39 usr  0.14 sys +  0.00 cusr  0.00 csys =  0.53 CPU)
create: Total time:  1 wallclock secs ( 0.02 usr  0.01 sys +  0.00 cusr  0.00 csys =  0.03 CPU)
insert: Total time:  3 wallclock secs ( 1.22 usr  0.24 sys +  0.00 cusr  0.00 csys =  1.46 CPU)
select: Total time:  3 wallclock secs ( 1.27 usr  0.21 sys +  0.00 cusr  0.00 csys =  1.48 CPU)
transactions: Test skipped because the database doesn't support transactions
wisconsin: Total time:  4 wallclock secs ( 1.40 usr  0.49 sys +  0.00 cusr  0.00 csys =  1.89 CPU)

All 9 test executed successfully

在清单的顶部,基准测试套件给出了描述测试运行的元数据,包括测试运行的日期、操作系统的版本、服务器的版本以及任何特殊的优化或安装的硬件(在本例中,没有)。看看元数据后面是什么。您可以看到每个测试运行的结果报告了挂钟运行的秒数。括号中显示的时间是在执行基准测试套件本身的过程中记录的时间,应该从实际的挂钟秒数中扣除,以获得准确的时间。不要太在意这一点,因为这一部分主要是用来简要查看分组测试的。下一部分是所有部分中最有趣的,因为它包含了每次测试中收集的实际数据。示例基准测试的结果如表 4-4 所示。注意有许多操作正在被测试。

表 4-4 。小型测试运行的具体测试结果数据(每次操作的总数)

image
image
image

在执行基准测试时,我喜欢将清单的后半部分转换成电子表格,这样我就可以对结果进行统计分析。这也允许我使用预期、之前和之后的结果来执行计算。表 4-4 显示了每次操作花费的总秒数、在基准测试工具(usr、sys、cpu)中花费的时间,以及每次操作运行的测试次数。

注意,在表 4-4 的底部,各列相加,给出了执行基准测试所花费的总时间。这些信息,加上清单 4-1 中的信息,形成了我的 Windows 系统当前的性能基线。我鼓励您为您的数据库服务器创建并归档您自己的基准。

运行单个测试

假设您对运行创建表的基准感兴趣。如表 4-3 所示,该测试被命名为 test-create。为了运行这个命令,我导航到 sql-bench 目录并输入命令 perl test-create。清单 4-6 显示了在我的 Windows 系统上运行这个命令的结果。

清单 4-6。 测试输出—创建基准测试

$ ./test-create --user = root --password = XXXX
Testing server 'MySQL 5.1.63 0ubuntu0.11.04.1' at 2012-06-20 10:40:59

Testing the speed of creating and dropping tables
Testing with 10000 tables and 10000 loop count

Testing create of tables
Time for create_MANY_tables (10000): 85 wallclock secs ( 0.87 usr  0.18 sys +  0.00 cusr  0.00 csys =  1.05 CPU)

Accessing tables
Time to select_group_when_MANY_tables (10000):  2 wallclock secs ( 0.22 usr  0.12 sys +  0.00 cusr  0.00 csys =  0.34 CPU)

Testing drop
Time for drop_table_when_MANY_tables (10000):  1 wallclock secs ( 0.18 usr  0.09 sys +  0.00 cusr  0.00 csys =  0.27 CPU)

Testing create + drop
Time for create + drop (10000): 83 wallclock secs ( 1.18 usr  0.50 sys +  0.00 cusr  0.00 csys =  1.68 CPU)
Time for create_key + drop (10000): 86 wallclock secs ( 1.45 usr  0.43 sys +  0.00 cusr  0.00 csys =  1.88 CPU)
Total time: 257 wallclock secs ( 3.90 usr  1.32 sys +  0.00 cusr  0.00 csys =  5.22 CPU)

清单 4-6 中,您可以看到每次测试运行所捕获的典型参数。请注意,该测试被设计为运行同一个测试的多次迭代。这是必要的,以确保计时不依赖于任何单个事件,并且在作为一个集合使用时有更多的意义。

我选择这个例子是为了让您可以考虑基准测试的另一种用途。假设您想要创建一个新的 CREATE SQL 命令。在这种情况下,您可以修改测试创建脚本来包含新命令的测试。然后,运行基准测试来建立新命令的基准性能。这是一个用于 MySQL 系统扩展的强大工具。如果您对您的扩展有任何性能甚至可伸缩性需求或顾虑,我鼓励您探索这个选项。

应用基准测试

在继续之前,我回到这个话题,因为理解和欣赏基准测试的好处是很重要的。基准测试对你有用的唯一方式是你将结果存档。我发现最好的解决方案是将结果保存在以基准测试日期命名的单独目录中。我建议将输出文件(来自- log 参数)以及系统和环境的当前配置的简短描述(使用您最喜欢的系统检查软件来完成)放入每组基准测试的单独目录中。

如果我需要将系统的性能与一个已知的状态进行比较——例如,每当我更改一个服务器变量并希望看到它对性能的影响时——我可以在更改前后运行基准测试工具。然后,我可以回顾基准测试的历史,并将这些结果与最稳定的状态进行比较。这种方法还允许我跟踪系统性能随时间的变化。

以这种方式使用的基准测试将使您能够在很少人能够达到的水平上管理您的系统。

MySQL 评测

尽管 MySQL server 工具套件(或源代码发行版)中没有包含正式的分析工具或套件,但是许多可用的诊断实用程序可以用作一组简单的分析技术。例如,您可以检查线程执行的状态,检查服务器日志,甚至检查优化器将如何执行查询。

要查看当前线程的列表,请使用 MySQL SHOW FULL PROCESSLIST 命令。该命令显示所有正在运行的当前进程或线程。运行它们的用户;发出连接的主机;正在使用的数据库;当前命令;执行时间;状态参数;和线程提供的附加信息。例如,如果我在我的系统上运行该命令,结果将类似于清单 4-7 中所示的内容。

清单 4-7。 显示完整进程列表命令的输出

mysql > SHOW FULL PROCESSLIST \G
*************************** 1\. row ***************************
     Id: 7
   User: root
   Host: localhost:1175
     db: test
Command: Query
   Time: 0
  State: NULL
   Info: SHOW FULL PROCESSLIST
1 row in set (0.00 sec)

该示例显示我是唯一从本地主机连接并运行的用户。该示例显示连接执行了一个执行时间为 0 的查询。该示例还显示了发出的命令。这个命令的缺点是它是一个即时快照,必须运行多次才能检测到性能瓶颈的模式。幸运的是,您可以使用一个名为 mytop 的工具,该工具反复调用该命令并显示几个有用的数据视图。欲了解更多信息或下载 mytop,请访问杰里米·扎沃德尼的网站(http://jeremy.zawodny.com/mysql/mytop)。

image 注意my top 应用在 Windows 平台上的成功有限。

显示服务器信息的另一个有用的命令是 SHOW STATUS。该命令显示所有服务器和状态变量。你可以想象,这是一个很长的列表。幸运的是,您可以通过向命令传递 LIKE 子句来限制显示。例如,要查看线程信息,请输入命令 SHOW STATUS,如“thread% ”;。清单 4-8 显示了这个命令的结果。

清单 4-8。 显示状态命令

mysql > SHOW STATUS LIKE "threads%";
+−−----------------- + −−-----+
| Variable_name     | Value |
+−−----------------- + −−-----+
| Threads_cached    | 0     |
| Threads_connected | 1     |
| Threads_created     | 6     |
| Threads_running     | 1     |
+−−----------------- + −−-----+
4 rows in set (0.00 sec)

要检查慢速查询日志,可以设置 log-slow-queries 变量,并使用 long-query-time 变量设置查询超时。长查询超时的典型值各不相同,但是应该根据您自己对长查询的理解来设置。要显示慢速查询,请使用 mysqldumpslow 命令来显示慢速查询。该命令根据相似性对慢速查询进行分组(也称为 s)。提供的其他元数据包括关于锁、预期的行和实际生成的行以及计时数据的信息。

可以使用 MySQL Workbench 软件检查一般的查询日志。如果您本地连接到服务器,则可以查看所有日志。如果你从未使用过 MySQLAdminstrator 软件,请从http://dev.mysql.com/downloads下载并尝试一下。

image 提示你可以使用 MySQL Workbench 软件来控制服务器的几乎每个方面,包括启动设置、日志记录和变量。

还可以使用 SHOW PROFILES 命令检查会话期间使用的资源。要使用此命令,必须打开性能分析,并使用性能分析会话变量进行控制。若要打开分析,请发出 SET PROFILING = ON 命令。

image 注意某些平台仅部分支持评测。不支持 getrusage()方法的平台返回 NULL。此外,分析是针对每个进程的,可能会受到其他线程上的活动的影响。

一旦启动了性能分析,发出 SHOW PROFILES 命令将显示当前会话中最近发出的 15 个查询的列表。您可以通过修改 profiling_history_size 变量的值来更改列表的大小。该列表将显示查询 id(顺序)、持续时间和命令。

要查看特定查询的细节,请使用 SHOW PROFILE FOR QUERY N 命令。这将显示查询执行时的状态和持续时间。您可以添加诸如块 IO、上下文切换、CPU 等选项。或全部显示更多细节。有关 SHOW PROFILE FOR QUERY N 命令选项的完整列表,请参见在线 MySQL 参考手册http://dev.mysql.com/doc/refman/5.6/en/show-profiles.html中的语法。清单 4-9 中显示了这些命令的使用示例。

清单 4-9。 显示轮廓和显示轮廓命令

mysql> SHOW VARIABLES LIKE 'profiling';
+−−------------- + −−-----+
| Variable_name | Value |
+−−------------- + −−-----+
| profiling     | OFF   |
+−−------------- + −−-----+
1 row in set (0.00 sec)

mysql> SET profiling = ON;
Query OK, 0 rows affected (0.00 sec)

mysql> SHOW VARIABLES LIKE 'profiling';
+−−------------- + −−-----+
| Variable_name | Value |
+−−------------- + −−-----+
| profiling     | ON    |
+−−------------- + −−-----+
1 row in set (0.00 sec)

mysql> use sakila;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> show full tables;
+−−-------------------------- + −−----------+
| Tables_in_sakila           | Table_type |
+−−-------------------------- + −−----------+
| actor                      | BASE TABLE |
| actor_info                 | VIEW       |
| address                    | BASE TABLE |
| category                   | BASE TABLE |
| city                       | BASE TABLE |
| country                    | BASE TABLE |
| customer                   | BASE TABLE |
| customer_list              | VIEW       |
| film                       | BASE TABLE |
| film_actor                 | BASE TABLE |
| film_category              | BASE TABLE |
| film_list                  | VIEW       |
| film_text                  | BASE TABLE |
| inventory                  | BASE TABLE |
| language                   | BASE TABLE |
| nicer_but_slower_film_list | VIEW       |
| payment                    | BASE TABLE |
| rental                     | BASE TABLE |
| sales_by_film_category     | VIEW       |
| sales_by_store             | VIEW       |
| staff                      | BASE TABLE |
| staff_list                 | VIEW       |
| store                      | BASE TABLE |
+−−-------------------------- + −−----------+
23 rows in set (0.00 sec)

mysql> select * from sales_by_store;
+−−------------------- + −−------------ + −−-----------+
| store               | manager      | total_sales |
+−−------------------- + −−------------ + −−-----------+
| Woodridge,Australia | Jon Stephens |    33726.77 |
| Lethbridge,Canada   | Mike Hillyer |    33679.79 |
+−−------------------- + −−------------ + −−-----------+
2 rows in set (0.05 sec)

mysql> show profiles;
+−−-------- + −−---------- + −−-------------------------------+
| Query_ID  | Duration    | Query                            |
+−−-------- + −−---------- + −−-------------------------------+
|        1  | 0.00039000  | SHOW VARIABLES LIKE 'profiling'  |
|        2  | 0.00008800  | SELECT DATABASE()                |
|        3  | 0.00023600  | show databases                   |
|        4  | 0.00020100  | show tables                      |
|        5  | 0.00078500  | show full tables                 |
|        6  | 0.04890200  | select * from sales_by_store     |
+−−-------- + −−---------- + −−-------------------------------+
6 rows in set (0.00 sec)

mysql> show profile for query 6;
+−−-------------------- + −−--------+
| Status               | Duration  |
+−−-------------------- + −−--------+
| starting             | 0.000034  |
| checking permissions | 0.000006  |
| Opening tables       | 0.000200  |
| checking permissions | 0.000004  |
| checking permissions | 0.000002  |
| checking permissions | 0.000001  |
| checking permissions | 0.000002  |
| checking permissions | 0.000001  |
| checking permissions | 0.000002  |
| checking permissions | 0.000001  |
| checking permissions | 0.000047  |
| init                 | 0.000011  |
| System lock          | 0.000010  |
| optimizing           | 0.000003  |
| optimizing           | 0.000016  |
| statistics           | 0.000064  |
| preparing            | 0.000020  |
| Creating tmp table   | 0.000013  |
| Sorting for group    | 0.000006  |
| Sorting result       | 0.000003  |
| statistics           | 0.000004  |
| preparing            | 0.000004  |
| executing            | 0.000007  |
| Sending data         | 0.000006  |
| executing            | 0.000002  |
| Sending data         | 0.048299  |
| Creating sort index  | 0.000033  |
| removing tmp table   | 0.000006  |
| Creating sort index  | 0.000010  |
| end                  | 0.000003  |
| query end            | 0.000006  |
| closing tables       | 0.000002  |
| removing tmp table   | 0.000003  |
| closing tables       | 0.000018  |
| freeing items        | 0.000038  |
| cleaning up          | 0.000015  |
+−−-------------------- + −−--------+
36 rows in set (0.00 sec)

mysql> show profile CPU for query 6;
+−−-------------------- + −−-------- + −−-------- + −−----------+
| Status               | Duration | CPU_user | CPU_system |
+−−-------------------- + −−-------- + −−-------- + −−----------+
| starting             | 0.000034 | 0.000030 |   0.000004 |
| checking permissions | 0.000006 | 0.000005 |   0.000001 |
| Opening tables       | 0.000200 | 0.000150 |   0.000050 |
| checking permissions | 0.000004 | 0.000001 |   0.000003 |
| checking permissions | 0.000002  | 0.000001  |   0.000000  |
| checking permissions | 0.000001  | 0.000001  |   0.000001  |
| checking permissions | 0.000002  | 0.000001  |   0.000001  |
| checking permissions | 0.000001  | 0.000001  |   0.000000  |
| checking permissions | 0.000002  | 0.000001  |   0.000001  |
| checking permissions | 0.000001  | 0.000000  |   0.000001  |
| checking permissions | 0.000047  | 0.000047  |   0.000001  |
| init                 | 0.000011  | 0.000009  |   0.000001  |
| System lock          | 0.000010  | 0.000010  |   0.000001  |
| optimizing           | 0.000003  | 0.000001  |   0.000001  |
| optimizing           | 0.000016  | 0.000015  |   0.000000  |
| statistics           | 0.000064  | 0.000059  |   0.000006  |
| preparing            | 0.000020  | 0.000018  |   0.000002  |
| Creating tmp table   | 0.000013  | 0.000012  |   0.000001  |
| Sorting for group    | 0.000006  | 0.000005  |   0.000001  |
| Sorting result       | 0.000003  | 0.000002  |   0.000001  |
| statistics           | 0.000004  | 0.000004  |   0.000001  |
| preparing            | 0.000004  | 0.000003  |   0.000000  |
| executing            | 0.000007  | 0.000006  |   0.000001  |
| Sending data         | 0.000006  | 0.000005  |   0.000001  |
| executing            | 0.000002  | 0.000001  |   0.000001  |
| Sending data         | 0.048299  | 0.048286  |   0.000025  |
| Creating sort index  | 0.000033  | 0.000027  |   0.000006  |
| removing tmp table   | 0.000006  | 0.000005  |   0.000001  |
| Creating sort index  | 0.000010  | 0.000010  |   0.000000  |
| end                  | 0.000003  | 0.000002  |   0.000001  |
| query end            | 0.000006  | 0.000005  |   0.000001  |
| closing tables       | 0.000002  | 0.000001  |   0.000000  |
| removing tmp table   | 0.000003  | 0.000003  |   0.000001  |
| closing tables       | 0.000018  | 0.000018  |   0.000001  |
| freeing items        | 0.000038  | 0.000012  |   0.000025     |
| cleaning up          | 0.000015  | 0.000013  |   0.000001  |
+−−-------------------- + −−-------- + −−-------- + −−----------+
36 rows in set (0.00 sec)

mysql>

MySQL 系统中包含的最后一项分析技术是检查优化器如何执行查询的能力。虽然严格来说它不是一个性能测量设备,但它可以用来诊断慢速查询日志中出现的棘手查询。举个简单的例子,让我们看看优化器对以下查询将如何执行的预测:

select * from customer where phone like "%575 %"

这个查询不是很有趣,使用 LIKE 子句并在值周围使用%s 效率不高,而且几乎肯定会导致无索引的访问方法。如果您运行前面带有 EXPLAIN 关键字的命令,您将看到建议的查询优化的结果。清单 4-10 显示了使用解释命令的结果。

清单 4-10。 输出解释命令

mysql> explain select * from customer where email like "%575 %" \G
*************************** 1\. row ***************************
           id: 1
  select_type: SIMPLE
        table: customer
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 541
        Extra: Using where
1 row in set (0.00 sec)

输出显示该命令是对 customer 表的简单选择,没有可能使用的键,表中有 541 行,并且优化器正在使用 WHERE 子句。在这种情况下,它告诉我们执行将是一个没有索引的简单的表扫描——可能是最慢的 select 语句之一。

摘要

在这一章中,我介绍了一些软件测试技术和策略。您了解了软件测试的好处,以及如何在您的软件项目中利用测试驱动开发。我还展示了您可以用来测试 MySQL 的测试工具。我向您展示了 MySQL 测试和基准测试套件,并向您介绍了 MySQL 的分析脚本。

这些测试设施的知识将使你做好准备,以确保你对 MySQL 源代码的修改是最高质量的。有了这些知识,您现在就可以开始创建 MySQL 系统的扩展和增强,以满足 Oracle 所坚持的高质量标准。现在你已经有了这些信息,你可以开始设计你的解决方案,并在设计的早期就进行测试。

下一章是本书第二部分的开始,它将向您介绍开发者工具箱中最重要的工具:调试!

1 通常包括从需求获取开始的大型团队项目。

尤其是当我宣布下一个项目时,团队会将他们的项目交给其他团队进行软件测试。令人惊讶的是,他们在其他学生的代码中发现了许多缺陷,却坚持认为自己的代码更好。

3 是的,这有点二分法,考虑敏捷实践是为了减少不必要的工作。

4 D .罗森伯格,m .斯蒂芬斯,m .科林斯-柯普。使用 ICONIX 过程的敏捷开发(加州伯克利:Apress,2005)。

一些统计学家认为微软 Excel 中的统计引擎不准确。但是,对于您可能看到的值,不准确不是问题。

6 关于软件测试的更多信息,参见http://en.wikipedia.org/wiki/Software_testing

7 需要 ActivePerl,Windows 的官方 Perl 发行版。请参见http://www.activestate.org了解详细信息并下载最新版本。

8 M .克鲁肯伯格和 j .皮波斯。 Pro MySQL (加州柏克莱:Apress,2005)。

除此之外,他们为什么会创造并向您提供测试、基准测试和剖析工具呢?

五、排除故障

本章讨论了开发者可以使用的最强大的工具之一:调试。良好的调试技能有助于确保您的软件项目易于开发并且不容易失败。我还将探索调试 MySQL 系统的最常见技术。如果您已经掌握了扎实的调试技能,请随意浏览以下章节,然后继续阅读“调试 MySQL”一节

调试说明

任何一个写过比“Hello world”程序更充实的东西的人都会在他的软件中遇到缺陷(bug)。虽然大多数缺陷很容易被发现,但是其他的缺陷可能很难定位和纠正。

如果你想向一个开发新手解释调试的概念,你可能会告诉她这很大程度上是一个故障排除的过程,以努力发现哪里出错了。您可能还注意到,开发良好的调试技能来自于掌握适当的调试技术和工具。虽然这可能是一个足够的介绍性定义,但请花时间更好地理解调试的细微差别。

对于初学者来说,恰当地确定您试图定位和纠正的缺陷类型是很重要的。缺陷有两种基本类型:语法错误逻辑错误 。在代码编译过程中自然会发现语法错误,尽管它们也可能很难纠正,但我们必须纠正它们来构建软件。逻辑错误是那些在编译期间没有发现的错误类型,因此,它们通常在软件执行期间表现为缺陷。因此,调试是在你的程序中发现并修复错误的行为。

image 注意可以在编译时(或更早)运行的可用工具有助于最小化逻辑错误的风险。从检测死代码的简单流控制分析器到更复杂的范围和类型检查器,检查您的代码以定位可能的数据不匹配。其他工具使用代码强化的最佳实践来检查错误处理是否正确。

当发现一个逻辑错误时,系统通常会做一些奇怪的事情或者产生错误的数据。在更极端的情况下,系统可能会崩溃。包含代码强化最佳实践的结构良好的系统往往比其他系统更健壮,因为它们被设计为在错误发生时捕获和处理错误。即使这样,一些错误也非常严重,以至于系统崩溃(或者操作系统终止它)以保护数据和系统状态。

调试软件的艺术在于快速定位错误的能力,要么通过观察系统的状态变化,要么通过直接检查代码和数据。我们把用来调试系统的工具叫做 。在接下来的小节中,我将研究一些常见的调试技术和相关的调试器。

调试的起源

毫无疑问,你已经听说过关于“电脑虫”这个术语是如何产生的故事,我想讲一个我最喜欢的故事。我很高兴能在少将·格蕾丝·赫柏发现第一个电脑病毒的地方附近工作。传说 1945 年,霍普正在使用一台叫做马克 2 号·艾肯接力计算器的大型计算计算机。今天称它为大型计算机有点夸张,但当时它只有半挂车那么大。当一个棘手的电子问题被追溯到一个故障继电器,其中有一只蛾子被困在里面时,Hopper 指出错误的来源是一个“错误”,系统已经被“调试”过,现在可以运行了。直到今天,我们还把去除有缺陷的代码称为调试。

调试技术

几乎有多少开发者,就有多少调试技术。似乎每个人调试代码的方式都略有不同。然而,这些方法通常可以分为几类。

这些方法中最基本的都包含在源代码中,成为可执行文件的一部分。这些包括内联调试语句(在执行期间打印消息或变量值的语句(例如printf("Code is at line 199\. my_var = %d\n", my_var);)和错误处理程序。大多数开发者要么将这些技术作为最后的手段(当缺陷不容易被发现时),要么在开发阶段(在编写代码时测试代码)使用这些技术。虽然您可能认为错误处理程序与健壮性和强化性的关系比与调试的关系更大,但是它们也可以是强大的调试工具。因为这种方法将调试代码嵌入到程序中,所以在调试完成时,您可以使用条件编译指令来省略代码。大多数开发者将调试语句留在代码中,因此,它们成为程序的一部分。当使用这种技术时,注意确保添加的调试代码不会对程序产生负面影响。

大多数人最熟悉的调试技术是使用外部调试器。外部调试器 是设计用于实时监控系统或允许您观察代码执行情况的工具,您可以随时停止和启动代码。这些技术将在下面的章节中详细描述。不过,首先让我们看看调试的基本过程。

基本过程

每个调试会话都是独特的,但是这个过程应该总是遵循相同的基本步骤。在调试过程中保持一致有助于使体验更有效、更有价值。在追逐了几个小时之后,没有比碾碎一只特别讨厌的虫子更好的感觉了。虽然您可能早就建立了一个首选的调试方法,但它很可能至少包含以下步骤:

  1. 识别缺陷(错误报告,测试)。
  2. 重现缺陷。
  3. 创建一个测试来确认缺陷。
  4. 隔离缺陷的原因。
  5. 创建一个修正补丁并应用它。
  6. 运行测试以确认缺陷已修复:是—继续,否—返回 4。
  7. 运行回归测试以确认该修补程序不会影响系统的其他部分。

识别缺陷有时会很困难。当面对缺陷报告时,无论是官方的 bug 报告还是失败的系统测试,您可能会认为缺陷是伪造的,尤其是当缺陷不明显的时候。那些导致系统崩溃或损坏数据的缺陷自然会马上引起你的注意。但是那些偶尔发生或者只在特定条件下发生的呢?对于这些,你必须首先假设缺陷存在。

如果您足够幸运地拥有一份完整的 bug 报告,其中包含了如何重新创建缺陷的描述,那么您可以从缺陷中创建一个测试,并运行它来确认缺陷的存在。如果你没有一个完整的关于如何重现缺陷的描述,这可能需要一些努力。

一旦您能够重现缺陷,创建一个包含重现问题的所有步骤的测试。当您需要确认您已经解决了问题时,这个测试将非常重要。

下一步是真正调试的开始:隔离缺陷。此时,您必须使用本章中讨论的一种或多种技术来隔离和诊断缺陷的原因。这是调试软件最重要也是最具挑战性的方面。

为缺陷创建一个补丁(有时称为差异修复)通常是一个迭代过程,很像编码本身。你应该一步一步地应用你的修正。一次做一个改变,并测试它对缺陷和系统其余部分的影响。当您认为您有一个可行的补丁时,重新运行您的缺陷测试来确认它。如果您已经更正了问题,测试将会失败。提醒一下,一个旨在发现缺陷的测试如果没有发现缺陷,将被认为是一个失败的测试——但这正是您想要的!如果测试通过,返回到检查和修复,重复迭代直到您的缺陷测试失败。

创建和使用修补程序

一种鲜为人知的软件开发技术被称为补丁。补丁只是一个包含原始文件及其修改形式之间差异的文件。当您创建一个补丁时,您运行一个名为diff的 GNU 程序,并将输出保存到一个文件中。(你可以在www.gnu.org/software/diffutils/diffutils.html找到diff。不幸的是,该代码仅适用于 Linux 和 Unix,但可以在使用 Cygwin 的 Windows 上运行。)例如,如果您正在修改mysqld文件,并添加了一行代码来更改版本号,您可以通过运行命令diff -Naur mysqld.cc.old mysqld.cc > mysqld.patch为代码更改创建一个补丁。这将创建一个如下所示的文件:

--- mysqld.cc.old  2006-08-19 15:41:09.000000000 -0400
+++ mysqld.cc  2006-08-19 15:41:30.000000000 -0400
@@ -7906,6 +7906,11 @@
#endif
   if (opt_log || opt_update_log || opt_slow_log || opt_bin_log)
     strmov(end, "-log");                        // This may slow down system
+/* BEGIN DBXP MODIFICATION */
+/* Reason for Modification: */
+/* This section adds the DBXP version number to the MySQL version number. */
+  strmov(end, "-DBXP 1.0");
+/* END DBXP MODIFICATION */
}

当您想要为整个文件列表或整个目录创建一个差异文件时,您也可以使用diff。然后,您可以使用生成的文件在其他地方修补文件的另一个安装。

当你使用补丁的时候,你使用的是名为patch的 GNU 程序。(你可以在www.gnu.org/software/patch/找到patch。不幸的是,代码仍然只适用于 Linux 和 Unix,但是可以在使用 Cygwin 的 Windows 上运行。)程序patchdiff程序中读取补丁文件,并将其应用于补丁顶部指定的文件。例如,要修补一个没有您用diff创建的更改的mysqld.cc文件,您可以运行命令patch < mysqld.patchpatch程序将更改应用到mysqld.cc文件,并将更改合并到文件中。

创建和应用补丁是向文件分发小更改的一种便捷方式——例如在修复缺陷时遇到的那些。每当您修复一个 bug 时,您可以创建一个补丁,并使用它来跟踪和应用相同的更改到旧文件。

许多开源项目使用补丁概念来传达变更。事实上,补丁是全球开发者社区对 MySQL 源代码进行修改的主要方式。开发者可以向 Oracle 发送一个补丁,而不是上传整个文件。从那里,Oracle 可以检查补丁的正确性,或者接受更改(并应用补丁)或者拒绝更改。如果您从未使用过diffpatch程序,请随意下载它们,并在您完成示例时试用它们。

最后,当缺陷被修复后,您应该执行一个回归测试步骤来确认没有其他缺陷被引入。如果您有幸工作在一个使用组件或模块化架构构建的系统上,并且该系统有很好的文档记录,那么您可以通过检查需求矩阵轻松地识别相关的组件或模块。一个需求矩阵跟踪来自用例、类和序列图的需求,并识别为每个需求创建的测试。因此,当一个类(模块)的一部分发生变化时,您可以很容易地找到您需要为您的回归测试运行的测试集。如果您没有需求矩阵,您可以使用一个简单的文档或电子表格来创建一个,或者用它们所满足的需求来注释源代码文件。

调试方法

您可以采用各种方法进行调试。这些包括简单地显示或打印感兴趣的值,或者使用交互式调试器跟踪可疑的代码部分。您甚至可以提前在代码中添加特殊的命令,以便于以后的调试——这种做法被称为插装

内联调试语句

大多数开发新手开始在代码中放置 print 语句。这是测试变量的一种常见形式,允许他们学习编程的艺术。你可能认为任何使用内联调试语句的调试技术都是初级的或者麻烦的,你可能部分正确。内联调试语句很麻烦,但却是一个强大的工具。内联调试语句是用于记录或呈现系统在某个时间点的数据或状态的任何代码。

在我给出一个内联调试语句的例子之前,让我们考虑一下使用它们的影响。首先想到的是调试语句是代码!因此,如果调试语句除了写入标准错误流(窗口)之外还做了其他事情,可能会导致进一步的意外后果。还应该注意,在构建系统之前,内联调试语句通常被去除或忽略(使用条件编译)。如果你是一个可靠的确认和验证支持者,你会认为这个过程引入了额外的不必要的风险。也就是说,为使用而编译的系统不同于用于调试的系统。

然而,内联调试语句在无法使用外部调试器或者缺陷似乎以随机间隔出现的情况下会很有帮助。例如,在实时系统、多进程和多线程系统以及处理大量数据的大型系统中,可能会出现这些情况。

使用仪器

许多开发者认为内联调试语句是一种插装的形式。这包括旨在跟踪性能、数据、用户、客户端和执行指标的代码。插装通常通过在代码中放置语句来显示数据值、警告、错误等来实现,但也可以使用在类似沙箱的环境中监控执行的包装代码来实现。软件工具套件一个例子是由英特尔公司生产的。有关软件工具和引脚的更多信息,参见http://www.pintool.org/

有两种类型的内联调试语句。第一个与检验有关。添加代码行来表示内存的状态或变量的值。这种类型的调试语句在开发过程中使用,通常使用条件编译注释掉或忽略。第二个问题是在系统执行时跟踪它的路径。这种类型的调试语句可以在任何时候使用,通常在运行时由开关启用或禁用。由于大多数开发者都熟悉第一种类型(我们大多数人都是这样学习调试的),所以我将用一个例子来讨论第二种类型。

假设您有一个运行在多线程模型中的大型系统,并且您试图确定是什么导致了缺陷。使用表示内存和变量值的内联调试语句可能会有所帮助,但缺陷很难被轻易发现。在这种情况下,您可能需要发现导致缺陷的系统状态。如果您的系统中有这样的代码,每当它进入一个函数时只写一个日志条目,当它离开时写另一个日志条目(可能带有一些关于数据的附加信息),那么您可以通过检查日志来确定系统处于什么状态。清单 5-1 描述了 MySQL 源代码 的摘录,其中包括内联调试语句。我用粗体突出显示了调试代码。在这种情况下,每个内联调试语句都会在跟踪文件中写入一个条目,在系统执行(或崩溃)后可以对其进行检查。

清单 5-1。 内联调试语句示例

/***************************************************************************
** List all Authors.
** If you can update it, you get to be in it :)
***************************************************************************/

bool mysqld_show_authors(THD *thd)
{
  List<Item> field_list;
  Protocol *protocol= thd->protocol;

  DBUG_ENTER("mysqld_show_authors");

  field_list.push_back(new Item_empty_string("Name",40));
  field_list.push_back(new Item_empty_string("Location",40));
  field_list.push_back(new Item_empty_string("Comment",80));

  if (protocol->send_result_set_metadata(&field_list,
                            Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF))
    DBUG_RETURN(TRUE);

  show_table_authors_st *authors;
  for (authors= show_table_authors; authors->name; authors++)
  {
    protocol->prepare_for_resend();
    protocol->store(authors->name, system_charset_info);
    protocol->store(authors->location, system_charset_info);
    protocol->store(authors->comment, system_charset_info);
    if (protocol->write())

     DBUG_RETURN(TRUE);
  }
  my_eof(thd);

  DBUG_RETURN(FALSE);
}

注意在清单 5-1 中,第一个内联调试语句代码通过指出函数的名字来记录系统到达这个函数或它的状态。还要注意,函数的每个退出点都与函数的返回值一起被记录下来。运行SHOW AUTHORS命令的跟踪文件摘录如清单 5-2 所示。为了向您展示跟踪文件如何成功执行SHOW AUTHORS命令,我省略了清单中的一大部分。

清单 5-2。 样本痕迹文件

T@3    : | | | | >mysqld_show_authors

...

T@3    : | | | | | >send_result_set_metadata
T@3    : | | | | | | packet_header: Memory: 0x7f889025c610  Bytes: (4)
01 00 00 01
T@3    : | | | | | | >alloc_root
T@3    : | | | | | | | enter: root: 0x270af88
T@3    : | | | | | | | exit: ptr: 0x287f9c0
T@3    : | | | | | | <alloc_root 247
T@3    : | | | | | | >Protocol::write
T@3    : | | | | | | <Protocol::write 820
T@3    : | | | | | | packet_header: Memory: 0x7f889025c5c0  Bytes: (4)
1A 00 00 02
T@3    : | | | | | | >Protocol::write
T@3    : | | | | | | <Protocol::write 820
T@3    : | | | | | | packet_header: Memory: 0x7f889025c5c0  Bytes: (4)
1E 00 00 03
T@3    : | | | | | | >Protocol::write
T@3    : | | | | | | <Protocol::write 820
T@3    : | | | | | | packet_header: Memory: 0x7f889025c5c0  Bytes: (4)
1D 00 00 04
T@3    : | | | | | | packet_header: Memory: 0x7f889025c5b0  Bytes: (4)
05 00 00 05
T@3    : | | | | | <send_result_set_metadata 807
T@3    : | | | | | info: Protocol_text::store field 0 (3): Brian (Krow) Aker
T@3    : | | | | | info: Protocol_text::store field 1 (3): Seattle, WA, USA
T@3    : | | | | | info: Protocol_text::store field 2 (3): Architecture, archive, federated, bunch of little stuff :)
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f889025ca70  Bytes: (4)
5E 00 00 06
T@3    : | | | | | info: Protocol_text::store field 0 (3): Marc Alff
T@3    : | | | | | info: Protocol_text::store field 1 (3): Denver, CO, USA
T@3    : | | | | | info: Protocol_text::store field 2 (3): Signal, Resignal, Performance schema
T@3    : | | | | | >Protocol::write

...

47 00 00 55
T@3    : | | | | | info: Protocol_text::store field 0 (3): Peter Zaitsev
T@3    : | | | | | info: Protocol_text::store field 1 (3): Tacoma, WA, USA
T@3    : | | | | | info: Protocol_text::store field 2 (3): SHA1(), AES_ENCRYPT(), AES_DECRYPT(), bug fixing
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f889025ca70  Bytes: (4)
4F 00 00 56
T@3    : | | | | | >set_eof_status
T@3    : | | | | | <set_eof_status 483
T@3    : | | | | <mysqld_show_authors 279

image 注意这些内联调试语句默认是关闭的。您可以通过使用debug编译服务器并使用- debug命令行开关在调试模式下运行服务器来打开它们。这将创建一个包含所有调试语句的跟踪文件。在 Linux 上,跟踪文件存储在/tmp/mysqld.trace中,在 Windows 上,文件存储在c:\mysqld.trace中。这些文件可能会变得很大,因为 MySQL 中的所有函数都是使用内联调试语句编写的。

这种技术虽然简单,但却是一种多用途的工具。当您通过检查跟踪文件来检查系统的流程时,您可以很容易地发现进一步调查的起点。有时候,仅仅知道去哪里找可能是最大的挑战。

错误处理程序

您在使用软件时遇到过错误信息吗?无论您使用的是太平洋西北地区开发的东西还是全球开发者社区开发的东西,您都有可能看到错误处理程序的最终结果。

您可能想知道为什么我会将错误处理程序作为一种调试技术。这是因为一个好的错误处理程序会给出问题的原因以及任何可能的纠正选项。好的错误处理程序为开发者提供了足够的信息,让他们了解哪里出错了,以及如何解决问题,在某些情况下,它们还包括可以帮助开发者诊断问题的附加信息。最后一点有时会走得太远。我们很多人都见过包含简洁错误信息的对话框,带有令人困惑的解决方案选项,例如图 5-1 所示的对话框。

9781430246596_Fig05-01.jpg

图 5-1。糟糕的错误处理程序示例

尽管这个例子很幽默,但是用户每天都会看到类似的消息。像这样写错误消息的开发者没有把自己说清楚。系统开发者完全可以理解的语句对用户来说可能是胡言乱语。最好的策略是创建错误消息,解释哪里出错了,并为用户提供一个解决方案(如果有的话),或者至少提供一种报告问题的方法。提供一种方法来记录开发者诊断问题所需的信息也是一个好主意。这可以通过日志记录、系统状态转储或自动生成的报告来完成。图 5-2 描述了如何向用户显示错误的一个更好的例子。

9781430246596_Fig05-02.jpg

图 5-2。更好的错误处理器示例

错误处理程序不仅仅用于报告错误。还有另一种构造也称为错误处理程序。这个构造只是用来捕获和处理(处理)错误的代码。也许你熟悉 C++ try...catch块。这是错误处理程序的一个很好的例子,因为该语言已经被修改为在其语法中包含该构造。清单 5-3 描述了一个典型的try...catch程序块。该示例显示了 C++错误处理程序(也称为异常处理程序)的基本语法。

清单 5-3。 示例 C++错误处理程序 try。。。捕捉块

try
{
  //attempt file operation here
}
catch (CFileException* e)
{
  //handle the exception here
}

虽然清单 5-3 没有 C++构造复杂,但是您可以用任何支持条件语句的语言创建错误处理程序。例如,清单 5-4 展示了一个来自 C 语言的例子。在这里,我们看到返回代码被检查,根据失败情况,代码处理问题。从头开始创建错误处理程序时要小心。您希望确保涵盖所有可能的情况,以便能够成功地恢复或至少以不影响系统性能和(更重要的)数据丢失或损坏的方式处理错误。

清单 5-4。 例子 C 错误处理程序

if (!(azopen(&frm_stream, az_file, O_RDONLY|O_BINARY)))
{
  if (errno == EROFS || errno == EACCES)
    DBUG_RETURN(my_errno= errno);
  DBUG_RETURN(HA_ERR_CRASHED_ON_USAGE);
}

错误处理程序不仅仅是报告错误。它们也是调试的第一道防线。编写好的错误处理程序不仅可以捕获和处理错误,还可以存储或显示诊断信息。

再看一下清单 5-4 中的。这段代码取自 MySQL 源代码的ha_archive.cc文件。请注意突出显示的代码行。这一行是整个代码中众多内联调试语句中的一个,但是它在这个错误处理程序中的使用说明了如何记录对系统的这一部分进行故障排除所必需的诊断信息。如果我正在调试关于此代码的会话,我可以在调试模式下运行服务器,并查看跟踪文件以读取此错误处理程序记录的诊断信息。

我鼓励您考虑以这种方式编写所有的错误处理代码。您可以始终向用户显示适当的错误消息,但也应该始终捕获错误代码(返回值)并记录它们以及任何其他相关的诊断信息。以这种方式使用错误处理程序将大大提高您的调试技能,并使您的系统更容易诊断。我发现有时我甚至根本不需要运行调试器。对包含诊断信息的跟踪文件的研究足以让我直接找到问题的根源。

外部调试器

调试器是一种软件工具,用于分析一组正在执行的代码,并跟踪系统执行时的流程。大多数我们认为是调试器的工具实际上是与被调试的软件一起执行的,因此得名外部调试器。为了简洁和一致,我将把本节中讨论的所有工具简称为调试器。

有几种类型的调试器,但大多数适合三个类别之一。您可能最熟悉的调试器是那些作为独立工具运行的调试器,您可以将它们附加到正在运行的进程并用来控制系统。还有一些调试器被设计成作为一个交互过程运行,将控制和检查功能结合起来。其他包括专门的调试器,提供更高级的系统控制。我将在接下来的小节中研究每一种类型。

独立的调试器

最常见的调试器是独立调试器。它们作为一个单独的进程运行,并允许您附加到一个已经编译为包含适当调试信息的系统(用于映射到源代码,尤其是链接到代码中的符号)。除非您正在调试包含源文件的代码(比如某些形式的解释语言),否则您通常必须拥有可用的源代码文件,并使用它们来完成与正在运行的进程的连接。

一旦您连接到想要调试的系统(或进程),独立调试器允许您停止、启动和逐步执行。单步执行涉及三个基本操作:

  1. 执行当前代码行并进入下一行代码。
  2. 跳过下一行代码(执行函数调用并返回到下一行)。
  3. 执行,直到特定的代码行成为焦点。

最后一个操作通常是指被标记为要停止的代码行(称为断点)或当前突出显示的代码行(称为运行到光标处)。

独立调试器提供了检查内存、调用堆栈甚至有时检查堆的工具。检查变量的能力可能是调试器所能提供的最重要的诊断工具。毕竟,几乎所有您想要检查的东西都存储在某个地方。

image A 是一种以树形结构存储可用内存地址的结构,用于内存块的快速分配和解除分配。一个是一个允许开发者以先进后出的方式将项目放在栈上的结构(很像自助餐的一堆盘子)。

独立调试器通常不与开发环境集成。也就是说,它们不是编译器工具套件的一部分。因此,许多在开发环境之外运行。使用独立调试器的优势在于有很多调试器可供选择,每一个都有稍微不同的特性集。这允许您选择最符合您需求的独立调试器。

这种调试器的一个流行例子是 GNU 调试器 ( gdb)。(欲了解更多信息,请访问www.gnu.org/software/gdb/documentation。)Linux 上的gdb调试器提供了一种方法来控制和检查在调试模式下编译的系统。清单 5-5 显示了我编写的计算阶乘的示例程序。眼尖的人会发现这个逻辑错误,但是让我们假设程序是按照编写的那样运行的。当我输入值 3 时,我应该得到返回值 6。相反,我得到 18。

清单 5-5 。示例程序(sample.c)

#include <stdio.h>
#include <stdlib.h>

static int factorial(int num)
{
  int i;
  int fact = num;

  for (i = 1; i < num; i++)
  {
    fact += fact * i;
  }
  return fact;
}

int main(int argc, char *argv[])
{
  int num;
  int fact = 0;

  num = atoi(argv[1]);
  fact = factorial(num);
  printf("%d! = %d\n", num, fact);
  return 0;
}

如果我想使用gdb调试这个程序,我首先必须在调试模式下使用以下命令编译程序:

gcc -g -o sample sample.c

一旦程序被编译,我使用命令启动gdb:

gdb sample

gdb调试器发出它的命令提示符时,我使用break命令发出断点(为断点提供源文件和行号)并运行程序,提供必要的数据。我也可以使用print命令打印出任何变量。如果我想继续执行,我可以发出continue命令。最后,完成后,我可以用quit命令退出gdb。清单 5-6 显示了使用这些命令的调试会话示例。

清单 5-6。 样本广发会话

cbell@ubuntu:∼/source/sample$ gcc -g -o sample sample.c
cbell@ubuntu:∼/source/sample$ gdb sample
GNU gdb (Ubuntu/Linaro 7.2-1ubuntu11) 7.2
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later < http://gnu.org/licenses/gpl.html >
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
For bug reporting instructions, please see:
< http://www.gnu.org/software/gdb/bugs/ >...
Reading symbols from /home/cbell/source/sample/sample...done.
(gdb) break sample.c:10
Breakpoint 1 at 0x40055a: file sample.c, line 10.
(gdb) run 3
Starting program: /home/cbell/source/sample/sample 3

Breakpoint 1, factorial (num=3) at sample.c:11
11           fact += fact * i;
(gdb) print i
$1 = 1
(gdb) print num
$2 = 3
(gdb) print fact
$3 = 3
(gdb) continue
Continuing.

Breakpoint 1, factorial (num=3) at sample.c:11
11           fact += fact * i;
(gdb) continue
Continuing.
3! = 18

Program exited normally.
(gdb) quit
cbell@ubuntu:

你看到逻辑错误了吗?我给你一个提示。计算数字 3 的阶乘时,第一个值应该是什么?看看factorial方法的变量声明。那个int fact = num;声明有点味道。

image 注意有些人可能想调用调试器,如gdb交互式调试器,因为它们在系统运行时与系统交互,从而允许用户观察执行过程。虽然这是真的,但请记住gdb是在外部控制系统,除了通过非常简单的方法(例如,列表命令list,列出源代码),您无法看到源代码或与源代码进行交互。如果gdb提供一个图形用户界面来呈现源代码,并允许您查看数据和与源代码交互,它将是一个交互式调试器。但是等等,这就是ddd调试器所做的。

交互式 调试器

这些调试器是开发环境的一部分,或者作为编译-链接-运行工具的一部分,或者作为交互式开发环境的一个集成部分。与独立调试器不同,交互式调试器使用与开发工具相同或非常相似的接口。集成良好的交互式调试器的一个很好的例子是 Microsoft Visual Studio 中的调试工具。网。在 Visual Studio 中,交互式调试器只是快速应用开发过程的一种不同模式。您修饰一个表单,编写一些代码,然后在调试模式下运行它。

图 5-3 描述了一个 Visual Studio 的例子。NET 2005 调试会话,使用前面显示的示例程序的 Windows 变体。

9781430246596_Fig05-03.jpg

图 5-3。Visual Studio 调试示例(sample.c)

交互式调试器具有与独立调试器相同的所有功能。您可以停止、开始、步入、跳过和运行到断点或光标处。当您发现缺陷的原因时,交互式调试器是最有用的;您可以停止执行,进行任何必要的更改,然后再次运行系统。表 5-1 提供了这些命令的简要描述。虽然大多数调试器都有所有这些命令,但有些使用不同的名称。有关命令的准确名称,请参考调试器的文档。

表 5-1 。基本调试器控制命令

命令 描述
开始(运行) 执行系统。
停止(中断) 暂时停止代码的执行。
进入 运行下一条代码语句,将焦点转移到下面的语句。如果正在执行的语句是一个函数,该命令会将焦点转移到正在调用的函数中的第一个可执行语句。
跨过 运行下一条代码语句,将焦点转移到下面的语句。如果正在执行的语句是一个函数,该命令将执行该函数,并将焦点转移到函数调用后的下一个可执行语句。
断点 当代码执行到发出断点的语句时,调试器停止。许多调试器允许使用条件断点,您可以根据表达式设置断点。
运行到光标处 调试器继续执行,但在控制到达光标所在的代码语句时停止执行。这是一次性断点的一种形式。

这个场景中的编译和链接发生在后台;它通常只需要一会儿就能完成,并且您又回到了调试器中。正如你所想象的,交互式调试器是实时的拯救者。如果您从未使用过独立调试器,您可能会对独立调试器与源代码项目明显缺乏集成感到沮丧。看似“老派”的东西实际上是大多数发展的状态。只是通过相对较新的快速应用开发工具的发展,交互式调试器才成为调试的首选工具。

GNU 数据显示调试器

交互式调试器的另一个例子是 GNU 数据显示调试器(ddd),可在http://www.gnu.org/software/ddd获得。ddd调试器允许你运行你的程序,并在程序运行时查看代码。它在概念上类似于快速应用开发调试器,如 Visual Studio。图 5-4 显示了我们在ddd中运行的示例程序。

9781430246596_Fig05-04.jpg

图 5-4。示例 ddd 会话调试“sample.c”

请注意,相同的变量显示在窗口的上部。使用ddd,我可以通过指向并点击代码行来设置代码断点,而不必记住文件中我想要中断的行号。我还可以通过双击变量来查看任何变量的内容。我甚至可以用类似的方式改变值。这允许我试验代码在不同的值下会如何执行。这是一个强大的特性,可以发现“差一个”的错误(例如,从 1 而不是 0 开始列表迭代器索引)。

image 注意有些人会称ddd工具为独立调试器,因为它本质上是以独立模式运行的。然而,由于其复杂的用户界面和类似开发的布局,我认为ddd工具是一个混合体,比大多数独立调试器更好地匹配了交互类型。此外,它确实把gdb提升了一个档次!

双向 调试器

尽管今天的调试器已经提供了所有的能力,但是使调试更加有效的工作仍在进行中。最有趣的是,研究人员正在研究执行和撤销操作的方法,以观察每个操作影响了什么。这给了进行调试的人回滚执行以发现缺陷来源的能力。这被推广它的研究人员称为逆向推理。他们认为,确定哪里出错的最有效的方法是观察代码执行的能力,并且能够在发现缺陷时倒带事件,并重放它们以查看发生了什么变化。实现这种技术的工具被称为双向调试器

一种商业产品,Undo 有限公司的 Undo db。(http://undo-software.com)可用于 Linux 平台,价格适中。Undo 提供评估下载,允许您评估其产品。UndoDB 是一个使用gdb信息的独立调试器。与gdb不同,它的命令允许你反向执行,返回并撤销上一条语句。清单 5-7 显示了在我们的示例程序中使用 UndoDB 的一个示例调试会话。

清单 5-7。 示例 UndoDB 会话调试(sample.c)

cbell@ubuntu:∼/source/sample$ ∼/undodb-3.5.205/undodb-gdb sample
undodb-gdb: Reversible debugging system.  Copyright 2006, 2007, 2008, 2009, 2010, 2011, 2012 Undo Ltd.
undodb-gdb: undodb-3.5.205
undodb-gdb: By running this software you agree to the terms in:
undodb-gdb:     /home/cbell/undodb-3.5.205/demo_license.html
undodb-gdb: starting gdb
GNU gdb (Ubuntu/Linaro 7.2-1ubuntu11) 7.2
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later < http://gnu.org/licenses/gpl.html >
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
For bug reporting instructions, please see:
< http://www.gnu.org/software/gdb/bugs/ >...
Reading symbols from /home/cbell/source/sample/sample...done.
(undodb-gdb) break sample.c:9
Breakpoint 1 at 0x400551: file sample.c, line 9.
(undodb-gdb) run 3
undodb-gdb: debug-server pid 2561, port 35605
Starting program: /home/cbell/source/sample/sample 3
undodb: license type: UndoDB version 3.5, demo, user: Charles Bell

Breakpoint 1, factorial (num=3) at sample.c:9
9         for (i = 1; i < num; i++)
(undodb-gdb) next
11           fact += fact * i;
(undodb-gdb) bnext

Breakpoint 1, factorial (num=3) at sample.c:9
9         for (i = 1; i < num; i++)
(undodb-gdb) next
undodb-gdb: Have switched to record mode.
11           fact += fact * i;
(undodb-gdb) break sample.c:13
Breakpoint 2 at 0x400575: file sample.c, line 13.
(undodb-gdb) continue
Continuing.

Breakpoint 2, factorial (num=3) at sample.c:13
13         return fact;
(undodb-gdb) print fact
$1 = 18
(undodb-gdb) bnext
9         for (i = 1; i < num; i++)
(undodb-gdb) print fact
$2 = 18
(undodb-gdb) bnext
11           fact += fact * i;
(undodb-gdb) print fact
$3 = 6
(undodb-gdb) print i
$4 = 2
(undodb-gdb) next
9         for (i = 1; i < num; i++)
(undodb-gdb) print i
$5 = 2
(undodb-gdb) print fact
$6 = 18
(undodb-gdb) print num
$7 = 3
(undodb-gdb) next
undodb-gdb: Have switched to record mode.

Breakpoint 2, factorial (num=3) at sample.c:13
13         return fact;
(undodb-gdb) continue
Continuing.
3! = 18

(undodb-gdb) quit
A debugging session is active.

      Inferior 1 [Remote target] will be killed.

Quit anyway? (y or n) y
cbell@ubuntu:∼/source/sample$

注意清单 5-7 中的命令bnextbnext命令是唯一的 UndoDB 命令之一,它允许执行的回溯(双向)。所有的 UndoDB 回溯命令都是gdb命令的镜像。这使得这个调试器对使用gdb的开发者非常友好。它最强大的功能是能够回滚语句以重新运行部分代码,而无需重新开始。

没有错误的方法

您可能想知道为什么我包含了一些调试方法,有些人可能认为这些方法是“老派”的,而不是最新的交互开发趋势。我认为,在某些情况下,甚至在一般情况下,有可能认为一种调试方法比另一种好。然而,这里介绍的任何方法,以及潜在的许多其他方法,都可以带来成功的结果。组织不应该强迫开发者遵循“这样做”的特定模式(这不仅仅适用于调试),因为适用于一个实例或个人的方法可能不适用于其他人。我的建议是采用您认为最符合您的需求和项目的调试工具或方法。如果这意味着使用类似跟踪的方法或交互式方法,那没关系,只要你能有效地调试你的项目。如果您掌握了良好的故障排除技能,并且能够获得发现问题所需的信息,那么如何找到问题并不重要。

调试 MySQL

在调试自己的应用时,您可能拥有出色的调试技能,其中一些应用可能确实非常大。然而,很少有人有机会尝试调试像 MySQL 这样的大型系统。虽然这并不难,但我在处理源代码的过程中发现了许多挑战。我希望下面的部分能给你我通过多次试验获得的知识。我鼓励你至少通读一遍这一部分,然后在有时间的时候按照我的例子来做。

我将通过一个使用内联调试语句调试 MySQL 的例子来研究调试会话。然后,我将继续讨论一个错误处理程序示例,然后深入研究如何在 Linux 和 Windows 上调试 MySQL。如果您一直在等待机会接触 MySQL 源代码,这一部分就是为您准备的。卷起袖子,拿一些你最喜欢的富含咖啡因的饮料,因为我们要进去了!

内联调试语句

Oracle 为他们的客户提供了一个健壮的内联调试语句调试工具,该工具基于最初由 Fred Fish 创建的调试器,后来由 MySQL 的创始人之一 Michael "Monty" Widenius 为线程安全进行了修改。这个工具实际上是一个名为 DBUG 的 C 宏集合。

使用 DBUG 很容易,因为提供的宏允许您简单地将一个代码语句放在您想要记录的地方。Oracle 开发者在代码中有很多很好的例子。它们记录了服务器执行的许多方面。各个宏被称为调试标记(在 MySQL 文档中称为 DBUG 标记)。MySQL 源代码中目前使用的标签包括:

  • DBUG_ENTER:使用函数说明识别函数入口。
  • DBUG_EXIT:记录函数返回的结果。
  • DBUG_INFO:记录诊断信息。
  • DBUG_WARNING:记录异常事件或意外事件。
  • DBUG_ERROR:记录错误代码(主要用于错误处理程序)。
  • DBUG_LOOP:记录循环的进入或退出。
  • DBUG_TRANS:记录事务信息。
  • DBUG_QUIT:记录导致系统过早关闭的故障。
  • DBUG_QUERY:记录查询语句。
  • DBUG_ASSERT:记录表达式测试失败的错误。

清单 5-8 展示了这些标签是如何在mysqld_show_privileges()函数中使用的。突出显示的代码语句是一些更常用的 DBUG 标记。

清单 5-8。 示例 DBUG 标签

bool mysqld_show_privileges(THD *thd)
{
  List<Item> field_list;
  Protocol *protocol= thd->protocol;

  DBUG_ENTER("mysqld_show_privileges");

  field_list.push_back(new Item_empty_string("Privilege",10));
  field_list.push_back(new Item_empty_string("Context",15));
  field_list.push_back(new Item_empty_string("Comment",NAME_CHAR_LEN));

  if (protocol->send_result_set_metadata(&field_list,
                            Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF))
    DBUG_RETURN(TRUE);

  show_privileges_st *privilege= sys_privileges;
  for (privilege= sys_privileges; privilege->privilege ; privilege++)
  {
    protocol->prepare_for_resend();
    protocol->store(privilege->privilege, system_charset_info);
    protocol->store(privilege->context, system_charset_info);
    protocol->store(privilege->comment, system_charset_info);
    if (protocol->write())

       DBUG_RETURN(TRUE);
  }
  my_eof(thd);
  DBUG_RETURN(FALSE);
}

调试标签列表非常全面。DBUG_ENTERDBUG_RETURN标签是最有用的,因为它们允许您记录系统在所有被调用的函数中的执行轨迹。特别需要指出的是,MySQL 源代码中的所有函数都分别在入口和出口包含这些标记。如果你要添加你自己的函数,做同样的事情,并记录你的函数的入口和出口。这些标签被写入一个跟踪文件,该文件存储在 Linux 上的/tmp/mysqld.trace和 Windows 上的c:\mysqld.trace中。

创建的跟踪文件可能会变得非常大。幸运的是,您可以通过在命令行上提供标记来控制将哪些标记写入跟踪文件。例如,要限制跟踪文件显示更有趣的调试标记,可以使用如下命令。开关的一般格式是a:b:c用于打开开关 a、b 和 c。任何带参数的开关都用逗号分隔。

mysqld-debug --debug=d,info,error,query,general,where:t:L:g:O,
/tmp/mysqd.trace -u root

前面的命令运行编译时启用了debug(mysqld-debug)的 MySQL 服务器。命令行参数--debug=d,info,error,query,general, where:t:L:g:O,/tmp/mysqd.trace指示 DBUG 系统启用DBUG_INFODBUG_ERRORDBUG_QUERYDBUG_WHERE宏的输出,打开函数进入/退出的跟踪行,包括调试语句的源代码行号,启用概要分析,并将文件写入/tmp/mysqld.trace-u root参数将用户名root传递给服务器执行。更多的选择是可用的;一些常用选项如表 5-2 所示。 2

表 5-2 。常用 DBUG 开关列表

转换 描述
d 打开参数中指定的 DBUG 标签的输出。空列表会导致所有标签的输出。
D 每次输出后执行延迟。参数指定要延迟的秒数。比如 D,40 会造成 4 秒的延迟。
f 将调试、跟踪和分析的记录限制在用 d。
F 为记录的每一行调试或跟踪输出源文件的名称。
为记录的每一行调试或跟踪输出进程 ID 或线程 ID。
g 打开分析。这些参数指定要分析的那些项目的关键字。空列表意味着所有关键字都被分析。
L 输出记录的每一行的源代码行号。
n 设置每行输出的嵌套深度。这有助于提高输出的可读性。
普通 在记录的每一行上放置连续的数字。
o 将输出保存到参数中指定的文件。默认值被写入 stderr。
O 将输出保存到参数中指定的文件。默认值被写入 stderr。在每次写入之间刷新文件。
P 输出记录的每一行的当前过程名称。
t 打开函数调用/退出跟踪线(用竖线表示)。

清单 5-9 显示了执行show authors;命令时的跟踪运行摘录。您可以看到系统在运行命令和返回数据时的完整轨迹(我省略了许多行,因为这个列表是用默认的调试开关生成的)。我已经标出了最有趣的线条。还要注意输出行下面的跟踪行。这允许您更容易地跟踪执行流程。

如果您在 MySQL 中编写自己的函数,可以使用 DBUG 标记将您自己的信息记录到跟踪文件中。如果您的代码导致不可预测或意外的行为,该文件会很有帮助。

清单 5-9。 显示特权命令的示例痕迹

T@3    : | | | | >mysqld_show_privileges
T@3    : | | | | | >alloc_root
T@3    : | | | | | | enter: root: 0x2264f88
T@3    : | | | | | | exit: ptr: 0x23d97b0
T@3    : | | | | | <alloc_root 247
T@3    : | | | | | >alloc_root
T@3    : | | | | | | enter: root: 0x2264f88
T@3    : | | | | | | exit: ptr: 0x23d9850
T@3    : | | | | | <alloc_root 247
T@3    : | | | | | >alloc_root
T@3    : | | | | | | enter: root: 0x2264f88
T@3    : | | | | | | exit: ptr: 0x23d9860
T@3    : | | | | | <alloc_root 247
T@3    : | | | | | >alloc_root
T@3    : | | | | | | enter: root: 0x2264f88
T@3    : | | | | | | exit: ptr: 0x23d9900
T@3    : | | | | | <alloc_root 247
T@3    : | | | | | >alloc_root
T@3    : | | | | | | enter: root: 0x2264f88
T@3    : | | | | | | exit: ptr: 0x23d9910
T@3    : | | | | | <alloc_root 247
T@3    : | | | | | >alloc_root
T@3    : | | | | | | enter: root: 0x2264f88
T@3    : | | | | | | exit: ptr: 0x23d99b0
T@3    : | | | | | <alloc_root 247
T@3    : | | | | | >send_result_set_metadata
T@3    : | | | | | | packet_header: Memory: 0x7f61f196f610  Bytes: (4)
01 00 00 01
T@3    : | | | | | | >alloc_root
T@3    : | | | | | | | enter: root: 0x2264f88
T@3    : | | | | | | | exit: ptr: 0x23d99c0
T@3    : | | | | | | <alloc_root 247
T@3    : | | | | | | >Protocol::write
T@3    : | | | | | | <Protocol::write 820
T@3    : | | | | | | packet_header: Memory: 0x7f61f196f5c0  Bytes: (4)
1F 00 00 02
T@3    : | | | | | | >Protocol::write
T@3    : | | | | | | <Protocol::write 820
T@3    : | | | | | | packet_header: Memory: 0x7f61f196f5c0  Bytes: (4)
1D 00 00 03
T@3    : | | | | | | >Protocol::write
T@3    : | | | | | | <Protocol::write 820
T@3    : | | | | | | packet_header: Memory: 0x7f61f196f5c0  Bytes: (4)
1D 00 00 04
T@3    : | | | | | | packet_header: Memory: 0x7f61f196f5b0  Bytes: (4)
05 00 00 05
T@3    : | | | | | <send_result_set_metadata 807
T@3    : | | | | | info: Protocol_text::store field 0 (3): Alter
T@3    : | | | | | info: Protocol_text::store field 1 (3): Tables
T@3    : | | | | | info: Protocol_text::store field 2 (3): To alter the table
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
20 00 00 06
T@3    : | | | | | info: Protocol_text::store field 0 (3): Alter routine
T@3    : | | | | | info: Protocol_text::store field 1 (3): Functions,Procedures
T@3    : | | | | | info: Protocol_text::store field 2 (3): To alter or drop stored functions/procedures
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
50 00 00 07
T@3    : | | | | | info: Protocol_text::store field 0 (3): Create
T@3    : | | | | | info: Protocol_text::store field 1 (3): Databases,Tables,Indexes
T@3    : | | | | | info: Protocol_text::store field 2 (3): To create new databases and tables
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
43 00 00 08
T@3    : | | | | | info: Protocol_text::store field 0 (3): Create routine
T@3    : | | | | | info: Protocol_text::store field 1 (3): Databases
T@3    : | | | | | info: Protocol_text::store field 2 (3): To use CREATE FUNCTION/PROCEDURE
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
3A 00 00 09
T@3    : | | | | | info: Protocol_text::store field 0 (3): Create temporary tables
T@3    : | | | | | info: Protocol_text::store field 1 (3): Databases
T@3    : | | | | | info: Protocol_text::store field 2 (3): To use CREATE TEMPORARY TABLE
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
40 00 00 0A
T@3    : | | | | | info: Protocol_text::store field 0 (3): Create view
T@3    : | | | | | info: Protocol_text::store field 1 (3): Tables
T@3    : | | | | | info: Protocol_text::store field 2 (3): To create new views
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
27 00 00 0B
T@3    : | | | | | info: Protocol_text::store field 0 (3): Create user
T@3    : | | | | | info: Protocol_text::store field 1 (3): Server Admin
T@3    : | | | | | info: Protocol_text::store field 2 (3): To create new users
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
2D 00 00 0C
T@3    : | | | | | info: Protocol_text::store field 0 (3): Delete
T@3    : | | | | | info: Protocol_text::store field 1 (3): Tables
T@3    : | | | | | info: Protocol_text::store field 2 (3): To delete existing rows
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
26 00 00 0D
T@3    : | | | | | info: Protocol_text::store field 0 (3): Drop
T@3    : | | | | | info: Protocol_text::store field 1 (3): Databases,Tables
T@3    : | | | | | info: Protocol_text::store field 2 (3): To drop databases, tables, and views
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
3B 00 00 0E
T@3    : | | | | | info: Protocol_text::store field 0 (3): Event
T@3    : | | | | | info: Protocol_text::store field 1 (3): Server Admin
T@3    : | | | | | info: Protocol_text::store field 2 (3): To create, alter, drop and execute events
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
3D 00 00 0F
T@3    : | | | | | info: Protocol_text::store field 0 (3): Execute
T@3    : | | | | | info: Protocol_text::store field 1 (3): Functions,Procedures
T@3    : | | | | | info: Protocol_text::store field 2 (3): To execute stored routines
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
38 00 00 10
T@3    : | | | | | info: Protocol_text::store field 0 (3): File
T@3    : | | | | | info: Protocol_text::store field 1 (3): File access on server
T@3    : | | | | | info: Protocol_text::store field 2 (3): To read and write files on the server
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
41 00 00 11
T@3    : | | | | | info: Protocol_text::store field 0 (3): Grant option
T@3    : | | | | | info: Protocol_text::store field 1 (3): Databases,Tables,Functions,Procedures
T@3    : | | | | | info: Protocol_text::store field 2 (3): To give to other users those privileges you possess
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
67 00 00 12
T@3    : | | | | | info: Protocol_text::store field 0 (3): Index
T@3    : | | | | | info: Protocol_text::store field 1 (3): Tables
T@3    : | | | | | info: Protocol_text::store field 2 (3): To create or drop indexes
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
27 00 00 13
T@3    : | | | | | info: Protocol_text::store field 0 (3): Insert
T@3    : | | | | | info: Protocol_text::store field 1 (3): Tables
T@3    : | | | | | info: Protocol_text::store field 2 (3): To insert data into tables
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
29 00 00 14
T@3    : | | | | | info: Protocol_text::store field 0 (3): Lock tables
T@3    : | | | | | info: Protocol_text::store field 1 (3): Databases
T@3    : | | | | | info: Protocol_text::store field 2 (3): To use LOCK TABLES (together with SELECT privilege)
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
4A 00 00 15
T@3    : | | | | | info: Protocol_text::store field 0 (3): Process
T@3    : | | | | | info: Protocol_text::store field 1 (3): Server Admin
T@3    : | | | | | info: Protocol_text::store field 2 (3): To view the plain text of currently executing queries
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
4B 00 00 16
T@3    : | | | | | info: Protocol_text::store field 0 (3): Proxy
T@3    : | | | | | info: Protocol_text::store field 1 (3): Server Admin
T@3    : | | | | | info: Protocol_text::store field 2 (3): To make proxy user possible
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
2F 00 00 17
T@3    : | | | | | info: Protocol_text::store field 0 (3): References
T@3    : | | | | | info: Protocol_text::store field 1 (3): Databases,Tables
T@3    : | | | | | info: Protocol_text::store field 2 (3): To have references on tables
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
39 00 00 18
T@3    : | | | | | info: Protocol_text::store field 0 (3): Reload
T@3    : | | | | | info: Protocol_text::store field 1 (3): Server Admin
T@3    : | | | | | info: Protocol_text::store field 2 (3): To reload or refresh tables, logs and privileges
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
45 00 00 19
T@3    : | | | | | info: Protocol_text::store field 0 (3): Replication client
T@3    : | | | | | info: Protocol_text::store field 1 (3): Server Admin
T@3    : | | | | | info: Protocol_text::store field 2 (3): To ask where the slave or master servers are
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
4D 00 00 1A
T@3    : | | | | | info: Protocol_text::store field 0 (3): Replication slave
T@3    : | | | | | info: Protocol_text::store field 1 (3): Server Admin
T@3    : | | | | | info: Protocol_text::store field 2 (3): To read binary log events from the master
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
49 00 00 1B
T@3    : | | | | | info: Protocol_text::store field 0 (3): Select
T@3    : | | | | | info: Protocol_text::store field 1 (3): Tables
T@3    : | | | | | info: Protocol_text::store field 2 (3): To retrieve rows from table
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
2A 00 00 1C
T@3    : | | | | | info: Protocol_text::store field 0 (3): Show databases
T@3    : | | | | | info: Protocol_text::store field 1 (3): Server Admin
T@3    : | | | | | info: Protocol_text::store field 2 (3): To see all databases with SHOW DATABASES
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
45 00 00 1D
T@3    : | | | | | info: Protocol_text::store field 0 (3): Show view
T@3    : | | | | | info: Protocol_text::store field 1 (3): Tables
T@3    : | | | | | info: Protocol_text::store field 2 (3): To see views with SHOW CREATE VIEW
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
34 00 00 1E
T@3    : | | | | | info: Protocol_text::store field 0 (3): Shutdown
T@3    : | | | | | info: Protocol_text::store field 1 (3): Server Admin
T@3    : | | | | | info: Protocol_text::store field 2 (3): To shut down the server
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
2E 00 00 1F
T@3    : | | | | | info: Protocol_text::store field 0 (3): Super
T@3    : | | | | | info: Protocol_text::store field 1 (3): Server Admin
T@3    : | | | | | info: Protocol_text::store field 2 (3): To use KILL thread, SET GLOBAL, CHANGE MASTER, etc.
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
47 00 00 20
T@3    : | | | | | info: Protocol_text::store field 0 (3): Trigger
T@3    : | | | | | info: Protocol_text::store field 1 (3): Tables
T@3    : | | | | | info: Protocol_text::store field 2 (3): To use triggers
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
1F 00 00 21
T@3    : | | | | | info: Protocol_text::store field 0 (3): Create tablespace
T@3    : | | | | | info: Protocol_text::store field 1 (3): Server Admin
T@3    : | | | | | info: Protocol_text::store field 2 (3): To create/alter/drop tablespaces
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
40 00 00 22
T@3    : | | | | | info: Protocol_text::store field 0 (3): Update
T@3    : | | | | | info: Protocol_text::store field 1 (3): Tables
T@3    : | | | | | info: Protocol_text::store field 2 (3): To update existing rows
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
26 00 00 23
T@3    : | | | | | info: Protocol_text::store field 0 (3): Usage
T@3    : | | | | | info: Protocol_text::store field 1 (3): Server Admin
T@3    : | | | | | info: Protocol_text::store field 2 (3): No privileges - allow connect only
T@3    : | | | | | >Protocol::write
T@3    : | | | | | <Protocol::write 820
T@3    : | | | | | packet_header: Memory: 0x7f61f196fa70  Bytes: (4)
36 00 00 24
T@3    : | | | | | >set_eof_status
T@3    : | | | | | <set_eof_status 483
T@3    : | | | | <mysqld_show_privileges 390

错误处理程序

关于 MySQL 中的错误处理程序,没有具体的工具可以演示。您应该努力生成处理所有可能错误的代码。向您展示如何做到这一点的最佳方式是使用一个错误处理程序的示例,该错误处理程序来自一个较旧的、不受支持的 MySQL 版本,它不能正确地管理错误。清单 5-10 显示了一段摘录自 MySQL 源代码的内容,该源代码有一个特殊类型的错误。

清单 5-10。MySQL 中错误处理程序示例

int my_delete(const char *name, myf MyFlags)
{
  int err;
  DBUG_ENTER("my_delete");
  DBUG_PRINT("my",("name %s MyFlags %d", name, MyFlags));

  if ((err = unlink(name)) == −1)
  {
    my_errno=errno;
    if (MyFlags & (MY_FAE+MY_WME))
      my_error(EE_DELETE,MYF(ME_BELL+ME_WAITTANG+(MyFlags & ME_NOINPUT)),
         name,errno);
  }
  DBUG_RETURN(err);
} /* my_delete */

你能看出缺陷吗?我给你一个提示。Windows 中unlink()函数的返回值有几个重要的值需要检查。在清单 5-10 中显示的错误处理程序中缺少了其中的一个。该缺陷导致optimize()函数在运行过程中错误地复制了一个中间文件。好在前段时间修复了这个缺陷。清单 5-11 显示了这个函数的修正形式。

清单 5-11。MySQL 中错误处理程序示例

int my_delete(const char *name, myf MyFlags)
{
  int err;
  DBUG_ENTER("my_delete");
  DBUG_PRINT("my",("name %s MyFlags %d", name, MyFlags));

  if ((err = unlink(name)) == −1)
  {
    my_errno=errno;
    if (MyFlags & (MY_FAE+MY_WME))
    {
      char errbuf[MYSYS_STRERROR_SIZE];
      my_error(EE_DELETE, MYF(ME_BELL+ME_WAITTANG+(MyFlags & ME_NOINPUT)),
               name, errno, my_strerror(errbuf, sizeof(errbuf), errno));
    }
  }
  else if ((MyFlags & MY_SYNC_DIR) &&
           my_sync_dir_by_file(name, MyFlags))
    err= −1;
  DBUG_RETURN(err);
} /* my_delete */

Oracle 提供了一个设计良好的错误消息机制,可以使您的错误处理程序更加健壮。要添加您自己的错误消息,请将它们添加到sql/share/errmsg-utf8.txt文件中。有关添加自己的错误消息的更多详细信息,请参见 dev.mysql.com 的内部文档。

形成错误处理程序来处理所有可能的错误,并采取适当的措施来纠正和报告错误,这一点我怎么强调都不为过。添加 DBUG 宏来跟踪和记录错误消息将确保您的所有调试会话更加高效。

Linux 中的调试

Linux 在其高级开发工具(主要是 GNU 工具)的质量方面表现出色。这些工具包括优秀的调试器,不仅能够处理单线程系统,还能够处理多线程系统。

许多调试器可用于 Linux。最受欢迎的是gdbddd。接下来的部分展示了调试 MySQL 系统的每个工具的例子。这些例子的场景是检查当发出SHOW AUTHORS命令时发生了什么。我将从gdb调试器开始,然后使用ddd向您展示相同的场景。

使用 gdb

让我们从重新检查show_authors()函数开始。回头参考清单 5-1 中该函数的完整代码。我需要做的第一件事是确保我已经在打开调试器的情况下构建了服务器。为此,请从源文件夹的根目录发出以下命令:

cmake . -DWITH_DEBUG=ON
make

这些命令将使用适当的调试信息编译系统,以便我可以使用调试器。我现在可以使用命令mysqld-debug在调试模式下启动服务器。清单 5-12 显示了服务器启动时出现的启动语句。

image 注意确保在调试模式下启动服务器之前,MySQL 服务器的所有安装都已关闭。虽然并非绝对必要,但这应该允许您避免尝试调试错误的进程。

清单 5-12。 以调试模式启动 MySQL 服务器

cbell@ubuntu:∼/source/mysql-5.6/mysql-test$ ./mysql-test-run.pl --start-and-exit --debug
Logging: ./mysql-test-run.pl  --start-and-exit --debug
120707 18:10:11 [Note] Plugin 'FEDERATED' is disabled.
120707 18:10:11 [Note] Binlog end
120707 18:10:11 [Note] Shutting down plugin 'CSV'
120707 18:10:11 [Note] Shutting down plugin 'MyISAM'
MySQL Version 5.6.6
Checking supported features...
 - skipping ndbcluster
 - SSL connections supported
 - binaries are debug compiled
Using suites: main,sys_vars,binlog,federated,rpl,innodb,innodb_fts,perfschema,funcs_1,opt_trace
Collecting tests...
Checking leftover processes...
 - found old pid 7375 in 'mysqld.1.pid', killing it...
   process did not exist!
Removing old var directory...
Creating var directory '/home/cbell/source/mysql-5.6/mysql-test/var'...
Installing system database...
Using server port 49434

==============================================================================

TEST                                      RESULT   TIME (ms) or COMMENT
------------------------------------------------------------------------------

worker[1] Using MTR_BUILD_THREAD 300, with reserved ports 13000..13009
worker[1]
Started [mysqld.1 - pid: 7506, winpid: 7506]
worker[1] Using config for test main.1st
worker[1] Port and socket path for server(s):
worker[1] mysqld.1  13000  /home/cbell/source/mysql-5.6/mysql-test/var/tmp/mysqld.1.sock
worker[1] Server(s) started, not waiting for them to finish

注意,在这种情况下,我使用的是指定为/var/lib/mysql/mysql.sock的套接字。这允许我在调试模式下运行服务器的副本,而不影响正在运行的服务器。然而,我需要告诉客户端使用同一个套接字。不过,首先,我需要确定我的服务器的进程 ID。我可以通过发出ps -A命令列出所有正在运行的进程来做到这一点。或者,我可以发出命令ps -A| grep mysql并获取名称中包含mysql的所有进程的进程 id。下面演示了该命令:

7506 pts/2    00:00:00 mysqld

现在我有了自己的进程 ID,我可以启动gdb并使用attach 10592命令连接到正确的进程。我还想在show_authors()函数中设置一个断点。对源文件的检查显示,我感兴趣的第一行是第 260 行。我发出命令 break/home/cbell/source/bzr/MySQL-5.6/SQL/SQL _ show . cc:260。该命令的格式为file:line#。现在我有了一个断点,我发出命令continue告诉进程执行,当遇到断点时gdb将暂停程序。清单 5-13 显示了完整的调试过程。

清单 5-13。 运行广发

cbell@ubuntu:∼/source/bzr/mysql-5.6/mysql-test$ gdb
GNU gdb (Ubuntu/Linaro 7.2-1ubuntu11) 7.2
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later < http://gnu.org/licenses/gpl.html >
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
For bug reporting instructions, please see:
< http://www.gnu.org/software/gdb/bugs/ >.
(gdb) attach 7506
Attaching to process 7506
Reading symbols from /home/cbell/source/bzr/mysql-5.6/sql/mysqld...done.
Reading symbols from /lib/x86_64-linux-gnu/libpthread.so.0...(no debugging symbols found)...done.
[Thread debugging using libthread_db enabled]
[New Thread 0x7f7ba8e57700 (LWP 7524)]
[New Thread 0x7f7ba4a26700 (LWP 7523)]
[New Thread 0x7f7ba5227700 (LWP 7522)]
[New Thread 0x7f7ba5a28700 (LWP 7521)]
[New Thread 0x7f7ba6229700 (LWP 7520)]
[New Thread 0x7f7ba6a2a700 (LWP 7519)]
[New Thread 0x7f7ba722b700 (LWP 7518)]
[New Thread 0x7f7ba7a2c700 (LWP 7517)]
[New Thread 0x7f7ba822d700 (LWP 7516)]
[New Thread 0x7f7ba8a2e700 (LWP 7515)]
[New Thread 0x7f7ba9658700 (LWP 7513)]
[New Thread 0x7f7ba9e59700 (LWP 7512)]
[New Thread 0x7f7baa65a700 (LWP 7511)]
[New Thread 0x7f7baae5b700 (LWP 7510)]
[New Thread 0x7f7bab65c700 (LWP 7509)]
[New Thread 0x7f7bb085f700 (LWP 7508)]
Loaded symbols for /lib/x86_64-linux-gnu/libpthread.so.0
Reading symbols from /lib/x86_64-linux-gnu/librt.so.1...(no debugging symbols found)...done.
Loaded symbols for /lib/x86_64-linux-gnu/librt.so.1
Reading symbols from /lib/x86_64-linux-gnu/libcrypt.so.1...(no debugging symbols found)...done.
Loaded symbols for /lib/x86_64-linux-gnu/libcrypt.so.1
Reading symbols from /lib/x86_64-linux-gnu/libdl.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib/x86_64-linux-gnu/libdl.so.2
Reading symbols from /usr/lib/x86_64-linux-gnu/libstdc++.so.6...(no debugging symbols found)...done.
Loaded symbols for /usr/lib/x86_64-linux-gnu/libstdc++.so.6
Reading symbols from /lib/x86_64-linux-gnu/libm.so.6...(no debugging symbols found)...done.
Loaded symbols for /lib/x86_64-linux-gnu/libm.so.6
Reading symbols from /lib/x86_64-linux-gnu/libgcc_s.so.1...(no debugging symbols found)...done.
Loaded symbols for /lib/x86_64-linux-gnu/libgcc_s.so.1
Reading symbols from /lib/x86_64-linux-gnu/libc.so.6...(no debugging symbols found)...done.
Loaded symbols for /lib/x86_64-linux-gnu/libc.so.6
Reading symbols from /lib64/ld-linux-x86-64.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib64/ld-linux-x86-64.so.2
0x00007f7bb0939ae3 in poll () from /lib/x86_64-linux-gnu/libc.so.6
(gdb) break /home/cbell/source/bzr/mysql-5.6/sql/sql_show.cc:260
Breakpoint 1 at 0x7cee94: file /home/cbell/source/bzr/mysql-5.6/sql/sql_show.cc, line 260.
(gdb) continue
Continuing.

Note: Here, you can run the mysql client and issue the SHOW AUTHORS command.

[New Thread 0x7f7ba8e16700 (LWP 7536)]
[Switching to Thread 0x7f7ba8e16700 (LWP 7536)]

Breakpoint 1, mysqld_show_authors (thd=0x3a62ca0) at /home/cbell/source/bzr/mysql-5.6/sql/sql_show.cc:260
260         field_list.push_back(new Item_empty_string("Name",40));
(gdb) next
261         field_list.push_back(new Item_empty_string("Location",40));
(gdb) next
262         field_list.push_back(new Item_empty_string("Comment",80));
(gdb) next
265                                   Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF))
(gdb) next
264         if (protocol->send_result_set_metadata(&field_list,
(gdb) next
269         for (authors= show_table_authors; authors->name; authors++)
(gdb) next
271           protocol->prepare_for_resend();
(gdb) print authors->name
$1 = 0xec70c2 "Brian (Krow) Aker"
(gdb) quit
A debugging session is active.

 Inferior 1 [process 7506] will be detached.

Quit anyway? (y or n) y
Detaching from program: /home/cbell/source/bzr/mysql-5.6/sql/mysqld, process 7506

清单 5-14 显示了在命令行上指定所需套接字的客户端初始化。然后,我启动 SHOW AUTHORS 命令。

清单 5-14。 启动 MySQL 客户端连接服务器

cbell@ubuntu:∼$ mysql -uroot -h 127.0.0.1 --port=13000
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 1
Server version: 5.6.6-m9-debug-CAB MODIFICATION Source distribution

Copyright (c) 2000, 2011, 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> show authors;

当我输入命令时,我注意到的第一件事是客户机停止了。原因是gdb调试器遇到了断点并暂停了执行。当我切换回调试器时,我可以使用next命令发出命令来逐步执行。我也可以使用print命令显示变量的值。(清单 5-13 展示了这些命令的运行。)一旦我完成了调试会话,我就可以关闭服务器并退出调试器。

调试器是一个强大的工具,但是它缺乏大多数集成开发环境(ide)中调试器的复杂性。ddd调试器通过提供一个健壮的图形环境弥补了这个限制。

使用 ddd

GNU ddd调试器是集成调试器的一个很好的例子。虽然不是专门围绕 IDE 构建的,但是ddd调试器提供了类似的体验。您可以启动想要调试的程序并查看源代码。使用集成的工具,您可以设置断点,停止和启动正在调试的程序,设置对变量的监控,查看堆栈跟踪,甚至编辑变量值。

几个窗口与调试器相关联。数据窗口显示您已设置手表的所有数据项。源代码窗口(主显示区)显示正在调试的程序的当前源代码。调试器控制台显示主机调试器(gdb)输出。这个窗口对于使用gdb的开发者来说很方便,因为它允许你输入自己的gdb命令。因此,您可以使用菜单系统来控制程序,也可以使用调试器控制台来直接向调试器发出命令。

ddd调试器实际上是 GNU gdb独立调试器的包装器。在典型的开源方式中,ddd的开发者重用了已经构建好的东西(gdb),而不是重新发明轮子(符号调试器代码),他们用一组新的功能来扩充它。此外,ddd可以支持几个独立的调试器,这使得它非常通用。事实上,它可以支持其宿主调试器所能支持的任何语言。在许多方面,ddd展示了集成调试器应该是什么样的。它拥有你调试任何用多种语言编写的程序所需的所有工具。

我发现ddd调试器最吸引人的特性之一是保存调试会话并在以后调用它的能力。这给了您不必重新创建一个场景来演示或重复一个缺陷的优势。我建议,为了最有效地使用它,您调试您的程序直到缺陷发现点(比如在有问题的函数的开始处),设置您所有的监控和断点,然后保存会话。这将允许您在以后需要追溯您的步骤时重新启动调试会话。虽然不如双向调试器高效,但保存调试会话可以节省大量时间。

您可以使用ddd调试器来检查核心转储。这允许您检查核心转储中的数据,以确定程序的状态以及崩溃前的最后几次操作。如果导致崩溃的缺陷也导致调试器崩溃,这就非常方便了。 3 还支持远程调试和直接检查内存。这允许您调试在另一台计算机(通常是服务器)上运行的系统,并在您的开发工作站上操作调试器。关于ddd调试器的更多信息,请参见www.gnu.org/software/ddd/ddd.html#Doc?? 的优秀文档

使用ddd调试 MySQL 可以通过以下步骤完成:

  1. 停止任何正在运行的 MySQL 服务器。使用命令mysqladmin -uroot -p shutdown并输入您的 root 密码。
  2. 转到包含源代码的目录。如果您正在调试服务器(mysqld),您希望切换到sql目录。
  3. 使用命令ddd mysqld-debug启动ddd调试器。
  4. 打开要调试的源代码文件。在下面的例子中,我使用了sql_show.cc
  5. 设置希望代码停止的任何断点。在下面的例子中,我在show_authors()函数的第 207 行设置了一个断点。
  6. 使用程序image运行菜单运行服务器,通过在对话框中提供参数u root指定服务器以 root 用户身份运行。
  7. 启动您的 MySQL 客户端。在下面的例子中,我使用普通的 MySQL 命令行客户端。
  8. 在客户端发出您的命令。调试器将暂时停止执行,并在定义的任何断点处停止。从这里,您可以开始调试。
  9. 当您完成调试后,使用命令mysqladmin -uroot -p shutdown退出客户端并关闭服务器,然后输入您的 root 密码。

image 提示您可能需要延长测试 MySQL 客户端的超时时间。如果您正在单步执行一系列断点或者正在检查大量变量,调试可能会花费一些时间。当你调试的时候,系统实际上处于僵死状态。这可能会导致服务器和客户端停止通信。一些客户端被设计为在一段时间后无法与服务器通信时终止。如果您使用的是 MySQL 命令行客户端,则需要延长超时时间。您可以通过使用--connection-timeout=600在命令行上指定值来实现这一点。在客户端断开连接之前,这给了您大约 10 分钟的时间来使用调试器。

清单 5-15 展示了如何使用ddd调试器来调试 MySQL 服务器。我选择了之前的同一个函数,在sql_show.cc源文件中的show_authors()函数。在这个场景中,我对服务器如何处理向客户机发送信息很感兴趣。你可能还记得在第 3 章中,我提到过一个向客户端返回数据的例子。

清单 5-15。【show _ authors】功能带高光

/***************************************************************************
** List all Authors.
** If you can update it, you get to be in it :)
***************************************************************************/

bool mysqld_show_authors(THD *thd)
{
  List<Item> field_list;
  Protocol *protocol= thd->protocol;
  DBUG_ENTER("mysqld_show_authors");

  field_list.push_back(new Item_empty_string("Name",40));
  field_list.push_back(new Item_empty_string("Location",40));
  field_list.push_back(new Item_empty_string("Comment",40));

  if (protocol->send_result_set_metadata(&field_list,
                            Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF))
      DBUG_RETURN(TRUE);

  show_table_authors_st *authors;
  for (authors= show_table_authors; authors->name; authors++)
  {
    protocol->prepare_for_resend();
    protocol->store(authors->name, system_charset_info);
    protocol->store(authors->location, system_charset_info);
    protocol->store(authors->comment, system_charset_info);
    if (protocol->write())
      DBUG_RETURN(TRUE);
  }
  my_eof(thd);
  DBUG_RETURN(FALSE);
}

粗体的语句是用于将数据发送回客户端的方法。show_authors()函数非常适合演示这个过程,因为它是最简单的实现(没有复杂的操作——只是发送数据)。第一个突出显示的语句显示了指向现有 threads 协议类的指针声明。protocol 类封装了所有底层通信方法(如网络和套接字控制)。下一组语句构建了一个字段列表。你总是先给客户发送一个字段列表。一旦构建了字段列表,就可以用protocol->send_fields()方法将它发送给客户机。在循环中,代码遍历在show_table_authors_st的链表中定义的作者列表。在这个循环中,有三个主要的方法用于向客户端发送数据。第一个是protocol->prepare_for_resend(),为发送数据清除合适的缓冲区和变量。下一个是protocol->store(),它将信息放在发送缓冲区中。您应该将每个字段作为对此方法的单独调用发送。protocol->write()方法发出适当的动作将数据发送给客户机。最后,send_eof()方法指示通信机制发送文件结束标记来标记数据的结束。此时,客户端显示数据。

让我们使用ddd调试器来看看这个函数是如何工作的。我通过发出以下命令,使用调试开关构建了我的服务器:

cmake . -DWITH_DEBUG=ON
make

这些命令将使用调试信息编译系统,以便我可以使用调试器。一旦我确认没有其他服务器在运行,我就启动ddd调试器,加载我的源文件(sql_show.cc,在第 207 行的show_authors()函数中设置一个断点,然后运行程序。此时,我启动我的 MySQL 客户端程序,将连接超时设置为 10 分钟,并发出SHOW AUTHORS命令。回头参考清单 5-13 查看服务器启动序列;清单 5-16 显示了客户端启动序列。

清单 5-16。 启动 MySQL 客户端与 ddd 调试器一起使用

cbell@ubuntu:∼$ mysql -uroot -h 127.0.0.1 --port=13000
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 1
Server version: 5.6.6-m9-debug-CAB MODIFICATION Source distribution

Copyright (c) 2000, 2011, 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> show authors;

当执行到达调试器中的断点时,服务器将停止,ddd调试器将显示代码,并带有一个指向断点的箭头。您还会注意到客户端已经停止。如果调试时间过长,客户端可能会超时。这就是我使用连接超时覆盖的原因。

一旦调试器停止执行,您就可以开始研究代码并检查任何变量、堆栈或内存的值。我已经设置了调试器来检查作者的结构,以便在数据写入客户端时查看数据。图 5-5 描述了ddd调试器,其作者结构显示在数据窗口中。

9781430246596_Fig05-05.jpg

图 5-5。 ddd 调试 show_authors()函数

我还可以展开作者的结构并查看当前的内容。图 5-6 显示了数据窗口中显示的作者结构的内容。

9781430246596_Fig05-06.jpg

图 5-6。作者在 ddd 调试器中构建数据

请注意,值和地址显示在数据窗口中。ddd调试器还允许你修改内存的内容。假设我正在调试这个方法,我想更改 authors 结构中的值。我只需右键单击 authors 结构中的每一项,从右键菜单中选择 Set Value,然后更改值。图 5-7 显示我已经改变了作者结构的内容。

9781430246596_Fig05-07.jpg

图 5-7。作者结构数据发生变化

你可能想知道这是否真的有用。的确如此。清单 5-17 显示了客户端的输出(为了清楚起见,我省略了许多行)。请注意,我更改的数据确实被发送到了客户端。

清单 5-17。 由数据修改产生的输出

mysql> show authors;
+−−-------------+−−------------+−−-------------------------------------+
| Name          | Location     | Comment                               |
+−−-------------+−−------------+−−-------------------------------------+
| Chuck Bell    | Anytown, USA | Replication, Backup, MySQL Utilities  |
...
+−−-------------+−−------------+−−-------------------------------------+
80 rows in set (48.35 sec)

mysql>

一旦我完成了我的调试会话,我发出命令关闭服务器,然后退出ddd:

mysqladmin -uroot -p shutdown

从这个简单的例子中可以看出,用ddd进行调试是一种有益的体验,它允许您在代码执行时查看代码。能够看到与当前执行相关的数据的能力是发现和纠正缺陷的有效手段。我鼓励您尝试这个例子,并尝试使用ddd,直到您能够熟练使用为止。

在窗口中调试

在 Windows 中调试的主要方法是使用 Microsoft Visual Studio .NET。一些开发者已经成功使用其他工具,如外部调试器,但大多数人将使用与 Visual Studio 集成的调试器。网。使用集成调试器很方便,因为您可以从同一个界面编译和调试。

我将使用与前面的ddd示例相同的场景。虽然步骤相似,但您会看到一些不同之处。具体来说,我通过启动 Visual Studio 并打开源代码目录根目录中的mysql.sln解决方案文件来开始我的调试会话。我确保我的会话被设置为在debugwin32平台编译程序。这将确保正确的调试信息被编译到可执行文件中。一旦 Visual Studio 启动并设置了正确的编译模式,我就可以设置我的断点(同样,在第 207 行的show_authors()函数中)。图 5-8 显示了正确配置了断点设置的 Visual Studio。

9781430246596_Fig05-08.jpg

图 5-8。 Visual Studio 调试器安装

为了调试服务器,我必须在调试模式下启动服务器。在 Windows 上,使用开关独立运行服务器,使其不作为服务运行。虽然这不是绝对必要的,但它允许您在命令窗口中看到来自服务器的任何消息,否则这些消息会被隐藏。您可以发出以下命令来实现这一点:

mysqld-debug --debug –standalone

image 我把mysqld.exe服务器改名为mysqld-debug.exe以便在 Windows 任务管理器中更容易找到。

一旦服务器运行,我就可以使用 Debug image Attach to Process 菜单选项从 Visual Studio 附加到进程。图 5-9 显示了附加到进程对话框。我选择运行并附加到mysqld-debug进程,这样我也可以在调试会话期间生成一个跟踪文件。

9781430246596_Fig05-09.jpg

图 5-9。附加到 Visual Studio 中的进程。网

我需要做的下一件事是启动客户端。我再次使用connect-timeout参数将超时设置为更长的延迟。我用来从命令窗口启动客户端的命令是:

mysql -uroot -p --connect-timeout=600

在客户端运行的情况下,我可以发出show authors;命令,Visual Studio 将在遇到断点时拦截该命令。然后,我可以使用“单步执行”( F10)和“单步执行”( F11)命令来单步执行代码。我停止了循环中发送数据的代码,并检查了作者的结构。图 5-10 显示了我进入循环后调试器的状态。

9781430246596_Fig05-10.jpg

图 5-10。在 Visual Studio 中显示变量值。网

ddd一样,您也可以更改变量值。然而,在 Visual Studio 中这样做有点复杂。虽然可能有其他选项,但在 Visual Studio 中更改值的最佳方式是在“监控”窗口中编辑值。但是,如果"监控"窗口中的值是指向内存位置的指针,则必须更改内存位置。为此,请打开“内存调试”窗口,并使用“监控”窗口来定位内存位置并就地编辑它。图 5-11 显示记忆窗口打开,数值被编辑。

9781430246596_Fig05-11.jpg

图 5-11。使用 Visual Studio 编辑内存中的值。网

在内存中的值被编辑后,我可以继续执行并在客户端窗口中看到结果。清单 5-18 显示了样本输出的摘录。

清单 5-18。 调试会话的输出

mysql> show authors;
+−−-----------+−−----------------+−−-----------------------+

| Name        | Location         | Comment                 |

+−−-----------+−−----------------+−−-----------------------+

| Aker, Brian | Seattle, WA, USA | bunch of little stuff :)|

| JonJon      | Hometown, USA    | Companion, Best Friend  |

...

+−−-----------+−−----------------+−−-----------------------+

80 rows in set (1 min 55.64 sec)

mysql>

为了停止调试会话,我在命令窗口中发出shutdown命令,然后使用 Visual Studio 中的 Debug image Detach All 菜单选项从进程中分离。

mysqladmin -uroot -p shutdown

既然您已经看到了如何在 Windows 上使用 Visual Studio 调试 MySQL 系统,我鼓励您再次通读这个示例,并在您自己的 Windows 开发机器上进行尝试。

摘要

在本章中,我解释了调试,向您展示了一些基本的调试技术,并提供了如何使用它们的示例。这些技术包括内联调试语句、错误处理和外部调试器。

您了解了如何使用 Oracle 提供的内联调试语句 DBUG 工具来创建系统执行的跟踪文件、写出相关数据以及记录错误和警告。您还了解了如何使用gdbddd和 Visual Studio .NET 在 Linux 和 Windows 中进行调试。

培养良好的调试技能对于成为一名优秀的软件开发者至关重要。希望这一章已经为你完善自己的调试技巧提供了基础。

在下一章中,我将研究系统集成商对 MySQL 系统的一种流行使用:嵌入式 MySQL。这允许 MySQL 系统成为另一个系统的一部分。可以想象,这个过程可能需要一些认真的调试,以找出在嵌入的哪一层出了什么问题。

1 我个人不相信随机区间。在计算机能够独立思考之前,它们只是执行人类指令的机器。

2

这是一种最令人讨厌的情况,很难克服。在这些情况下,我通常求助于内联调试语句和核心转储进行调试。

六、嵌入式 MySQL

MySQL 服务器以其轻量级和高性能特性而闻名,但是您知道它也可以用作企业应用的嵌入式数据库吗?本章解释了嵌入式应用的概念,以及如何使用 MySQL C API 来创建自己的嵌入式 MySQL 应用。我将向您介绍编译嵌入式服务器和为 Linux 和 Windows 编写应用的技术。

构建嵌入式应用

已经使用轻量级数据库系统作为内部数据存储构建了许多应用。如果您使用 Microsoft Windows 作为主要的桌面操作系统,那么您很可能已经见过或使用过至少一个使用 Microsoft Access 数据库引擎的应用。即使应用没有宣传 Access 的使用,通常只要粗略地看一下安装目录就可以知道。

一些嵌入式应用使用主机上现有的数据库系统(如 Access),而另一些则使用大型数据库系统的专用安装。不太明显的是那些将数据库系统编译进软件本身的应用。

什么是嵌入式 系统?

一个嵌入式系统是包含在另一个系统 中的系统。简单来说,嵌入式系统是主机系统的从机。嵌入式系统的目的是提供主机系统需要的一些功能。这可能是通信机制、数据存储和检索,甚至是图形用户显示。

嵌入式系统传统上被认为是专用的硬件或电子设备。例如,自动柜员机(ATM)是包含专用硬件的嵌入式系统。今天,嵌入式系统不仅包括专用硬件,还包括专用软件系统。与难以或不可能修改的嵌入式硬件不同,嵌入式软件经常被修改以在特定环境中工作。嵌入式硬件和软件都具有独立的特性,并为主机系统提供一些服务。

嵌入式软件系统通常与您日常看到和使用的应用不同。有些,比如那些使用嵌入式 MySQL 库的,是对现有功能的改编,是为了在另一个软件系统中更有效地工作。然而,与其独立的服务器版本不同,嵌入式 MySQL 服务器被设计为在编程级别上运行。也就是说,对服务器的调用是通过编程语言完成的,而不是作为特别的查询。方法在嵌入式服务器中公开,以将特定查询作为参数,并启动服务器来执行它们。

这意味着嵌入式 MySQL 服务器只能通过另一个应用来访问。正如您将在接下来的几节中看到的,嵌入式软件可以存在于许多应用中,其集成级别从封闭的仅编程访问到被宿主应用“隐藏”的全功能系统不等。让我们先来看看最常见的嵌入式系统类型。

嵌入式系统的类型

由于嵌入式系统使用的独特性,很难对其进行分类。它们通常属于以下一个(或多个)类别:

实时:在要求主机系统在给定阈值内做出响应和动作的设施中使用的系统。这套系统最常见的特征是计时。每个命令过程的执行时间必须最小化以实现系统的目标。通常,这些系统需要在外部发生的事件内执行,而不是以任何内部处理速度执行。路由或电信交换机就是实时系统的一个例子。

反应型:只对外部事件做出反应的系统。这些事件在本质上倾向于重复发生和循环,但它们也可能以用户输入的形式出现(交互系统是反应式系统)。反应系统被设计成总是可操作的。定时通常是次要的,只受循环操作频率的限制。反应式系统的一个例子是安全监控系统,该系统被设计成当某些事件或阈值发生时寻呼或警告服务人员。

过程控制:用于控制其他系统的系统。这些往往是那些旨在监测和控制硬件设备,如机器人和加工机械。这些系统通常被编程为重复一系列动作,并且通常不会偏离其预期的编程或响应外部事件或状态变量或条件的阈值。过程控制系统的一个例子是在装配汽车特定部件的汽车装配线上使用的机器人。

关键:用于具有高成本因素的设施中的系统,例如安全、医疗或航空。这些系统被设计成不会失败(或者永远不会失败)。这些系统通常包括前面描述的嵌入式系统的变体。关键系统的例子包括医疗系统,如呼吸器或人工循环系统。

嵌入式数据库系统

一个嵌入式数据库 系统是一个为主机应用或环境提供数据的系统。这些数据通常是在进程中被请求的,因此数据库必须响应请求并毫不延迟地返回任何信息。嵌入式数据库系统被认为对主机应用和整个系统至关重要。因此,嵌入式数据库系统也必须满足用户的时间要求。这些需求意味着嵌入式数据库系统通常被归类为反应式系统。

除了最普通的应用,个人和企业使用的所有应用都生产、消费和存储数据。许多应用的数据结构良好,对客户具有内在价值。事实上,在许多情况下,数据是自动持久化的,客户希望在需要的时候数据是可用的。此类应用的子系统要么具有访问方法,要么连接到外部文件或数据处理系统,如数据库服务器。

使用文件访问数据的嵌入式系统面临着许多问题,尤其是数据是否可以在主机应用之外访问。在这种情况下,访问限制可能必须从头开始创建,或者作为系统中的另一层添加。文件系统通常具有非常好的性能,并提供更快的访问时间,但它们不如数据库系统灵活。数据库系统在存储数据的形式上提供了更多的灵活性(作为表格而不是结构化文件),但通常会导致访问速度较慢。

虽然保护数据的原因可能多种多样,但最基本的要求是以最有效的方式存储和检索数据,而不将数据暴露给其他人。很多时候这仅仅是对数据库系统的需要。例如,Adobe Bridge 之类的应用管理着 Adobe 生产工具套件中使用的大量文件、项目、照片等数据。这些文件需要以一种易于搜索和检索的方式进行组织。Adobe 使用嵌入式数据库(MySQL)来管理 Adobe Bridge 存储的文件的元数据。在这种情况下,应用使用数据库系统来处理更困难的工作,即存储、搜索和检索关于它所管理的对象的元数据。

由于数据必须受到保护,使用外部数据库系统的选择变得有限,因为完全保护(或隐藏)数据并不总是容易或可能的。嵌入式数据库系统允许应用使用数据库系统的全部功能,同时对外部来源隐藏机制和数据。

嵌入 MySQL

MySQL 工程师在 MySQL 开发的早期就认识到,它的许多客户都是系统集成商,需要一个健壮、高效、可编程访问的数据库系统。他们不仅提供了一个嵌入式库,还提供了一个全功能的客户端库。客户端库允许您创建自己的 MySQL 客户端。例如,您可以创建自己版本的 MySQL 命令行客户端。客户端库被命名为libmysql。如果您想了解典型的 MySQL 客户端如何使用这个库,请查看mysql项目源文件。

MySQL 嵌入式库以服务器可执行文件的名称命名为libmysqld。您可能会看到这个库被称为嵌入式服务器,或者简称为 C API。本章专门介绍嵌入式库(libmysqld),但是,客户端和嵌入式服务器库之间的大部分访问和连接是相似的。

嵌入式库提供了许多通过应用编程接口(API)访问数据库系统的功能。API 提供了许多允许系统利用 MySQL 服务器的特性(通过编程)。这些特性包括:

  • 连接并建立服务器实例
  • 断开与服务器的连接
  • 使用受控(安全)机制关闭服务器
  • 操作服务器启动选项
  • 处理错误
  • 生成 DBUG 跟踪文件
  • 发出查询并检索结果
  • 管理数据
  • 访问 MySQL 服务器的(几乎)全部特性集

最后一点是独立服务器和嵌入式服务器之间最重要的区别之一。嵌入式服务器不使用完整的身份验证机制,默认情况下是禁用的。这是嵌入式 MySQL 系统难以保护的原因之一(更多细节请参见后面的“安全性问题”)。但是,您可以使用配置选项--with-embedded-privilege-control打开认证,并重新编译嵌入式服务器。除此之外,在特性和功能方面,服务器的行为与独立服务器几乎相同。

真正酷的是,由于嵌入式库使用与独立服务器相同的访问方法,所以您使用独立服务器创建的所有数据库和表都可以与嵌入式服务器一起使用。这允许您创建表并使用独立的服务器测试它们,然后将它们移动到嵌入式系统中。尽管可以让两台服务器访问同一个数据目录,但这是绝对不鼓励的,因为这可能会导致数据丢失和不可预测的行为(永远不要在 MySQL 服务器实例之间“共享”数据目录)。

这是否意味着您可以将独立服务器作为嵌入式服务器在同一台机器上运行?不仅是可以,您想要多少台嵌入式服务器?只要嵌入式服务器实例没有使用同一个数据目录,就可以同时运行几个。每个人管理的数据与其他人管理的数据是分开的,没有数据共享。我在自己的系统上试了一下,效果不错。我有一个 5.6.9 嵌入式应用,与我的 5.1(正式上市)GA 独立服务器一起运行。在撰写本文时,MySQL 5.5 是最新的 GA 版本。我不必停止甚至中断单机来与嵌入式服务器交互。多酷啊。

嵌入 MySQL 的方法

有许多类型的嵌入式应用。嵌入式数据库应用通常分为三类。它们要么部分隐藏在另一个接口后面(捆绑嵌入),要么隐藏在一个包装或包含数据库服务器的系统后面(深度嵌入)。下面几节描述了与嵌入 MySQL 系统相关的每一种类型。

捆绑服务器嵌入

捆绑服务器嵌入是一个独立安装 MySQL 服务器构建的系统。服务器级嵌入式系统通过关闭外部(网络)访问来隐藏 MySQL 服务器,而不是让系统或网络上的任何人都可以使用 MySQL。因此,这种形式的嵌入式 MySQL 系统只是一个关闭了网络访问(TCP/IP)的独立服务器。

这种类型的嵌入式 MySQL 系统的优势在于,可以使用本地安装的(并正确配置的)客户端应用来维护服务器。因此,系统集成商、管理员和开发者可以使用常规的管理和开发工具来维护嵌入式 MySQL 服务器,而不必使用外部应用加载数据。

服务器级嵌入式 MySQL 系统的一个例子是 LeapTrack 软件,由 LeapFrog ( www.leapfrogschoolhouse.com/do/findsolution?detailPage=overview&name=ReadingPro)生产。MySQL 报告称,LeapFrog 选择 MySQL 是因为其跨平台支持,允许 LeapFrog 在各种平台上提供其产品,而不改变核心数据库功能。在那之前,LeapFrog 一直为其各种平台使用不同的专有数据库解决方案。

深度嵌入 (libmysqld)

深度嵌入甚至比捆绑嵌入更具限制性。这种类型的嵌入式系统使用 MySQL 系统作为一个完整的组件。这意味着 MySQL 系统不仅不能从网络访问,而且也不能从普通的客户端应用访问。相反,该系统是使用 Oracle 提供的名为libmysqld的特殊嵌入式库构建的。大多数嵌入式 MySQL 系统都属于这一类。

因为这种类型的嵌入式系统仍然使用 MySQL 机制进行数据访问,所以它提供了相同的数据库功能,只有一些限制(我将稍后讨论)。开发者能够通过各种开发语言在各种平台上使用深度嵌入的 MySQL 系统(正如我前面解释的)。此外,它为开发者提供了一个代码级的解决方案,这是很少关系数据库系统能够提供的。

使用深度嵌入的 MySQL 系统的最大优势是,它提供了一个几乎完全隔离的 MySQL 系统,单独服务于嵌入式应用的目的。

深度嵌入的 MySQL 应用的一个例子是 Adobe 公司的 Adobe Bridge(www.adobe.com/products/bridge.html)。Adobe Bridge 是更大的 Adobe Creative Suite 的一部分,用于管理 Creative Suite 支持的数据的各个方面,而最终用户并不知道他们正在运行一个专用的 MySQL 系统。 1 大多数深度嵌入式系统都是用户安装在本地计算机上的桌面应用。

资源需求

运行嵌入式服务器的要求取决于嵌入的类型。如果使用捆绑嵌入,要求与独立安装的要求相同。然而,一个深度嵌入的 MySQL 系统是不同的。除了应用的需求之外,深度嵌入式系统还需要大约 2MB 的内存来运行。编译后的嵌入式服务器为可执行内存增加了相当多的空间,但并不繁重或难以管理。

磁盘空间是要考虑的最不可预测的资源。这是真的,因为它确实取决于嵌入式系统使用了多少数据。对于高吞吐量系统或处理大量数据更改的系统,磁盘空间和时间也是需要考虑的问题。处理对数据的大量更改对响应时间的影响往往大于对所用空间的影响。在这些情况下,数据库的维护可能需要对服务器的特殊访问或特殊接口,以允许管理员访问数据。这是一个很好的例子,在这个例子中,以捆绑的嵌入形式访问数据库服务器比使用深度嵌入更容易。

安全问题

安全性是另一个依赖于嵌入类型的领域。如果系统是使用服务器嵌入构建的,解决安全问题可能会非常具有挑战性。这是真的,因为 MySQL 系统仍然可以从本地服务器使用正常的工具集进行访问。完全锁定这种类型的嵌入式系统可能非常困难。

捆绑嵌入要容易得多,因为嵌入式独立 MySQL 系统只能通过嵌入式应用访问。除非嵌入式应用开发者有一个失调的道德指南针,否则他们会采取措施来确保正确的凭证是访问管理功能所必需的。

深度嵌入式系统是保护数据最困难的情况。嵌入式 MySQL 系统可能没有为其设置任何密码(它们通常没有),因为与捆绑嵌入一样,它们要求用户使用提供的接口来访问数据。不幸的是,事情没那么简单。在许多情况下,数据放在用户可以访问的目录中。事实上,数据需要用户可以访问;否则,她怎么能读取数据呢?

这就是问题所在。数据文件不受保护,可以使用另一个 MySQL 安装进行复制和访问。这不仅限于嵌入式服务器;这也是单机服务器的一个问题。这令人震惊吗?如果你的组织在开源软件的使用上有严格控制的限制,这是可能的。想象一下当你的信息保证官发现时他脸上的表情。好吧,所以你最好委婉地告诉他。因此,可能需要在嵌入式应用中包含额外的安全特性,以适当地保护嵌入式 MySQL 系统及其数据。

MySQL 嵌入的优势

MySQL 嵌入式 API 使开发者能够在另一个应用中使用全功能的 MySQL 服务器。最重要的好处是提高了数据访问的速度(因为服务器要么是应用的一部分,要么与应用运行在同一硬件上),内置的数据库管理工具,以及非常灵活的存储和检索机制。这些好处使开发者有机会整合使用 MySQL 的所有好处,同时对用户隐藏其实现。这意味着开发者可以通过利用 MySQL 的特性来增加他们自己产品的功能。

MySQL 嵌入的局限性

使用嵌入式 MySQL 服务器有一些限制。幸运的是,这是一个简短的列表。大多数限制是合理的,通常对系统集成商来说不是问题。表 6-1 列出了使用嵌入式 MySQL 系统的已知限制。每个都包括一个简短的描述。

表 6-1。使用嵌入式 MySQL 的局限性

限制 描述
安全 默认情况下,访问控制是关闭的。特权系统处于非活动状态。
复制 没有复制或记录功能。
从外部接近 不允许外部网络通信(除非您自己构建)。
装置 深度嵌入的应用(如libmysqld)可能需要额外的库来部署。
事件 事件计划程序不可用。
数据 嵌入式服务器像独立服务器一样存储数据,为每个数据库使用一个文件夹,为每个表使用一组文件。
版本 嵌入式服务器不支持 MySQL 5.1 的某些版本。
南非民主统一战线(United Democratic Front) 不允许使用用户定义的函数。
调试/跟踪 核心转储不会生成堆栈跟踪。
连通性 您不能通过网络协议连接到嵌入式服务器。请注意,您可以通过嵌入式应用提供这种外部访问。
资源 如果使用捆绑嵌入并支持大量数据和/或许多同时连接,可能会很繁重。

MySQL C API

乍一看 MySQL C API 文档(MySQL 参考手册中题为“API 和库”的一章)可能会令人生畏。嗯,确实是。C API 旨在封装独立服务器的所有功能。这不是一项简单或容易的任务。幸运的是,Oracle 在http://dev.mysql.com/doc提供了对 MySQL 文档的在线访问。查找在线参考手册的“libmysqld,嵌入式 MySQL 服务器库”小节。

image 注意在线文档通常是最新版本。如果为了方便起见,您已经下载了一个副本,您可能希望定期检查联机文档。通过重新检查在线文档,我找到了几个绊脚石的答案。

具有讽刺意味的是,也许 C API 最令人生畏的方面是文档本身。简单地说,它有点简洁,需要通读几遍,概念才会变得清晰。我的目标是以一个简短的教程和几个例子的形式向您介绍 C API,以帮助您快速启动嵌入式应用项目。

入门指南

我给想学习如何构建嵌入式应用的开发者的第一个建议是阅读文档。尽管有目前的文本和章节,在开始使用 API 之前通读产品文档总是一个好主意,即使你没有马上接受这些信息。我经常在 MySQL 文档中发现一些表面上看起来无关紧要的信息,但后来证明这些信息是成功编译和令人沮丧地寻找错误来源之间的关键。

我还建议登录 MySQL 网站,浏览论坛(在http://forums.mysql.com有一个专门的嵌入式论坛)和邮件列表(http://lists.mysql.com)存储库。您不必阅读所有内容,但是您的一些问题可能可以通过阅读这些存储库中的条目得到解答。我有时也会看看 MySQL 博客(www.planetmysql.org)。许多作者已经发布了关于嵌入式服务器和许多其他感兴趣的项目的信息。有如此多有趣的信息,有时我发现自己一次要读一个多小时。许多 MySQL 专家认为这种策略是成为 MySQL 大师的关键。信息就是力量。

在线文档、各种列表和博客绝对是 MySQL 最新信息的最佳来源。你应该做的最重要的阅读包含在以下章节中。我将介绍主要的 C API 函数,并通过一个简单的嵌入式应用示例进行演示。稍后,我将演示一个更复杂的嵌入式应用,用一个抽象的数据访问类完成,并用. NET 编写。

学习如何创建嵌入式应用的最好方法是自己编写一个。请随意打开您最喜欢的源代码编辑器,跟着我演示几个例子。我首先按照需要调用的顺序遍历每个需要调用的函数。然后,在后面的部分中,我将向您展示如何构建这个库并编写您的第一个嵌入式服务器应用。

最常用的功能

快速浏览一下文档就会发现 C API 支持超过 65 个函数。其中一些已经被否决了,但是 Oracle 非常擅长在文档中指出这一点(这是阅读它的另一个好理由)。只有少数功能是经常使用的。

库中的大多数函数都提供了连接和服务器操作函数。一些专用于收集关于服务器和数据的信息,而另一些用于提供执行查询和其他数据操作的调用。还有检索错误信息的函数。

表 6-2 列出了最常用的功能。包括函数的名称、简短描述和定义函数的源文件。这些函数大致按照在一个简单的嵌入式服务器示例中被调用的顺序列出。

表 6-2 。最常用的功能

功能 描述 来源
mysql_server_init() 初始化嵌入式服务器库。 libmysql.c
mysql_init() 启动服务器。 客户端. c
mysql_options() 允许您更改或设置服务器选项。 客户端. c
mysql_debug() 打开调试跟踪文件(DBUG)。 libmysql.c
mysql_real_connect() 建立与嵌入式服务器的连接。 客户端. c
mysql_query() 发出查询语句(SQL)。语句作为空终止字符串传递。 libmysql.c
mysql 存储结果() 检索上次查询的结果。 客户端. c
mysql_fetch_row() 从结果集中返回一行。 客户端. c
mysql_num_fields() 返回结果集中的字段数。 客户端. c
mysql_num_rows() 返回结果集中的行数(记录数)。 客户端. c
mysql_error() 返回描述上一个错误的格式化错误消息(字符串)。 客户端. c
mysql_errno() 返回上一个错误的错误号。 客户端. c
mysql 自由结果() 释放分配给结果集的内存。注意:不要忘记经常使用这个功能。对空结果集调用此方法不会生成错误。 客户端. c
mysql_close() 关闭与服务器的连接。 客户端. c
mysql 服务器端() 完成嵌入式服务器库并关闭服务器。 libmysql.c

image 我鼓励你在通读完本章并理解示例之后,花些时间通读 MySQL 参考手册 C API 部分的函数列表。您可能会发现一些满足您特殊数据库需求的有趣函数。

有关这些函数的完整描述,包括返回值和用法,请参见 MySQL 参考手册。

创建嵌入式服务器

在初始化函数调用期间,嵌入式服务器被建立为实例。大多数函数需要一个指向服务器实例的指针作为必需的参数。当您创建一个嵌入式 MySQL 应用时,您需要创建一个指向MYSQL对象的指针。您还需要为结果集和结果集中的一行(称为记录)创建实例。幸运的是,服务器的定义和主要结构都在 MySQL 头文件中定义。您需要使用的头文件(对于大多数应用)是:

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <mysql.h>

使用以下语句可以创建指向嵌入式服务器、结果集和记录结构的指针变量:

MYSQL *mysql;                         // the embedded server class
MYSQL_RES *results;                   // stores results from queries
MYSQL_ROW record;                     // a single row in a result set

这些语句允许您访问嵌入式服务器(MYSQL)、结果结构(MYSQL_RES)和记录(MYSQL_ROW)。您可以使用全局变量来定义这些指针。你们中的一些人可能不喜欢使用全局变量,也没有理由一定要使用。结果集和记录可以随意创建和销毁。只要确保在整个应用中保持MYSQL指针变量是同一个实例。

我们还没完成设置。我们仍然需要建立一些字符串,以便在连接过程中使用。我见过许多不同的方法来实现这一点,但最流行的方法是创建一个字符串数组。至少,您需要为my.cnf(在 Windows 中为my.ini)文件的位置和数据的位置创建字符串。一组典型的初始化字符串是:

static char *server_options[] = {"mysql_test",
  "--defaults-file=c:\\mysql_embedded\\my.ini",
  "--datadir=c:\\mysql_embedded\\data", NULL };

本章中的示例描述了 Windows 编译的服务器选项。如果您使用 Linux,您将需要使用适当的路径并将my.ini更改为my.cnf。在这个例子中,我使用了标签"mysql_test"(被mysql_server_init()忽略),my.cnf (my.ini)文件的位置到普通安装目录,数据目录到普通 MySQL 安装。如果要建立独立服务器和嵌入式服务器,应该为每台服务器使用不同的数据位置。为了保持整洁,您可能还想使用不同的配置文件。

为了帮助将错误降到最低,我还使用了一个整数变量来标识字符串数组中元素的数量(我稍后将讨论这一点)。这允许我编写边界检查代码,而不必记住允许多少个元素。我可以允许元素的数量在运行时改变,从而允许边界检查代码在必要时适应变化。

int num_elements=(sizeof(server_options) / sizeof(char *)) - 1;

最后一个设置步骤是创建另一个字符串数组,它标识包含我的配置文件(my.cnf)中任何附加服务器选项的服务器组。这定义了服务器启动时将要读取的部分。

static char *server_groups[] = {"libmysqld_server", "libmysqld_client", NULL };

正在初始化服务器

在连接到嵌入式服务器之前,必须对其进行初始化或启动。这通常涉及两个初始化调用,然后是任意数量的设置附加选项的调用。启动嵌入式服务器需要调用的第一个初始化函数是mysql_server_init() 2 。该功能定义为:

int mysql_server_init(int argc, char **argv, char **groups)

在调用任何其他函数之前,该函数只被调用一次。它将参数argcargv,作为程序的普通参数(与 main 函数相同)。此外,来自配置的组标签被传递以允许服务器读取运行时服务器选项。返回值要么是 0 表示成功,要么是 1 表示失败。这允许您在条件语句中调用函数,并在出现故障时采取行动。下面是使用启动部分的声明调用该函数的示例:

mysql_server_init(num_elements, server_options, server_groups);

image 注意为了让示例简短易懂,我避免在示例源代码中使用错误处理。我将在后面的例子中再次讨论错误处理。

你需要调用的第二个初始化函数是mysql_init()。这个函数在连接到服务器时为您分配MYSQL对象。该函数定义为:

MYSQL *mysql_init(MYSQL *mysql)

下面是使用前面定义的全局变量调用此函数的示例:

mysql = mysql_init(NULL);

注意,我使用了NULL来传递给函数。这是因为这是请求一个新的MYSQL对象实例的函数的第一次调用。在这种情况下,一个新的对象被分配和初始化。如果您调用了传入该对象的现有实例的函数,则该函数只初始化该对象。

如果有错误,函数返回NULL,如果成功,函数返回对象的地址。这意味着您可以将这个调用放在条件语句中,以便在失败时处理错误,或者简单地询问MYSQL指针变量来检测NULL

image 提示几乎所有的mysql_XXX函数成功返回 0,失败返回非零。只有那些返回指针的才返回非零表示成功,返回 0 ( NULL)表示失败。

设置选项

嵌入式服务器允许您在连接到服务器之前设置其他连接选项。用于设置连接选项的函数定义如下:

int mysql_options(MYSQL *mysql, enum mysql_option, const char *arg)

第一个参数是嵌入式服务器对象的实例。第二个参数是可能选项的枚举值,最后一个参数用于为使用可选字符串选择的选项传递参数值。选项列表有一个很长的可能值列表。一些更常用的选项及其值如表 6-3 所示。MySQL 参考手册中列出了完整的选项集。

表 6-3 。连接选项的部分列表

[计]选项 价值 描述
MYSQL _ OPT _ USE _ REMOTE _ CONNECTION 不适用的 强制连接使用远程服务器进行连接
MYSQL _ OPT _ USE _ EMBEDDED _ CONNECTION 不适用的 强制连接到嵌入式服务器
MYSQL_READ_DEFAULT_GROUP 指示服务器从配置文件中的指定组读取服务器配置选项
MYSQL _ SET _ 客户端 _IP 国际电脑互联网地址 为配置为使用身份验证的嵌入式服务器提供 IP 地址

以下对此函数的示例调用指示服务器从配置文件的[libmysqld_client]部分读取配置选项,并告诉服务器使用嵌入式连接:

mysql_options(mysql, MYSQL_READ_DEFAULT_GROUP, "libmysqld_client");
mysql_options(mysql, MYSQL_OPT_USE_EMBEDDED_CONNECTION, NULL);

对于成功,返回值为 0;对于任何无效或具有无效值的选项,返回值为非零。

连接到服务器

现在服务器已经初始化,所有选项都已设置,您可以连接到服务器了。你用来做这件事的函数叫做mysql_real_connect()。它有大量允许微调连接的参数。该函数被声明为;

MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *user, const
char *passwd, const char *db, unsigned int port, const char *unix_socket,
unsigned long client_flag)

此功能必须正确完成。如果失败(实际上,如果前面的任何功能失败),您将无法使用服务器,应该重新尝试连接到服务器或正常中止操作。

该函数的参数包括MYSQL实例、定义主机名(IP 地址或完全限定名)的字符串、用户名、密码、要使用的初始数据库的名称、要使用的端口号、要使用的 Unix 套接字,最后是启用特殊客户端行为的标志。有关客户端标志的更多详细信息,请参见 MySQL 参考手册。任何被指定为NULL的参数值将通知函数使用该参数的默认值。以下是对此函数的调用示例,它使用除数据库之外的所有默认值进行连接:

mysql_real_connect(mysql, NULL, NULL, NULL, "information_schema", 0, NULL, 0);

如果成功,该函数返回一个连接句柄,如果失败,则返回NULL。大多数应用不会捕获连接句柄。相反,它们检查NULL的返回值。请注意,我没有使用任何身份验证参数。这是因为默认情况下身份验证是关闭的。如果我在打开身份验证开关的情况下编译嵌入式服务器,就必须提供这些参数。最后,第四个参数是您想要连接的默认数据库的名称。该数据库必须存在,否则您可能会遇到错误。

至此,您应该拥有了设置变量以调用嵌入式服务器、初始化、设置选项和连接到嵌入式服务器所需的所有代码。下面显示了由前面的代码示例表示的这些操作:

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include "mysql.h"

MYSQL *mysql;                         //the embedded server class
MYSQL_RES *results;                   //stores results from queries
MYSQL_ROW record;                     //a single row in a result set

static char *server_options[] = {"mysql_test",
  "--defaults-file=c:\\mysql_embedded\\my.ini",
  "--datadir=c:\\mysql_embedded\\data", NULL };
int num_elements = (sizeof(server_options) / sizeof(char *)) - 1;
static char *server_groups[] = {"libmysqld_server", "libmysqld_client", NULL };

int main(void)
{
  mysql_server_init(num_elements, server_options, server_groups);
  mysql = mysql_init(NULL);
  mysql_options(mysql, MYSQL_READ_DEFAULT_GROUP, "libmysqld_client");
  mysql_options(mysql, MYSQL_OPT_USE_EMBEDDED_CONNECTION, NULL);
  mysql_real_connect(mysql, NULL, NULL, NULL, "INFORMATION_SCHEMA",
    0, NULL, 0);

...

  return 0;
}

运行查询

最后,我们谈到了好东西——使数据库系统成为数据库系统的核心:对特定查询的处理。允许您发出查询的函数是mysql_query()函数。该函数被声明为;

int mysql_query(MYSQL *mysql, const char *query)

该函数的参数是MYSQL对象实例和一个包含 SQL 语句的字符串(null 终止)。SQL 语句可以是任何有效的查询,包括数据操作语句(SELECTINSERTUPDATEDELETEDROP等)。).如果查询产生结果,可以使用方法mysql_store_result()mysql_fetch_row()将结果绑定到一个指针变量进行访问。如果没有返回结果,结果集将是NULL

调用此函数来检索服务器上的数据库列表的示例如下:

mysql_query(mysql, " SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA;")

如果成功,该函数的返回值为 0,如果失败,返回值为非零。

检索结果

发出查询后,接下来的步骤是获取结果集,并在结果指针变量中存储对结果集的引用。然后,您可以获取下一行(记录)并将其存储在记录结构(恰好是一个命名数组)中。完成这个过程的函数是mysql_store_result()mysql_fetch_row(),定义为;

MYSQL_RES *mysql_store_result(MYSQL *mysql)
MYSQL_ROW mysql_fetch_row(MYSQL_RES *result)

mysql_store_result()函数接受MYSQL对象作为其参数,并返回最近运行的查询的结果集的实例。如果出现错误或者上一次查询没有返回任何结果,该函数将返回NULL。此时,您必须注意通过调用mysql_errno()函数来检查错误。如果有错误,您必须调用错误函数,并将结果与已知错误列表进行比较。该函数生成的已知错误值有CR_OUT_OF_MEMORY(没有存储结果的可用内存)、CR_SERVER_GONE_ERRORCR_SERVER_LOST(与服务器的连接丢失),以及CR_UNKNOWN_ERR(一个指示服务器处于不可预测状态的总括错误)。

image 注意使用mysql_store_result()功能有许多可能的情况。这里描述了最常见的用法。要更详细地了解该函数的用法,或者如果您在诊断使用该函数的问题时遇到问题,请参阅 MySQL 参考手册了解更多详细信息。

mysql_fetch_row()函数接受结果集作为唯一的参数。如果结果集中没有更多的行,函数返回NULL。这很方便,因为它允许您在循环或迭代器中使用这个特性。如果该函数失败,仍然设置NULL的返回值。由您来检查mysql_errno()功能,看看是否发生了任何已定义的错误。这些错误包括指示连接失败的CR_SERVER_LOST,以及无处不在的“出错”错误指示器CR_UNKOWN_ERROR

这些调用一起用于查询表并将结果打印到控制台的示例有:

mysql_query(mysql, "SELECT ItemNum, Description FROM tblTest");
results = mysql_store_result(mysql);
while(record=mysql_fetch_row(results))
{
  printf("%s\t%s\n", record[0], record[1]);
}

注意,在查询运行之后,我调用了mysql_store_result()函数来获得结果;然后,我将mysql_fetch_row()函数放在我的循环评估中。由于mysql_fetch_row()在没有更多行可用时返回NULL(在记录集的末尾),循环将在该点终止。当有行时,我使用数组下标(从 0 开始)访问行中的每一列。

这个例子演示了对嵌入式服务器的所有查询的基本结构。您可以包装这个过程,并将其包含在一个类或一组抽象的函数中。我在第二个嵌入式应用示例中演示了这一点。

清除

从查询返回并放入结果集中的数据需要分配资源。因为我们是优秀的程序员,所以我们努力释放不再需要的内存以避免内存泄漏。 3 Oracle 提供了 mysql_free_result()函数来帮助释放那些资源。该功能定义为:

void mysql_free_result(MYSQL_RES *result)

这个函数是调用安全的,这意味着您可以使用已经释放的结果集调用它,而不会产生错误。这只是以防你高兴起来,开始到处扔“自由”代码。别笑——我见过“免费”电话比“新”电话多的程序。大多数情况下这不是问题,但是如果免费调用使用不当,过多的免费调用可能会释放一些您不想释放的东西。与新操作一样,您应该谨慎使用 free 操作。

以下是调用此函数释放结果集的示例:

mysql_free_result(results);

断开与服务器的连接并完成服务器

当您使用完嵌入式服务器后,您需要断开并关闭它。这可以通过使用 mysql_close()mysql_server_end() 4 函数来实现。close 函数关闭连接,另一个函数终结服务器并释放内存。这些功能定义如下:

void mysql_close(MYSQL *mysql);
void mysql_server_end();

这些函数的调用示例如下所示。请注意,这些是您需要进行的最后一次函数调用,通常在关闭应用时调用。

mysql_close(mysql);
mysql_server_end();

把这一切放在一起

现在,让我们一起来看看这些代码。清单 6-1 显示了一个完整的嵌入式服务器,它列出了可以从给定的数据目录访问的数据库。我将在后面的小节中介绍构建和运行这个示例的过程。

image 下面的例子是为 Windows 写的。一个 Linux 示例将在后面的章节中讨论。

清单 6-1 嵌入式服务器应用实例

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include "mysql.h"

MYSQL *mysql;                         //the embedded server class
MYSQL_RES *results;                   //stores results from queries
MYSQL_ROW record;                     //a single row in a result set

static char *server_options[] = {"mysql_test",
  "--defaults-file=c:\\mysql_embedded\\my.ini",
  "--datadir=c:\\mysql_embedded\\data", NULL };
int num_elements = (sizeof(server_options) / sizeof(char *)) - 1;
static char *server_groups[] = {"libmysqld_server", "libmysqld_client", NULL };

int main(void)
{
  mysql_server_init(num_elements, server_options, server_groups);
  mysql = mysql_init(NULL);
  mysql_options(mysql, MYSQL_READ_DEFAULT_GROUP, "libmysqld_client");
  mysql_options(mysql, MYSQL_OPT_USE_EMBEDDED_CONNECTION, NULL);
  mysql_real_connect(mysql, NULL, NULL, NULL, "information_schema",
    0, NULL, 0);
  mysql_query(mysql, "SHOW DATABASES");                  // issue query
  results = mysql_store_result(mysql);                    // get results
  printf("The following are the databases supported:\n");
  while(record=mysql_fetch_row(results))                  // fetch row
  {
    printf("%s\n", record[0]);                            // process row
  }
  mysql_query(mysql, "CREATE DATABASE testdb1;");
  mysql_query(mysql, "SHOW DATABASES;");                  // issue query
  results = mysql_store_result(mysql);                    // get results
  printf("The following are the databases supported:\n");
  while(record=mysql_fetch_row(results))                  // fetch row
  {
    printf("%s\n", record[0]);                            // process row
  }
  mysql_free_result(results);
  mysql_query(mysql, "DROP DATABASE testdb1;");           // issue query
  mysql_close(mysql);
  mysql_server_end();
  return 0;
}

错误处理

您可能想知道在前一章中读到的所有错误处理都发生了什么。这些工具在 C API 中。Oracle 提供了两个函数来处理错误。第一个是msyql_errno(),从最近的错误中检索错误号。第二个是mysql_error(),检索最近错误的相关错误消息。这些功能定义如下:

unsigned int mysql_errno(MYSQL *mysql)
const char *mysql_error(MYSQL *mysql)

为这两个函数传递的参数是MYSQL对象。因为这些方法是错误处理程序,所以它们不会失败。然而,如果在没有错误发生时调用它们,mysql_errno()返回 0,mysql_error()返回一个空字符串。

以下是对这些函数的一些调用示例:

if(somethinggoeshinkyhere)
{
  printf("There was an error! Error number : %d = $s\n",
    mysql_errno(&mysql), mysql_error(&mysql));
}

咻!这就是全部了。我希望我的解释能澄清参考手册中的迷雾。我写这一节主要是因为我觉得没有任何像样的例子可以帮助您学习如何使用嵌入式服务器——至少没有一个例子能在短短的几页中说明需要什么。

构建嵌入式 MySQL 应用

前几节向您介绍了嵌入式 MySQL 应用中使用的基本函数。本节将向您展示如何实际构建一个。我首先向您展示如何编译应用,然后讨论构建嵌入式库调用的方法。我还提供了两个示例应用,供您在自己的系统中进行实验。

我还简要介绍了对核心 MySQL 源代码的修改。是的,我知道这可能有点吓人,但我会一步一步地告诉你所有的细节。幸运的是,这是一个简单的修改,只需要更改两个文件。

我鼓励你阅读我包含的源代码。我知道有很多,但我已经把它精简到我认为可以控制的范围。通过阅读 MySQL 源代码,我学到了很多有趣的东西。我的目标是,通过研究这些示例的源代码,您可以获得构建自己的嵌入式 MySQL 应用的更多见解。

编译库(libmysqld )

在使用嵌入式库(libmysqld)之前,您需要编译它。MySQL 二进制文件的一些发行版可能不包括预编译的嵌入式库。嵌入式库包含在大多数源代码发行版中,可以在源代码树根下的/libmysqld目录中找到。该库通常是在没有调试信息的情况下构建的。您将希望有一个支持调试的版本用于您的开发。

在 Linux 上编译 libmysqld

要在 Linux 下编译这个库,使用configure脚本设置配置,然后执行一个普通的makemake install步骤。您将需要的配置参数是--with-debug--with-embedded-server。下面显示了完整的过程。从源代码目录的根目录运行。编译过程可能需要一段时间,所以您可以在继续阅读的同时开始编译。编译可能需要几分钟到大约一个小时的时间,这取决于机器的速度以及之前是否用调试信息构建了系统。

image 注意以下命令构建服务器并将其安装到默认位置。这些操作需要 root 权限。

cmake . -DWITH_EMBEDDED_SERVER=ON -DWITH_DEBUG=ON
make
sudo make install

image 提示您也可以使用图形界面中的cmake-gui .命令来设置参数。一旦设置好选项,点击Configure and,然后点击Generate

在 Windows 上编译 libmysqld

要在 Windows 下编译库,请启动 Visual Studio 并打开根源代码目录中的主解决方案文件(mysql.sln)。打开调试只是选择libmysqld项目并将构建配置设置为Debug Win32。您可以用通常的方式编译这个库,首先在当前项目中点击选择它,然后选择 Build image Build 或者构建完整的解决方案。任何依赖项目都将根据需要构建。编译过程可能需要一段时间,所以您可以在继续阅读的同时开始编译。根据机器的速度以及之前是否已经使用调试信息构建了系统,编译可能需要几分钟到半小时的时间。

调试呢?

您可能想知道在嵌入式库中调试是否和在独立服务器中一样。的确如此。事实上,您可以使用相同的调试方法。在运行时调试嵌入式服务器有点困难,但是因为服务器应该是嵌入式的,所以不太可能需要调试到那个级别。为了帮助调试应用,您可能需要创建一个跟踪文件。

我在上一章解释了几种调试技术。DBUG 包是最强大和最容易使用的包之一。虽然嵌入式服务器已经连接了所有的管道,并且确实遵循了标记所有函数入口和出口的相同调试实践,但是 DBUG 包并没有通过嵌入式库公开。

您可以创建自己的 DBUG 包实例,并使用它来编写自己的跟踪文件。对于使用嵌入式服务器的大型应用,您可以选择这样做。大多数应用都很小,所以增加的工作没有什么帮助。在这种情况下,如果嵌入式库提供了调试选项,那就太酷了。

DBUG 包既可以通过配置文件打开,也可以通过直接调用嵌入式库打开。当然,这假设您的嵌入式库是在启用调试的情况下编译的。

在运行时打开跟踪文件需要调用嵌入式库。方法是mysql_debug(),,它采用一个指定调试选项的字符串参数。下面的示例在运行时打开跟踪文件,指定更常用的选项并指示库将跟踪文件写入根目录。应该在连接到服务器之前调用此方法。

mysql_debug("debug=d:t:i:O,\\mysqld_embedded.trace");

image 提示为你的嵌入式服务器跟踪使用不同的文件名。这将有助于将嵌入式服务器跟踪与您可能运行的任何其他独立服务器区分开来。

您也可以使用配置文件打开调试。只需将前一个例子中的字符串放入您的源代码在启动时指定的my.cnf (my.ini)文件中(稍后会详细介绍)。

如果您想从嵌入式应用中使用 DBUG 包,但不想在自己的代码中包含 DBUG 包,该怎么办?你只是运气不好吗?嵌入式库没有公开 DBUG 方法,但是它可以!以下段落解释了修改嵌入式服务器以包含简单 DBUG 方法的过程。我用一个简单的例子,因为我还不想让你陷入困境。

你需要做的第一件事是备份原始源代码。如果你下载了一个焦油或压缩文件,你没事。如果您发现自己在添加了一些代码后还在努力让服务器编译,那么回到最初的副本会对您的压力水平(和理智)产生深远的影响。如果您已经删除了更改,但仍然无法编译,这一点尤其正确!

添加一个新方法真的很简单。编辑/include目录中的mysql.h文件并添加定义。我选择创建一个公开DBUG_PRINT函数的方法。我把它简单地命名为mysql_dbug_print()清单 6-2 显示了这个方法的函数定义。请注意,该函数接受单个字符指针。我用它来传入我在嵌入式应用中定义的字符串。这允许我向跟踪文件中写入一个字符串,作为我的嵌入式应用与来自嵌入式服务器的跟踪同步的某种标记。

清单 6-2对 mysql.h 的修改

/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Adds a method to permit embedded applications to call DBUG_PRINT */
void STDCALL mysql_dbug_print(const char *a);
/* END CAB MODIFICATION */

要创建函数,编辑/libmysqld/libmysqld.c并将函数添加到源代码的其余部分。位置并不重要,只要它在源代码主体的某个地方。我选择将它放在其他公开的库函数附近(第 89 行附近)。清单 6-3 显示了这个方法的代码。注意,代码只是将字符串回显到了DBUG_PRINT方法中。请注意,我还在传递的字符串末尾添加了一个字符串。这有助于我定位来自我的应用的所有跟踪行,而不管我传递什么来打印。

清单 6-3 对 libmysqld.c 的修改

/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Adds a method to permit embedded applications to call DBUG_PRINT */
void STDCALL mysql_dbug_print(const char *a)
{
  DBUG_PRINT(a, (" -- Embedded application."));
}
/* END CAB MODIFICATION */

要将方法添加到 Windows 中的嵌入式库中,您还必须修改libmysqld_exports.def文件以包含新方法。清单 6-4 显示了一个简化的清单作为例子。这里,我已经将mysql_dbug_print()语句添加到文件中。请注意,该文件是按字母顺序维护的。

清单 6-4 对 libmysqld_exports.def 的修改

LIBRARY    LIBMYSQLD
DESCRIPTION  'MySQL 5.6 Embedded Server Library'
VERSION    5.6
EXPORTS
  _dig_vec_upper
  _dig_vec_lower
...
  mysql_dbug_print
  mysql_debug
  mysql_dump_debug_info
  mysql_eof
...

就这样!现在只需重新编译嵌入式服务器,您的新方法就可以在应用中使用了。我已经对我的嵌入式服务器安装完成了这一步。下面的示例使用此方法将字符串写入跟踪文件。这极大地帮助了我在跟踪文件中找到与我的源代码同步的点。

image 提示在前面的清单中,我使用了我在第 3 章中介绍的相同的注释策略。这将有助于您在需要迁移到新版本时识别与源代码的任何差异。

那的数据呢?

在开始创建和运行您的第一个嵌入式 MySQL 应用之前,请考虑您想要使用的数据。如果您计划创建一个嵌入式应用,该应用提供一个管理界面,允许您创建表格并填充它们,那么您就万事俱备了。如果您还没有计划这样的接口或类似的设施,那么您将需要使用其他工具来配置数据库。

幸运的是,只要使用较简单的表类型(比如 MyISAM),就可以使用独立的服务器和您喜欢的实用程序来创建数据库和表并填充它们。如果您使用 InnoDB,您应该使用--innodb_file_per_table选项启动服务器,或者创建 MySQL 的全新安装,添加您的数据,然后将数据目录和 InnoDB 文件复制到新位置。创建数据后,您可以将目录从单机服务器安装的数据目录复制到另一个位置。请记住,将嵌入式服务器数据位置与独立服务器数据位置分开非常重要。记下您放置数据的位置,因为您的嵌入式应用将需要这些数据。

我在我所有的例子和我自己的嵌入式应用中使用了这种技术。它让我能够形成和填充我想首先使用的数据,而不必担心创建管理界面。大多数嵌入式 MySQL 应用都是这样构建的。

创建一个基本的嵌入式服务器

前面几节向您展示了使用嵌入式库所需的所有必要功能。我向您展示了一个简单的例子,它使用了我描述的所有函数。我包含了一个 Linux 和 Windows 的例子。虽然它们几乎完全相同,但在源代码中还是有一些细微的差别。最大的区别在于程序是如何编译的。本章中的示例假设您正在使用一个已经编译了调试信息的嵌入式库。

示例程序读取嵌入式服务器的数据目录中的数据库列表,将列表打印到控制台,创建名为testdb1的新数据库,再次读取数据库列表,将列表打印到控制台,最后删除数据库testdb1。虽然不太复杂,但所有示例函数调用都得到了练习。我还包含了打开跟踪文件(DBUG)的调用,以及使用嵌入式库中新的mysql_dbug_print()函数将信息打印到跟踪文件的调用。

Linux 示例

您需要创建的第一个文件是配置文件(my.cnf)。您可以使用现有的配置文件,但是我建议将它复制到您的嵌入式服务器的位置。例如,如果您创建了一个名为/var/lib/mysql_embedded的目录,您将把配置文件放在那里,并将所有数据目录(数据库文件和文件夹)也复制到那个目录中。那些是唯一需要在那个目录中的文件。唯一的例外是,如果您想为您的嵌入式服务器使用不同的语言。在这种情况下,我建议将适当的文件从独立安装复制到您的嵌入式服务器目录中,并从配置文件中引用它们。清单 6-5 显示了示例程序的配置文件。

清单 6-5Linux 版 my.cnf 文件示例

[libmysqld_server]
basedir=/var/lib/mysql_embedded
datadir=/var/lib/mysql_embedded
#slow query log#=
#tmpdir#=
#port=3306
#set-variable=key_buffer=16M

[libmysqld_client]
#debug=d:t:i:O,\\mysqld_embedded.trace

注意,我已经禁用了大多数选项(通过使用行首的符号)。我通常这样做,以便在需要的时候可以轻松快速地打开它们。调试是关闭的,这样我可以向您展示如何通过编程方式打开它。

您需要创建的下一个文件是应用的源代码。如果您已经按照前面的 C API 教程进行了学习,它应该看起来非常熟悉。清单 6-6 显示了一个简单的嵌入式 MySQL 应用的完整源代码。

清单 6-6 嵌入示例 1 (Linux: example1_linux.c)

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include "mysql.h"

MYSQL *mysql;                         //the embedded server class
MYSQL_RES *results;                   //stores results from queries
MYSQL_ROW record;                     //a single row in a result set

/*
  These variables set the location of the ini file and data stores.
*/
static char *server_options[] = {"mysql_test",
  "--defaults-file=/var/lib/mysql_embedded/my.cnf",
  "--datadir=/var/lib/mysql_embedded", NULL };
int num_elements = (sizeof(server_options) / sizeof(char *)) - 1;
static char *server_groups[] = {"libmysqld_server", "libmysqld_client", NULL };

int main(void)
{
  /*
    This section initializes the server and sets server options.
  */
  mysql_server_init(num_elements, server_options, server_groups);
  mysql = mysql_init(NULL);
  mysql_options(mysql, MYSQL_READ_DEFAULT_GROUP, "libmysqld_client");
  mysql_options(mysql, MYSQL_OPT_USE_EMBEDDED_CONNECTION, NULL);
  /*
    The following call turns debugging on programmatically.
    Comment out to turn off debugging.
  */
  //mysql_debug("d:t:i:O,\\mysqld_embedded.trace");
  /*
    Connect to embedded server.
  */
  mysql_real_connect(mysql, NULL, NULL, NULL, "information_schema",
    0, NULL, 0);
  /*
    This section executes the following commands and demonstrates
    how to retrieve results from a query.

    SHOW DATABASES;
    CREATE DATABASE testdb1;
    SHOW DATABASES;
    DROP DATABASE testdb1;
  */
  mysql_dbug_print("Showing databases.");                 //record trace
  mysql_query(mysql, "SHOW DATABASES;");                  //issue query
  results = mysql_store_result(mysql);                    //get results
  printf("The following are the databases supported:\n");
  while(record=mysql_fetch_row(results))                  //fetch row
  {
    printf("%s\n", record[0]);                            //process row
  }
  mysql_dbug_print("Creating the database testdb1.");     //record trace
  mysql_query(mysql, "CREATE DATABASE testdb1;");
  mysql_dbug_print("Showing databases.");
  mysql_query(mysql, "SHOW DATABASES;");                  //issue query
  results = mysql_store_result(mysql);                    //get results
  printf("The following are the databases supported:\n");
  while(record=mysql_fetch_row(results))                  //fetch row
  {
    printf("%s\n", record[0]);                            //process row
  }
  mysql_free_result(results);
  mysql_dbug_print("Dropping database testdb1.");         //record trace
  mysql_query(mysql, "DROP DATABASE testdb1;");           //issue query
  /*
    Now close the server connection and tell server we're done (shutdown).
  */
  mysql_close(mysql);
  mysql_server_end();

  return 0;
}

我添加了一些注释(有些人会说是多余的)来帮助您理解代码。我做的第一件事是创建全局变量并设置初始化数组。然后,我用数组选项初始化服务器,再设置几个选项,并连接到服务器。示例应用的主体从数据库中读取数据并打印出来。该示例的最后一部分关闭并终结服务器。

在编译这个例子时,您可以使用mysql_config脚本来标识库的位置。该脚本向命令行返回传递给它的每个选项的实际路径。您也可以从命令行运行该脚本,并查看所有选项及其值。编译该示例的示例命令是:

gcc example1_linux.c -g -o example1_linux -lstdc++ -I./include -L./lib -lmysqld -lpthread -ldl -lcrypt -lm -lrt

这个命令应该适用于大多数 Linux 系统,但是在某些情况下,这可能是一个问题。如果您的 MySQL 安装在另一个位置,您可能需要用mysql_config脚本修改这个短语。如果您的系统上安装了多个 MySQL,或者您在另一个位置安装了嵌入式库,您可能无法使用mysql_config脚本,因为它将返回错误的库路径。对于安装了多个版本的 MySQL 源代码的情况也是如此。您当然希望避免使用一个版本的服务器的包含文件来编译另一个版本的嵌入式库。如果你没有早期的glibc库,你也会遇到问题。

image 注意如果您正在从源代码树编译嵌入式服务器,并且您正在使用 mysql_config,那么您必须设置 cmake 选项-DCMAKE_INSTALL_PREFIX=ON。

要纠正这些问题,首先从命令行运行mysql_config脚本,并记下库的路径。您还应该找到要使用的库和头文件的正确路径。下面是我如何克服这些问题的一个例子(我在我的 SUSE 机器上遇到了所有这些情况):

g++ example1_linux.c -g -o example1_linux -lz -I/usr/include/mysql
-L/usr/lib/mysql -lmysqld -lz -lpthread -lcrypt -lnsl -lm -lpthread -lc
-lnss_files -lnss_dns -lresolv -lc -lnss_files -lnss_dns -lresolv -lrt

注意,我使用了更新的g++编译器,而不是普通的gcc。这是因为我的系统有最新的 GNU 库,没有旧的。当然,我可以加载旧的库并修复这个问题,但是输入g++要容易得多。好吧,所以我们程序员很懒。

清单 6-7 显示了在 MySQL 的典型安装下运行这个例子的输出示例。在本例中,我将独立服务器目录中的所有数据复制到我的嵌入式服务器目录中。

清单 6-7 样本输出

linux:/home/Chuck/source/Embedded # ./example1_linux
The following are the databases supported:
information_schema
mysql
test
The following are the databases supported:
information_schema
mysql
test
testdb1
linux:/home/Chuck/source/Embedded #

请花一些时间在您自己的机器上研究这个示例应用。我建议您对应用的主体进行试验,并运行一些自己的查询,以获得如何编写自己的嵌入式 MySQL 应用的感觉。如果您在嵌入式库中实现了mysql_dbug_print()函数,那么通过删除对mysql_debug()函数调用的注释或者删除配置文件中对debug选项的注释,在示例中尝试一下。

下一个例子将向您展示如何封装嵌入的库调用;它展示了它们在更现实的应用中的用途。

Windows 示例

您需要创建的第一个文件是配置文件(my.ini)。您可以使用现有的配置文件,但是我建议将它复制到您的嵌入式服务器的位置。例如,如果您创建了一个名为c:/mysql_embedded的目录,那么您应该将配置文件放在那里,并将所有数据目录也复制到该目录中。那些是唯一需要在那个目录中的文件。唯一的例外是,如果您想为您的嵌入式服务器使用不同的语言。在这种情况下,我建议将适当的文件从独立安装复制到您的嵌入式服务器目录中,并从配置文件中引用它们。清单 6-8 显示了示例程序的配置文件。包括最常用的选项以及它们在文件中的指定位置。

清单 6-8Windows 版示例 my.ini 文件

[mysqld]
basedir=C:/mysql_embedded
datadir=C:/mysql_embedded/data
language=C:/mysql_embedded/share/english

[libmysqld_client]
#debug=d:t:i:O,\\mysqld_embedded.trace

创建项目文件有点复杂。为了充分利用 Visual Studio,我建议从源代码目录的根目录打开主解决方案文件(mysql.sln ),并将您的新应用作为新项目添加到该解决方案中。您不必将源代码存储在同一个源代码树中,但是您应该以这样一种方式存储它,以便知道它适用于哪个版本的源代码。

您可以使用项目向导创建项目。应该选择 C++ image Win32 控制台项目模板(Visual Studio 2012 中的 TemplatesimageVisual c++imageWin32)并给项目命名。这将在向导中指定的文件夹的根目录下创建一个与项目同名的新文件夹。您应该创建一个空项目并添加您自己的源文件。

创建一个项目文件作为解决方案的子项目会给你带来一些非常棒的优势。利用自动化构建过程(没有生成文件——耶!),将libmysqld项目添加到项目依赖项中。你可以从项目image的项目依赖菜单中打开项目依赖工具。使用解决方案的配置下拉框将生成配置设置为活动(调试),并使用标准工具栏上的解决方案的平台下拉框将平台设置为活动(Win32)。

您还需要在项目属性中设置一些开关。通过选择项目image属性或右键单击项目并选择属性.打开项目-属性对话框。您要检查的第一项是运行时库生成。通过展开树中的 C/C++标签,单击树中的代码生成标签,并从运行时库下拉列表中选择它,将此开关设置为多线程调试 DLL (/MDd)。此选项使您的应用使用特定于调试多线程和 DLL 的运行时库版本。图 6-1 显示了项目属性对话框和该选项的位置。

9781430246596_Fig06-01.jpg

图 6-1。项目属性对话框,显示代码生成页面

接下来,将 MySQL include 目录添加到项目属性中。最简单的方法是展开 C/C++标签并单击命令行标签。这将显示命令行参数。要添加新参数,请在附加选项文本框中键入它。在这种情况下,您需要添加一个选项,例如:

/I ../include

如果您的项目不在 MySQL 源代码树下,您可能需要相应地修改参数。图 6-2 显示了项目属性对话框和该选项的位置。

9781430246596_Fig06-02.jpg

图 6-2。项目属性对话框:命令行页面

如果不想(或不需要)使用预编译头,也可以移除预编译头选项。此选项位于“项目属性”对话框中的“C/C++预编译头”页上。

使用项目image添加引用菜单选项将 libmysqld 项目添加到 example1_win32 项目。将打开一个对话框(如图 6-3 所示),允许您在解决方案中选择一个项目作为参考。选择 libmysqld 项目。图 6-4 显示了添加了 libmysqld 项目引用的项目属性对话框。不执行这一步将导致编译时出现大量未定义的符号错误。

9781430246596_Fig06-03.jpg

图 6-3。选择一个项目参考

9781430246596_Fig06-04.jpg

图 6-4。项目属性对话框:参照

现在您已经正确配置了项目,如果您在创建项目时选择了创建基本项目文件,请添加您的源文件或粘贴示例代码。清单 6-9 显示了完整的 Windows 版本。

清单 6-9。 Embedded Example 1 (Windows: example1_win32.cpp)

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include "mysql.h"

MYSQL *mysql;                         //the embedded server class
MYSQL_RES *results;                   //stores results from queries
MYSQL_ROW record;                     //a single row in a result set

/*
  These variables set the location of the ini file and data stores.
*/
static char *server_options[] = {"mysql_test",
  "--defaults-file=c:\\mysql_embedded\\my.ini",
  "--datadir=c:\\mysql_embedded\\data", NULL };
int num_elements = (sizeof(server_options) / sizeof(char *)) - 1;
static char *server_groups[] = {"libmysqld_server", "libmysqld_client", NULL };

int main(void)
{
  /*
    This section initializes the server and sets server options.
  */
  mysql_server_init(num_elements, server_options, server_groups);
  mysql = mysql_init(NULL);
  mysql_options(mysql, MYSQL_READ_DEFAULT_GROUP, "libmysqld_client");
  mysql_options(mysql, MYSQL_OPT_USE_EMBEDDED_CONNECTION, NULL);
  /*
    The following call turns debugging on programmatically.
    Comment out to turn off debugging.
  */
  mysql_debug("d:t:i:O,\\mysqld_embedded.trace");
  /*
    Connect to embedded server.
  */
  mysql_real_connect(mysql, NULL, NULL, NULL, "information_schema",
    0, NULL, 0);
  /*
    This section executes the following commands and demonstrates
    how to retrieve results from a query.

    SHOW DATABASES;
    CREATE DATABASE testdb1;
    SHOW DATABASES;
    DROP DATABASE testdb1;
  */
  mysql_dbug_print("Showing databases.");                 //record trace
  mysql_query(mysql, "SHOW DATABASES;");                  //issue query
  results = mysql_store_result(mysql);                    //get results
  printf("The following are the databases supported:\n");
  while(record=mysql_fetch_row(results))                  //fetch row
  {
    printf("%s\n", record[0]);                            //process row
  }
  mysql_dbug_print("Creating the database testdb1.");     //record trace
  mysql_query(mysql, "CREATE DATABASE testdb1;");
  mysql_dbug_print("Showing databases.");
  mysql_query(mysql, "SHOW DATABASES;");                  //issue query
  results = mysql_store_result(mysql);                    //get results
  printf("The following are the databases supported:\n");
  while(record=mysql_fetch_row(results))                  //fetch row
  {
    printf("%s\n", record[0]);                            //process row
  }
  mysql_free_result(results);
  mysql_dbug_print("Dropping database testdb1.");         //record trace
  mysql_query(mysql, "DROP DATABASE testdb1;");           //issue query
  /*
    Now close the server connection and tell server we’re done (shutdown).
  */
  mysql_close(mysql);
  mysql_server_end();

  return 0;
}

我添加了一些注释(有些人会说是多余的)来帮助您理解代码。我做的第一件事是创建全局变量并设置初始化数组。然后,我用数组选项初始化服务器,如果需要,再设置几个选项,并连接到服务器。示例应用的主体从数据库中读取数据并打印出来。该示例的最后一部分关闭并终结服务器。

编译这个例子非常简单。只需选择项目,然后选择项目image构建,或者右键单击项目并选择构建。如果你已经编译了libmysqld项目,你应该看到的只是例子的编译。如果出于某种原因,目标文件对于libmysqld或它的任何依赖项来说都是过期的,Visual Studio 也会编译这些文件。

image 注意你可能会在mysql_com.h或者类似的头文件中遇到一些非常奇怪的错误。最有可能的原因是优化策略。微软自动将#define WIN32_LEAN_AND_MEAN语句包含在stdafx.h文件中。如果你打开了它,它会告诉编译器忽略一些不需要的包含和链接(正常情况下)。您可能希望将这一行完全删除(或者将其注释掉)。你的程序现在应该编译没有错误。如果您选择不使用stdafx文件,您应该不会遇到这个问题。

编译完成后,您可以从“调试”菜单命令运行程序,或者打开命令窗口并从命令行运行程序。如果这是您第一次使用,您应该会看到如下错误消息:

This application has failed to start because LIBMYSQLD.dll was not found. 
Re-installing the application may fix this problem.

这个错误的原因与错误消息中的第二句话无关。这意味着嵌入库不在搜索路径中。如果你曾经和。NET 或 COM 应用,并且从未使用过 C 库,您可能从未遇到过该错误。不像。NET 和 COM、C 库没有在全局程序集缓存(GAC)或注册表中注册。这些库(dll)应该与调用它们的应用放在一起,或者至少放在一个执行路径上。大多数开发者将 DLL 的副本放在执行目录中。

要解决这个问题,请将libmysqld.dll文件从lib_debug目录复制到example1_win32.exe文件所在的目录(或将lib_debug添加到执行路径)。一旦你越过了这个障碍,你应该会看到类似于清单 6-10 所示的输出。

清单 6-10。 示例输出

c:\source\mysql-5.6\example1_win32\Debug>example1_win32
The following are the databases supported:
information_schema
cluster
mysql
test
The following are the databases supported:
information_schema
cluster
mysql
test
testdb1

请花一些时间在您自己的机器上研究这个示例应用。我建议您对应用的主体进行实验,并运行一些自己的查询,以获得如何编写自己的嵌入式 MySQL 应用的感觉。如果您在嵌入式库中实现了mysql_dbug_print()函数,那么通过删除对mysql_debug()函数调用的注释或者删除配置文件中对debug选项的注释,在示例中尝试一下。

错误处理呢?

你们中的一些人可能想知道错误处理。具体来说,如何检测嵌入式服务器的问题并妥善处理它们?许多嵌入式库调用都有错误代码,您可以对其进行查询和操作。前面几节描述了我将使用的函数的返回值。虽然我在第一个嵌入式 MySQL 示例中没有包括太多的错误处理,但我会在下一个示例中介绍。请注意我是如何捕获错误并处理向客户端发送错误的。

嵌入式服务器应用

前面的例子展示了如何创建一个基本的嵌入式 MySQL 应用。虽然这些示例展示了如何连接和读取专用 MySQL 安装中的数据,但它们并不是构建您自己的嵌入式应用的好模型,因为除了最琐碎的需求之外,它们没有足够的覆盖面。哦,他们没有任何错误处理!这一章中的例子虽然是虚构的,但都是为了给你提供构建一个真正的嵌入式应用所需的工具。

这个应用被称为 售书机(BVM),是一个嵌入式系统,旨在运行在基于微软 Windows 的专用 PC 上,带有触摸屏。该系统及其其他输入设备安装在专门用于分发书籍的机械售货机中。BVM 背后的想法是允许出版商以半移动包的形式提供他们最受欢迎的图书,供应商可以根据需要进行配置和补充。BVM 将允许出版商在空间有限的地方安装他们的自动售货机,如贸易展览、机场和购物中心。这些地区通常有大量对购买印刷书籍感兴趣的顾客。BVM 减少了对店面和员工的需求,从而为出版商节省了资金。

注我经常发现自己怀疑这个想法是否被考虑过。我读过几篇预测按需印刷持续增长的文章,但我很少看到任何关于售书机如何工作的文章。我知道一些出版商已经安装了一些原型,但是这些试验并没有产生太多的热情。我选择用这个例子来增加一些真实性。我也阅读技术书籍,并经常发现自己对不切实际或琐碎的例子感到厌烦。这里有一个例子,我希望你同意至少是可信的。

界面

该应用需要一个双 接口;一个用于正常的自动售货机活动,另一个允许供应商重新进货自动售货机,根据需要调整信息。自动售货机界面旨在为客户提供一组按钮,这些按钮为自动售货机中的特定插槽提供图书缩略图。由于大多数现代自动售货机使用产品按钮,当产品可用时,按钮被照亮,当产品耗尽时,按钮变暗或关闭,BVM 界面在该槽中的产品可用时启用按钮,当产品耗尽时禁用按钮。

当顾客点击一个产品按钮时,屏幕变成一个简短、详细的显示,描述这本书并给出它的价格。如果顾客想购买这本书,她可以点击购买,并提示付款。这个应用就是用来模拟这些活动的。一个真正的实现将调用适当的硬件控制库来接收付款,验证付款,并使用自动售货机的机械部分来从指定的插槽分发产品。图 6-5 显示了售书机的主界面。图 6-6 显示了一些书籍的低数量的影响。

9781430246596_Fig06-05.jpg

图 6-5。售书机客户界面

9781430246596_Fig06-06.jpg

图 6-6。产生客户界面的“产品耗尽”视图

图 6-7 显示了其中一本书的细节示例。

9781430246596_Fig06-07.jpg

图 6-7。图书详情界面

如果没有任何补充产品的方法,自动售货机就不会很有用。BVM 通过管理界面提供这种功能。当供应商需要补充书籍或更改细节以匹配不同的书籍集时,供应商打开机器并关闭嵌入式应用(该功能必须添加到示例中)。然后,供应商将重启应用,在命令行上提供管理员开关,如下所示:

C:\>Books BookVendingMachine -admin

管理界面允许供应商输入特定查询并执行它。图 6-8 显示了管理界面。该示例显示了重置产品数量的典型更新操作。该接口允许供应商输入她需要的任何查询来为嵌入式应用重置数据。

9781430246596_Fig06-08.jpg

图 6-8。管理界面

数据和数据库

本例中的 数据是在独立的 MySQL 服务器上创建的,并被复制到嵌入式 MySQL 目录中。当我创建这个应用时,我首先设计了数据结构和数据库来保存数据。这总是一个好主意。

image 注意有些开发者可能不同意,他们认为最好从用户界面设计开始,让数据需求不断发展。这两种做法都不比另一种好。重要的一点是,数据必须是设计的重点。

您的大多数项目都将带有对现有存储库中的数据或实际数据的需求。对于新的应用,比如这个例子,总是通过设计表来设计数据库,以表示项目和它们之间的关系。这通常是小型项目中的一个步骤,但也可能是一个迭代过程,在这个过程中,您使用初始表和关系作为输入,使用 UML 绘图和建模技术来设计和规划用户界面。对数据库(数据的组织)的更改通常是在后面的步骤中发现的,然后您可以将这些更改用作再次经历该过程的起点。

本例中的数据由一个简短的列表组成,该列表是关于机器中书籍的描述性字段。这包括标题、作者、价格和描述。我添加了 ISBN 作为表的键(因为它在定义上是唯一的,并且被出版业用作标识图书的主要方式)。我还添加了一些其他领域,我想看看,然后再决定购买一本书。这些包括出版日期和页数。我还需要存储一个缩略图。(我选择了外部方法,将路径和文件名存储到文件中,并从文件系统中读取它。我本来可以使用二进制大对象(BLOB)来存储缩略图,但是这样更容易——尽管不可否认容易出错。)最后,我计划了运行用户界面所需的内容,并决定添加一个字段来记录图书所在和分发的位置编号,以及一个字段来测量现有数量。我将该表命名为books,并将其放在名为bvm的数据库中。这里显示了该表的CREATE SQL 语句。清单 6-11 显示了使用EXPLAIN命令的表格布局。

CREATE DATABASE BVM;
CREATE TABLE Books (
  ISBN varchar(15) NOT NULL,
  Title varchar(125) NOT NULL,   
  Authors varchar(100) NOT NULL,
  Price float NOT NULL,   
  Pages int NOT NULL,   
  PubDate date NOT NULL,
  Quantity int DEFAULT 0,   
  Slot int NOT NULL,   
  Thumbnail varchar(100) NOT NULL,
  Description text NOT NULL
);

清单 6-11。 表结构

mysql> USE bvm
mysql> explain Books;
+-----------------+------------------+--------+-----+--- -----+--------+
| Field           | Type             | Null   | Key | Default | Extra  |
+-----------------+------------------+--------+-----+---------+--------+
| ISBN            | varchar(15)      | NO     |     |         |        |
| Title           | varchar(125)     | NO     |     |         |        |
| Authors         | varchar(100)     | NO     |     |         |        |
| Price           | float            | NO     |     |         |        |
| Pages           | int(11)          | NO     |     |         |        |
| PubDate         | date             | NO     |     |         |        |
| Quantity        | int(11)          | YES    |     | 0       |        |
| Slot            | int(11)          | NO     |     |         |        |
| Thumbnail       | varchar(100)     | NO     |     |         |        |
| Description     | text             | NO     |     |         |        |
+-----------------+------------------+--------+-----+---------+--------+
10 rows in set (0.08 sec)

为了管理缩略图,我将缩略图文件名存储在缩略图字段中,并对路径使用系统级选项。一种方法是创建一个命令行开关。另一种方法是将它放在 MySQL 配置文件中并从那里读取。也可以从数据库中读取。我使用了一个名为settings的数据库表,它只包含两个字段:FieldName,存储选项的名称(例如"ImagePath"),以及Value,存储它的值(例如"c:\images\mypic.tif")。这个方法允许我创建任意数量的系统选项,并从外部控制它们。此处显示了用于settings表的CREATE SQL 命令,后面是一个示例INSERT命令,用于设置示例应用的ImagePath选项:

CREATE TABLE settings (FieldName varchar(20), Value varchar(255));
INSERT INTO settings VALUES ("ImagePath", "c:\\mysql_embedded\\images\\");

创建项目

创建项目的最佳方式是使用向导创建新的 Windows 项目。我建议从源代码目录的根目录打开主解决方案文件,并将您的新应用作为新项目添加到该解决方案中。您不必将您的源代码存储在同一个源代码树中,但是您应该以这样一种方式存储它,以便知道它适用于哪个版本的源代码。

您可以使用项目向导创建项目。选择 CLR Windows 窗体应用项目模板,并将项目命名为。这将在向导中指定的文件夹的根目录下创建一个与项目同名的新文件夹。

创建一个项目文件作为解决方案的子项目会给你带来一些非常棒的优势。利用自动化的构建过程(不用生成文件——好极了!),您需要将libmysqld项目添加到项目的依赖项中。您可以从项目image项目依赖菜单中打开项目依赖工具。使用解决方案的配置下拉框将生成配置设置为活动(调试),并使用标准工具栏上的解决方案的平台下拉框将平台设置为活动(Win32)。

您还需要在项目属性中设置一些开关。打开“项目属性”对话框。首先要检查的是运行时库生成。通过展开树中的 C/C++标签,单击树中的代码生成标签,并从运行时库下拉列表中选择它,将此开关设置为多线程调试 DLL (/MDd)。[本章前面的图 6-1](#Fig1) 显示了项目属性对话框和这个选项的位置。

然后,将 MySQL include 目录添加到项目属性中。最简单的方法是展开 C/C++标签并单击命令行标签。这将显示命令行参数。要添加新参数,请在附加选项文本框中键入它。在这种情况下,你需要添加类似/I ../include`的东西。如果您的项目不在 MySQL 源代码树下,您可能需要相应地修改参数。图 6-2 本章前面显示了项目属性对话框和这个选项的位置。

如果不想(或不需要)使用预编译头,也可以移除预编译头选项。此选项位于“项目属性”对话框中的“C/C++预编译头”页上。

和前面的例子一样,您还需要使用 Project->Add Reference 菜单项添加 libmysqld 项目作为引用。图 6-36-4 描述了该操作的对话框。

最后,将公共语言运行时设置设为/clr。您可以在“项目属性”对话框中进行设置,方法是在树中单击“常规”,然后从“公共语言运行时支持”选项中选择“公共语言运行时支持(/clr)”。图 6-9 显示了项目对话框和该选项的位置。

9781430246596_Fig06-09.jpg

图 6-9。项目属性对话框:常规页面

设计

我必须满足两个重要的需求来设计应用。我不仅需要设计一个易于使用且没有错误的用户界面,还需要能够从. NET 应用中调用 C API。如果你在 MySQL 论坛和列表中做一些搜索,你会看到一些可怜的人在努力让它工作。如果你跟随我的例子,你应该不会遇到那些问题。问题的主要原因似乎是无法调用嵌入式库中的 C API 函数。我通过使用托管 C++代码用 C++编写我的应用来解决这个问题。您不能在托管应用中使用 C API 调用,但是 C++允许您通过使用#pragma unmanaged#pragma managed指令暂时关闭和重新打开它。

调用非托管代码的需求也是封装库调用的一个巨大动力。非托管代码使开发者能够编写一个可以在不是用. NET 编写的程序中使用的 DLL。对于这个例子,我使用一个 C++类来封装包装在#pragma unmanaged指令中的 C API 调用。这允许我向您展示一个直接调用嵌入式库 C API 的. NET 应用的示例。酷吧。

我还想让用户界面完全独立于任何与嵌入式库有关的东西。我想这样做,这样我就可以为您提供一个封装的数据库访问类,您可以重用它作为您自己的应用的基础。它还允许我向您展示一个真实应用的例子(Windows ),而不需要您通读长长的源代码列表。因此,本例的数据访问设计是一个单一的非托管 C++类,它封装了嵌入式库 C API 调用。该设计还包括两个表单:每个用户界面对应一个表单(CustomerAdministrator)。

托管与非托管代码

托管代码是。在公共语言运行库(CLR)控制下运行的. NET 应用。这些应用可以利用 CLR 的所有功能,特别是垃圾回收和更好的程序执行控制。非托管代码是不在 CLR 下运行的 Windows 应用,因此不能从。净增强。

数据库引擎类

我开始设计数据库引擎类时只使用了纸和笔。我本来可以使用 UML 绘图应用,但是因为这个类很小,所以我只列出了我需要的方法。例如,我需要初始化、连接和关闭嵌入式 MySQL 服务器的方法。这些方法很容易封装,因为它们不需要表单中的任何参数。

我遇到的第一个挑战是错误处理。我如何在不要求客户端了解任何关于嵌入库的信息的情况下将错误传达给客户端表单?可能有许多方法可以做到这一点,但是我选择实现一个错误检查方法,它允许客户端在一个操作之后检查是否存在错误,然后使用另一个方法来检索错误消息。这允许我再次将数据库访问与表单分开。

与发出查询和检索结果有关的类方法是从选择的实现中设计出来的。我选择实现一个访问迭代器,它允许客户机发出查询,然后遍历结果。我还需要一种方法来告诉数据库一本书已经售出,这样数据库就可以减少这本书的现有量。

数据检索是使用三种方法完成的,它们返回一个字符串、一个整数或一个大的文本字段。我还添加了 helper 方法,用于从settings表中获取设置,从数据库中获取字段(用于管理员界面),以及检索现有数量的快速方法。

清单 6-12 显示了数据库类头的完整源代码。我给这个班取名为DBEngine表 6-4 包含了对类中每个方法的描述和使用。

清单 6-12 。数据库引擎类头(DBEngine.h)

#pragma once
#pragma unmanaged
#include <stdio.h>

class DBEngine
{
private:
  bool mysqlError;
public:
  DBEngine(void);
  const char *GetError();
  bool Error();

  void Initialize();
  void Shutdown();
  char *GetSetting(char *Field);
  char *GetBookFieldStr(int Slot, char *Field);
  char *GetBookFieldText(int Slot, char *Field);
  int GetBookFieldInt(int Slot, char *Field);
  int GetQty(int Slot);
  void VendBook(char *ISBN);
  void StartQuery(char *QueryStatement);
  void RunQuery(char *QueryStatement);
  int GetNext();
  char *GetField(int fldNum);
  ∼DBEngine(void);
};
#pragma managed

表 6-4 。数据库引擎类方法

方法 返回 描述
GetError() 字符* 返回生成的最后一个错误的错误消息。
错误() (同 Internationalorganizations)国际组织 如果服务器检测到错误情况,则返回 1。
初始化() 空的 封装嵌入式服务器初始化和连接操作。
关机() 空的 封装嵌入式服务器终止和关闭操作。
GetSetting() 字符* 返回名为的设置的值。在设置表中查找信息。
GetBookFieldStr() 字符* 从 books 表中为指定槽中传递的字段返回一个字符串值。
GetBookFieldText() 字符* 从 books 表中为指定槽中传递的字段返回一个字符串值。
GetBookFieldInt() (同 Internationalorganizations)国际组织 从 books 表中为在指定槽中传递的字段返回一个整数值。
获取数量() (同 Internationalorganizations)国际组织 返回指定槽中图书的现有数量。
VendBook() 空的 减少指定插槽中图书的现有数量。
开始查询() 空的 通过执行查询并检索结果集来初始化查询迭代器。
RunQuery() 空的 一种帮助器方法,用于运行不返回结果的查询。
下一步() (同 Internationalorganizations)国际组织 检索结果集中的下一条记录。如果结果集中没有更多记录,则返回 0;如果成功,则返回非零值。
盖菲尔德 字符* 返回传递的字段编号的字段名。

定义类是容易的部分。完成所有这些方法的代码有点困难。我没有从头开始,而是使用了第一个示例中的代码,并将其更改为数据库类的源代码。清单 6-13 显示了数据库类的完整源代码。请注意,我在初始化和启动选项中使用了相同的全局(对这个源代码来说是本地)变量和字符数组。这部分你应该很熟悉。花些时间通读这段代码。当你完成后,我会解释一些更具体的细节。

清单 6-13。 数据库引擎类(DBEngine.cpp)

#pragma unmanaged

#include "DBEngine.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include "mysql.h"

MYSQL *mysql;                         //the embedded server class
MYSQL_RES *results;                   //stores results from queries
MYSQL_ROW record;                     //a single row in a result set
bool IteratorStarted;                 //used to control iterator
MYSQL_RES *ExecQuery(char *Query);

/*
  These variables set the location of the ini file and data stores.
*/
static char *server_options[] = {"mysql_test",
  "--defaults-file=c:\\mysql_embedded\\my.ini",
  "--datadir=c:\\mysql_embedded\\data", NULL };
int num_elements = (sizeof(server_options) / sizeof(char *)) - 1;
static char *server_groups[] = {"libmysqld_server", "libmysqld_client", NULL };

DBEngine::DBEngine(void)
{
  mysqlError = false;
}

DBEngine::∼DBEngine(void)
{
}

const char *DBEngine::GetError()
{
  return (mysql_error(mysql));
  mysqlError = false;
}

bool DBEngine::Error()
{
  return(mysqlError);
}

char *DBEngine::GetBookFieldStr(int Slot, char *Field)
{
  char *istr = new char[10];
  char *str = new char[128];

  _itoa_s(Slot, istr, 10, 10);
  strcpy_s(str, 128, "SELECT ");
  strcat_s(str, 128, Field);
  strcat_s(str, 128, " FROM books WHERE Slot = ");
  strcat_s(str, 128, istr);
  mysqlError = false;
  results=ExecQuery(str);
  strcpy_s(str, 128, "");
  if (results)
  {
    mysqlError = false;
    record=mysql_fetch_row(results);
    if(record)
    {
      strcpy_s(str, 128, record[0]);
    }
    else
    {
      mysqlError = true;
    }
  }
  return (str);
}

char *DBEngine::GetBookFieldText(int Slot, char *Field)
{
  char *istr = new char[10];
  char *str = new char[128];

  _itoa_s(Slot, istr, 10, 10);
  strcpy_s(str, 128, "SELECT ");
  strcat_s(str, 128, Field);
  strcat_s(str, 128, " FROM books WHERE Slot = ");
  strcat_s(str, 128, istr);
  mysqlError = false;
  results=ExecQuery(str);
  delete str;
  if (results)
  {
    mysqlError = false;
    record=mysql_fetch_row(results);
    if(record)
    {
      return (record[0]);
    }
    else
    {
      mysqlError = true;
    }
  }
  return ("");
}

int DBEngine::GetBookFieldInt(int Slot, char *Field)
{
  char *istr = new char[10];
  char *str = new char[128];
  int qty = 0;

  _itoa_s(Slot, istr, 10, 10);
  strcpy_s(str, 128, "SELECT ");
  strcat_s(str, 128, Field);
  strcat_s(str, 128, " FROM books WHERE Slot = ");
  strcat_s(str, 128, istr);
  results=ExecQuery(str);
  if (results)
  {
    record=mysql_fetch_row(results);
    if(record)
    {
      qty = atoi(record[0]);
    }
    else
    {
      mysqlError = true;
    }
  }
  delete str;
  return (qty);
}

void DBEngine::VendBook(char *ISBN)
{
  char *str = new char[128];
  char *istr = new char[10];
  int qty = 0;

  strcpy_s(str, 128, "SELECT Quantity FROM books WHERE ISBN = '");
  strcat_s(str, 128, ISBN);
  strcat_s(str, 128, "'");
  results=ExecQuery(str);
  record=mysql_fetch_row(results);
  if (record)
  {
    qty = atoi(record[0]);
    if (qty >= 1)
    {
      _itoa_s(qty - 1, istr, 10, 10);
      strcpy_s(str, 128, "UPDATE books SET Quantity = ");
      strcat_s(str, 128, istr);
      strcat_s(str, 128, " WHERE ISBN = '");
      strcat_s(str, 128, ISBN);
      strcat_s(str, 128, "'");
      results=ExecQuery(str);
    }
  }
  else
  {
    mysqlError = true;
  }
}

void DBEngine::Initialize()
{
  /*
    This section initializes the server and sets server options.
  */
  mysql_server_init(num_elements, server_options, server_groups);
  mysql = mysql_init(NULL);
  if (mysql)
  {
    mysql_options(mysql, MYSQL_READ_DEFAULT_GROUP, "libmysqld_client");
    mysql_options(mysql, MYSQL_OPT_USE_EMBEDDED_CONNECTION, NULL);
    /*
      The following call turns debugging on programmatically.
      Comment out to turn off debugging.
    */
    //mysql_debug("d:t:i:O,\\mysqld_embedded.trace");
    /*
      Connect to embedded server.
    */
    if(mysql_real_connect(mysql, NULL, NULL, NULL, "INFORMATION_SCHEMA",
       0, NULL, 0) == NULL)
    {
      mysqlError = true;
    }
    else
    {
      mysql_query(mysql, "use bvm");
    }
  }
  else
  {
    mysqlError = true;
  }
  IteratorStarted = false;
}

void DBEngine::Shutdown()
{
  /*
    Now close the server connection and tell server we're done (shutdown).
  */
  mysql_close(mysql);
  mysql_server_end();
}

char *DBEngine::GetSetting(char *Field)
{
  char *str = new char[128];
  strcpy_s(str, 128, "SELECT * FROM settings WHERE FieldName = '");
  strcat_s(str, 128, Field);
  strcat_s(str, 128, "'");
  results=ExecQuery(str);
  strcpy_s(str, 128, "");
  if (results)
  {
    record=mysql_fetch_row(results);
    if (record)
    {
      strcpy_s(str, 128, record[1]);
    }
  }
  else
  {
    mysqlError = true;
  }
  return (str);
}

void DBEngine::StartQuery(char *QueryStatement)
{
  if (!IteratorStarted)
  {
    results=ExecQuery(QueryStatement);
    if (results)
    {
      record=mysql_fetch_row(results);
    }
  }
  IteratorStarted=true;
}

void DBEngine::RunQuery(char *QueryStatement)
{
  results=ExecQuery(QueryStatement);
  if (results)
  {
    record=mysql_fetch_row(results);
    if(!record)
    {
      mysqlError = true;
    }
  }
}

int DBEngine::GetNext()
{
  //if EOF then no more records
  IteratorStarted=false;
  record=mysql_fetch_row(results);
  if (record)
  {
    return (1);
  }
  else
  {
    return (0);
  }
}

char *DBEngine::GetField(int fldNum)
{
  if (record)
  {
    return (record[fldNum]);
  }
  else
  {
    return ("");
  }
}
MYSQL_RES *ExecQuery(char *Query)
{
  mysql_dbug_print("ExecQuery.");
  mysql_free_result(results);
  mysql_query(mysql, Query);
  return (mysql_store_result(mysql));
}
#pragma managed

关于这段代码,您应该注意到的一点是,我添加了所有的错误处理,以使代码更加健壮,或者说更加坚固。虽然我没有实现所有可能的错误处理程序,但最重要的是。

get 方法都是使用相同的过程实现的。我首先生成适当的查询(从而对客户机隐藏 SQL 语句),执行查询,检索结果集,然后从查询中检索记录,并返回值。

一个有趣的方法是VendBook()。花点时间再看一遍。您将看到,我使用了类似的方法生成查询,但是这次我没有得到结果,因为没有任何结果。实际上,有一个结果—它是受影响的记录的数量。如果您想在应用中进行一些额外的过程或规则检查,这可能会很方便。

其余的方法对您来说应该很熟悉,因为它们都是我向您展示的原始示例的副本,只是这次它们包含了错误处理。现在,让我们看看用户界面代码如何调用数据库类。

客户界面(主表单)

客户界面的源代码非常大。这是因为微软将自动生成的代码放在了form.h文件中。我只包括我写的那些部分。我包含了这一部分,向您展示如何编写自己的程序。NET(或其他)用户界面。除了按钮事件中的代码,我只使用了完成用户界面所需的四个附加方法。第一种方法DisplayError()定义为:

void DisplayError()

我使用这个函数来检测数据库类中的错误,并向用户显示错误消息。该方法的实现是对MessageBox::Show()函数的典型调用。

第二个方法是一个辅助方法,它完成所选书籍的详细视图。这个函数被命名为LoadDetails()。我抽象了这个方法,因为我意识到我将为所有十个按钮重复代码。以这种方式抽象可以最大限度地减少代码并允许更容易的调试。该方法定义为:

void LoadDetails(int Slot)

该方法将插槽号(对应于按钮号)作为参数。它使用数据库类方法查询数据库,并填充细节界面元素。这是与数据库引擎类通信的大部分繁重工作发生的地方。

image 注意你可能想知道字符串周围那些错综复杂的代码是什么。事实证明。NET 字符串类与 C 样式的字符串不兼容。我包含的额外代码旨在在这些格式之间封送字符串。

第三个方法是名为Delay()的帮助器方法,定义为:

void Delay(int secs)

该函数导致处理延迟,延迟时间为作为参数传递的秒数。虽然这不是您希望包含在自己的应用中的东西,但我还是添加了它来模拟售货过程。这是一个很好的例子,说明如何使用存根功能来演示应用。这在构建新界面的原型时尤其有用。

第四种方法CheckAvailability(),用于根据是否有足够数量的产品可用来打开或关闭界面上的按钮。该方法定义为:

void CheckAvailability()

该函数对数据库引擎进行一系列调用,以检查每个插槽的数量。如果插槽是空的(quantity == 0),按钮被禁用。

清单 6-14 显示了客户界面源代码的摘录。我省略了大量自动生成的代码(表示为. . .)。注意,在文件的顶部,我使用了#include "DBEngine.h"指令来引用数据库引擎头。还要注意,我定义了一个DBEngine类型的变量。我在整个代码中使用这个对象。因为它是表单的局部变量,所以我可以在任何事件或方法中使用它。我使用. . .来表示清单中省略的自动生成代码和注释部分。

清单 6-14。 主窗体源代码(MainForm.h)

#include <stdio.h>
#include <stdlib.h>
#include <string>
#include "vcclr.h"
#include <time.h>
#include "DBEngine.h"

namespace BookVendingMachine {

  const char GREETING[] = "Please make a selection.";

  DBEngine  *Database = new DBEngine();

. . .

#pragma endregion
  void DisplayError()
  {
    String ^str = gcnew String("There was an error with the database system.\n" \
                               "Please contact product support.\nError = ");
    str = str + gcnew String(Database->GetError());
    MessageBox::Show(str, "Internal System Error", MessageBoxButtons::OK,
                     MessageBoxIcon::Information);
  }

  void LoadDetails(int Slot)
  {
    int Qty = Database->GetBookFieldInt(Slot, "Quantity");
    if (Database->Error()) DisplayError();
    pnlButtons->Visible = false;
    pnlDetail->Visible = true;
    lblStatus->Visible = false;
    lblTitle->Text = gcnew String(Database->GetBookFieldStr(Slot, "Title"));
    if (Database->Error()) DisplayError();
    lblAuthors->Text =
      gcnew String(Database->GetBookFieldStr(Slot, "Authors"));
    if (Database->Error()) DisplayError();
    lblISBN->Text = gcnew String(Database->GetBookFieldStr(Slot, "ISBN"));
    if (Database->Error()) DisplayError();
    txtDescription->Text =
      gcnew String(Database->GetBookFieldText(Slot, "Description"));
    if (Database->Error()) DisplayError();
    lblPrice->Text = gcnew String(Database->GetBookFieldStr(Slot, "Price"));
    if (Database->Error()) DisplayError();
    lblNumPages->Text =
      gcnew String(Database->GetBookFieldStr(Slot, "Pages"));
    if (Database->Error()) DisplayError();
    lblPubDate->Text =
      gcnew String(Database->GetBookFieldStr(Slot, "PubDate"));
    if (Database->Error()) DisplayError();
    if(Qty < 1)
    {
      btnPurchase->Enabled = false;
    }
  }
  void CheckAvailability()
  {
    btnBook1->Enabled = (Database->GetBookFieldInt(1, "Quantity") >= 1);
    if (Database->Error()) DisplayError();
    btnBook2->Enabled = (Database->GetBookFieldInt(2, "Quantity") >= 1);
    if (Database->Error()) DisplayError();
    btnBook3->Enabled = (Database->GetBookFieldInt(3, "Quantity") >= 1);
    if (Database->Error()) DisplayError();
    btnBook4->Enabled = (Database->GetBookFieldInt(4, "Quantity") >= 1);
    if (Database->Error()) DisplayError();
    btnBook5->Enabled = (Database->GetBookFieldInt(5, "Quantity") >= 1);
    if (Database->Error()) DisplayError();
    btnBook6->Enabled = (Database->GetBookFieldInt(6, "Quantity") >= 1);
    if (Database->Error()) DisplayError();
    btnBook7->Enabled = (Database->GetBookFieldInt(7, "Quantity") >= 1);
    if (Database->Error()) DisplayError();
    btnBook8->Enabled = (Database->GetBookFieldInt(8, "Quantity") >= 1);
    if (Database->Error()) DisplayError();
    btnBook9->Enabled = (Database->GetBookFieldInt(9, "Quantity") >= 1);
    if (Database->Error()) DisplayError();
    btnBook10->Enabled = (Database->GetBookFieldInt(10, "Quantity") >= 1);
    if (Database->Error()) DisplayError();
  }

  void Delay(int secs)
  {
    time_t start;
    time_t current;

    time(&start);
    do
    {
      time(&current);
    } while(difftime(current,start) < secs);
  }

private: System::Void btnCancel_Click(System::Object^  sender,
                                      System::EventArgs^  e)
  {
    lblStatus->Visible = true;
    pnlDetail->Visible = false;
    pnlButtons->Visible = true;
    btnPurchase->Enabled = true;
    lblStatus->Text = gcnew String(GREETING);
  }

private: System::Void btnPurchase_Click(System::Object^  sender,
                                        System::EventArgs^  e)
  {
    String ^orig = gcnew String(lblISBN->Text->ToString());
    pin_ptr<const wchar_t> wch = PtrToStringChars(orig);

    // Convert to a char*
    size_t origsize = wcslen(wch) + 1;
    const size_t newsize = 100;
    size_t convertedChars = 0;
    char nstring[newsize];
    wcstombs_s(&convertedChars, nstring, origsize, wch, _TRUNCATE);

    lblStatus->Visible = true;
    pnlDetail->Visible = false;
    pnlButtons->Visible = true;
    btnPurchase->Enabled = true;
    Database->VendBook(nstring);
    //
    // Simulate buying the book.
    //
    lblStatus->Text = "Please Insert your credit card.";
    this->Refresh();
    Delay(3);
    lblStatus->Text = "Thank you. Processing card number ending in 4-1234.";
    this->Refresh();
    Delay(3);
    lblStatus->Text = "Vending....";
    this->Refresh();
    Delay(5);
    this->Refresh();
    CheckAvailability();
    lblStatus->Text = gcnew String(GREETING);
  }

private: System::Void MainForm_Load(System::Object^  sender,
                                    System::EventArgs^  e)
  {
    String ^imageName;
    String ^imagePath;

    Database->Initialize();
    if (Database->Error()) DisplayError();
    //
    //For each button, check to see if there are sufficient qty and load
    //the thumbnail for each.
    //
    imagePath = gcnew String(Database->GetSetting("ImagePath"));

    imageName = imagePath +
      gcnew String(Database->GetBookFieldStr(1, "Thumbnail"));
    if (Database->Error()) DisplayError();
    btnBook1->Image = btnBook1->Image->FromFile(imageName);
    imageName = imagePath +
      gcnew String(Database->GetBookFieldStr(2, "Thumbnail"));
    if (Database->Error()) DisplayError();
    btnBook2->Image = btnBook2->Image->FromFile(imageName);
    imageName = imagePath +
      gcnew String(Database->GetBookFieldStr(3, "Thumbnail"));
    if (Database->Error()) DisplayError();
    btnBook3->Image = btnBook3->Image->FromFile(imageName);
    imageName = imagePath +
      gcnew String(Database->GetBookFieldStr(4, "Thumbnail"));
    if (Database->Error()) DisplayError();
    btnBook4->Image = btnBook4->Image->FromFile(imageName);
    imageName = imagePath +
      gcnew String(Database->GetBookFieldStr(5, "Thumbnail"));
    if (Database->Error()) DisplayError();
    btnBook5->Image = btnBook5->Image->FromFile(imageName);
    imageName = imagePath +
      gcnew String(Database->GetBookFieldStr(6, "Thumbnail"));
    if (Database->Error()) DisplayError();
    btnBook6->Image = btnBook6->Image->FromFile(imageName);
    imageName = imagePath +
      gcnew String(Database->GetBookFieldStr(7, "Thumbnail"));
    if (Database->Error()) DisplayError();
    btnBook7->Image = btnBook7->Image->FromFile(imageName);
    imageName = imagePath +
      gcnew String(Database->GetBookFieldStr(8, "Thumbnail"));
    if (Database->Error()) DisplayError();
    btnBook8->Image = btnBook8->Image->FromFile(imageName);
    imageName = imagePath +
      gcnew String(Database->GetBookFieldStr(9, "Thumbnail"));
    if (Database->Error()) DisplayError();
    btnBook9->Image = btnBook9->Image->FromFile(imageName);
    imageName = imagePath +
      gcnew String(Database->GetBookFieldStr(10, "Thumbnail"));
    if (Database->Error()) DisplayError();
    btnBook10->Image = btnBook10->Image->FromFile(imageName);

    CheckAvailability();
  }

private: System::Void btnBook1_Click(System::Object^  sender,
                                     System::EventArgs^  e)
  {
    LoadDetails(1);
  }

private: System::Void btnBook2_Click(System::Object^  sender,
                                     System::EventArgs^  e)
  {
    LoadDetails(2);
  }

private: System::Void btnBook3_Click(System::Object^  sender,
                                     System::EventArgs^  e)
  {
    LoadDetails(3);
  }

private: System::Void btnBook4_Click(System::Object^  sender,
                                     System::EventArgs^  e)
  {
    LoadDetails(4);
  }

private: System::Void btnBook5_Click(System::Object^  sender,
                                     System::EventArgs^  e)
  {
    LoadDetails(5);
  }

private: System::Void btnBook6_Click(System::Object^  sender,
                                     System::EventArgs^  e)
  {
    LoadDetails(6);
  }

private: System::Void btnBook7_Click(System::Object^  sender,
                                     System::EventArgs^  e)
  {
    LoadDetails(7);
  }

private: System::Void btnBook8_Click(System::Object^  sender,
                                     System::EventArgs^  e)
  {
    LoadDetails(8);
  }
private: System::Void btnBook9_Click(System::Object^  sender,
                                     System::EventArgs^  e)
  {
    LoadDetails(9);
  }

private: System::Void btnBook10_Click(System::Object^  sender,
                                      System::EventArgs^  e)
  {
    LoadDetails(10);
  }

private: System::Void MainForm_FormClosing(System::Object^  sender,
  System::Windows::Forms::FormClosingEventArgs^  e)
  {
    Database->Shutdown();
  }

};
}

MainForm_Load()事件是数据库引擎初始化和按钮加载适当缩略图的地方。我用语句跟踪对数据库的每个调用。

if (Database->Error()) DisplayError();

这个语句允许我检测错误发生的时间并通知用户。虽然我不会在这个事件中对错误采取行动,但我可以在其他事件中对它采取行动。如果这里出现严重的数据库错误,最坏的情况是按钮不会被缩略图填充。我在整个源代码中使用这个概念。

实现btnBook1_Click()btnBook10_Click()事件来调用LoadDetails()方法,并用适当的数据填充细节接口组件。如您所见,抽象细节的加载为我节省了大量代码!

在界面的细节部分有两个按钮。btnCancel_Click()事件将界面返回到初始的自动售货机视图。btnPurchase_Click()事件更有趣一点。这是贩卖部分发生的地方。注意,我首先调用了VendBook()方法,然后运行自动售货过程的模拟,并将界面返回到自动售货视图。

就这样!客户界面非常简单,就像大多数自动售货机一样。只有一排按钮和一个收钱的机制(在这种情况下,我假设机器接受信用卡支付,但真正的自动售货机可能会采取几种支付形式)。

管理界面(管理表单)

客户界面简单易用。但是如何维护数据呢?供应商如何补充自动售货机的库存,甚至改变提供的图书列表?一种方法是使用独立于客户界面的管理界面。您还可以创建另一个单独的嵌入式应用来处理这个问题,或者可能在另一台机器上创建数据并复制到自动售货机。我选择构建一个简单的管理表单,如图 6-10 所示。

9781430246596_Fig06-10.jpg

图 6-10。示例给药形式

与客户界面一样,我需要创建一个助手函数。函数LoadList(),用于填充一个列表,该列表显示了books表中的所有数据。这很方便,因为它允许供应商查看数据库包含的内容。

清单 6-15 显示了管理表单源代码的摘录。我省略了自动生成的 Windows 窗体代码(表示为. . .)。在源代码的顶部,我将指针变量定义为AdminDatabase而不是Database。这主要是为了清楚起见,并不意味着分散您对数据库引擎类的使用的注意力。我使用. . .来表示清单中省略的自动生成代码和注释部分。

清单 6-15。 管理表单源代码(AdminForm.h)

#pragma once
#include "DBEngine.h"

using namespace System;
using namespace System::ComponentModel;
using namespace System::Collections;
using namespace System::Windows::Forms;
using namespace System::Data;
using namespace System::Drawing;

namespace BookVendingMachine {

  DBEngine  *AdminDatabase = new DBEngine();

. . .
#pragma endregion
  void LoadList()
  {
    int i = 0;
    int j = 0;
    String^ str;

    lstData->Items->Clear();
    AdminDatabase->StartQuery("SELECT ISBN, Slot, Quantity, Price," \
      " Pages, PubDate, Title, Authors, Thumbnail," \
      " Description FROM books");
    do
    {
      str = gcnew String("");
      for (i = 0; i < 10; i++)
      {
        if (i != 0)
        {
          str = str + "\t";
        }
        str = str + gcnew String(AdminDatabase->GetField(i));
      }
      lstData->Items->Add(str);
      j++;
    }while(AdminDatabase->GetNext());
  }

private: System::Void btnExecute_Click(System::Object^  sender,
                                       System::EventArgs^  e)
  {
    String ^orig = gcnew String(txtQuery->Text->ToString());
    pin_ptr<const wchar_t> wch = PtrToStringChars(orig);

    // Convert to a char*
    size_t origsize = wcslen(wch) + 1;
    const size_t newsize = 100;
    size_t convertedChars = 0;
    char nstring[newsize];
    wcstombs_s(&convertedChars, nstring, origsize, wch, _TRUNCATE);
    AdminDatabase->RunQuery(nstring);
    LoadList();
  }

private: System::Void Admin_Load(System::Object^  sender,
                                 System::EventArgs^  e)
  {
    AdminDatabase->Initialize();
    LoadList();
  }

private: System::Void AdminForm_FormClosing(System::Object^  sender,
  System::Windows::Forms::FormClosingEventArgs^  e)
  {
    AdminDatabase->Shutdown();
  }
};
}

我在表单 load 和 closing 事件中包含了对数据库引擎的初始化和关闭方法调用。

这个接口被设计成接受一个特别的查询,并在点击Execute按钮时执行它。因此,btnExecute_Click()是这个源代码中唯一的其他方法。该方法调用数据库引擎并请求运行查询,但不检查任何结果。那是因为这个界面是用来调整数据库中的东西,而不是选择数据。这个方法中的最后一个调用是LoadList()助手方法,它重新填充列表。

检测接口请求

您可能想知道我打算如何检测要执行哪个接口。答案是,我使用命令行参数来告诉代码运行哪个接口。该开关在BookVendingMachine.cpp源文件中的main()函数中实现。处理命令行参数的源代码是不言自明的。清单 6-16 包含嵌入式应用的main()函数的完整源代码。

清单 6-16。bookvending machine 主要功能(BookVendingMachine.cpp)

// BookVendingMachine.cpp : main project file.

#include "MainForm.h"
#include "AdminForm.h"

using namespace BookVendingMachine;

[STAThreadAttribute]
int main(array<System::String ^> ^args)
{
  // Enabling Windows XP visual effects before any controls are created
  Application::EnableVisualStyles();
  Application::SetCompatibleTextRenderingDefault(false);

  // Create the main window and run it
  if ((args->Length == 1) && (args[0] == "-admin"))
  {
    Application::Run(gcnew AdminForm());
  }
  else
  {
    Application::Run(gcnew MainForm());
  }
  return 0;
}

现在,您应该能够从本文或从图书网站下载信息来重新创建这个示例。我鼓励您熟悉客户机源代码(表单),这样您就可以看到并理解数据库引擎是如何使用的。当你准备好了,你可以编译并运行这个例子。

编译和运行

编译这个例子只需要点击 BuildimageBuild bookvending machine。如果你已经编译了libmysqld项目,你应该看到的只是例子的编译。如果出于某种原因,目标文件对于libmysqld或它的任何依赖项来说都是过期的,Visual Studio 也会编译这些文件。

image 下面的例子我用的是 Visual Studio 2010。较新版本的 Visual Studio 可能会更改某些菜单命令的位置。所有描述的变化都可以在新版本中设置。

编译完成后,您可以从调试菜单命令运行程序,或者打开一个命令窗口,通过从项目目录输入命令debug\BookVendingMachine从命令行运行程序。如果这是您第一次使用,您应该会看到如下错误消息:

此应用未能启动,因为找不到 LIBMYSQLD.dll。

重新安装应用可能会解决这个问题。

这个错误的原因与错误消息中的第二句话无关。这意味着嵌入库不在搜索路径中。如果你曾经和。NET 或 COM 应用,并且从未使用过 C 库,您可能从未遇到过该错误。不像。NET 和 COM,C 库没有在 GAC 或注册表中注册。这些库(dll)应该与调用它们的应用放在一起,或者至少在一个执行路径上。大多数开发者将 DLL 的副本放在执行目录中。

要解决这个问题,请将libmysqld.dll文件从lib_debug目录复制到bookvendingmachine.exe文件所在的目录(或将lib_debug添加到执行路径)。一旦将库复制到执行目录,您应该看到应用如图 6-36-46-5 所示运行。

花些时间,摆弄一下界面。如果时间延迟太烦人,您可以减少延迟的秒数,或者注释掉延迟方法调用。

如果您想访问管理界面,请使用-admin命令行开关运行程序。如果从命令行运行该示例,可以输入命令:

BookVendingMarchine -admin

如果希望使用调试器从 Visual Studio 运行该示例,请在项目属性中设置命令行开关。通过选择项目image项目属性打开对话框,并点击树中的调试标签。通过将命令行参数键入 Command Arguments 选项,可以添加任意数量的命令行参数。图 6-11 显示了该选项在项目属性中的位置。

9781430246596_Fig06-11.jpg

图 6-11。从 Visual Studio 设置命令行参数

我鼓励你尝试这个例子。如果您没有运行 Windows,您仍然可以使用数据库引擎类,并为应用提供自己的接口。这应该不难,因为您已经看到了该接口如何与抽象的libmysqld系统调用一起工作的一个例子。如果您发现自己正在使用嵌入式 MySQL 系统构建独特的自动售货机,请给我发一张照片!

摘要

在本章中,您学习了如何创建嵌入式 MySQL 应用。MySQL embedded library 经常被忽视,但是它非常成功地允许系统集成商向他们的企业应用和产品添加强大的数据管理工具。

也许这一章最吸引人的地方是你对 MySQL 嵌入式库 C API 的引导之旅。我希望通过学习本章中的例子,您能够体会到嵌入式 MySQL 应用的强大功能。我还希望,如果你在编译源代码时遇到了问题,你不会沮丧地扔掉这本书。一个优秀的开源开发者的主要素质是她系统地诊断和调整环境以适应当前项目需求的能力。如果你遇到问题,不要绝望。解决问题是学习周期的自然组成部分。

您还探索了为嵌入式应用打开调试跟踪的概念。我还带您进行了一次简短的旅程,通过嵌入式库公开一个 DBUG 方法来修改 MySQL 服务器源代码,该方法允许您向 DBUG 跟踪输出添加自己的字符串。您看到了一些有趣的错误处理情况以及如何处理它们。最后,我向您展示了一个封装的数据库访问类,您可以在自己的嵌入式应用中使用它。

在下一章,我将研究 MySQL 系统的一个更流行的扩展。这包括添加您自己的用户定义函数(UDF),扩展现有的 SQL 命令,以及向服务器添加您自己的 SQL 命令。这些技术允许 MySQL 系统进一步发展,以满足您的环境的特定需求。

1 嗯,直到现在看来。

2 此功能可改为mysql_library_init()。在撰写本文时,这两种功能都得到支持。

3 它实际上泄漏的并不多,因为它不再被引用,而是仍然被分配,使得那部分内存不可用。

4 这在 MySQL 以后的版本中可能会改为mysql_library_end()。在撰写本文时,这两种功能都得到支持。

我发现 Visual Basic 只有一个特性很酷:控件数组。唉,它们都是过去式了。`

七、向 MySQL 添加函数和命令

系统集成商面临的最大挑战之一是克服被集成系统的局限性。这通常是由于系统对集成有限制,或者没有集成所需的某些功能或命令。通常,这意味着通过创建更多的“粘合”程序来翻译或增强现有的功能和命令来解决问题。

MySQL 开发者认识到了这一需求,并在 MySQL 服务器中添加了灵活的选项来添加新的功能和命令。例如,您可能需要添加函数来执行一些计算或数据转换,或者您可能需要一个新命令来提供特定的管理数据。

本章向您介绍了可用于添加函数的选项,并向您展示了如何向服务器添加您自己的 SQL 命令。我们将探索用户定义的函数、本地函数和新的 SQL 命令。本章的大部分背景材料已经在前面的章节中介绍过了。在你继续学习的时候,请随意查阅这些章节。

添加用户自定义函数

MySQL 支持用户自定义函数(UDF)已经有一段时间了。UDF 是一个新功能(计算、转换等)。),您可以将它添加到服务器中,从而扩展可以在 SQL 命令中使用的可用函数的列表。UDF 最好的一点是它们可以在运行时动态加载。此外,您可以创建自己的 UDF 库,并在您的企业中使用它们,甚至免费提供它们(作为开源)。这可能是系统集成商寻求扩展 MySQL 服务器的第一个地方。MySQL 工程师对 UDF 机制有另一个天才级的想法。

只要 SQL 语言允许使用表达式,就可以在任何地方使用用户定义函数。例如,您可以在存储过程和SELECT语句中使用 UDF。它们是扩展您的服务器而不必修改服务器源代码的极好方法。事实上,您可以定义任意多的 UDF,甚至可以将它们组合在一起形成函数库。每个库都是一个单独的文件,包含编译成库的源代码(Linux 中的.so或 Windows 中的.dll)。

该机制类似于插件接口,事实上,早于插件接口。UDF 接口利用外部的、可动态加载的目标文件来加载和卸载 UDF。该机制使用一个CREATE FUNCTION命令在每个函数的基础上建立到可加载目标文件的连接,并使用一个DROP FUNCTION命令删除函数的连接。让我们来看看这些命令的语法。

创建函数语法

CREATE FUNCTION命令向服务器注册函数,在 mysql.func 表中放置一行。语法是:

CREATE FUNCTION function_name RETURNS [STRING | INTEGER | REAL | DECIMAL] SONAME "mylib.so";

function_name参数代表您正在创建的函数的名称。返回类型可以是STRINGINTEGERREALDECIMAL中的一种,SONAME表示库的名称。CREATE FUNCTION命令告诉 MySQL 服务器创建命令(function_name)中的函数名到目标文件的映射。当调用函数时,服务器调用库中的函数来执行。

DROP 函数语法

DROP FUNCTION命令通过从所选数据库的func表中删除相关的行,向服务器注销该函数。语法如下所示。function_name参数代表您正在创建的函数的名称。

DROP FUNCTION function_name;

让我们看看如何创建一个 UDF 库,并在 MySQL 服务器安装中使用它。我们将从修改现有的示例 UDF 库开始。一旦熟悉了函数的编码方式,创建一个新的源文件并将其添加到服务器构建文件(CMakeLists.txt)是一个基本的练习。

创建用户自定义库

有两种类型的用户定义函数:

  • 您可以创建作为单个调用运行的函数,该调用计算一组参数并返回单个结果。
  • 您可以创建函数,作为从分组函数中调用的聚合。例如,您可以创建将一种数据类型转换为另一种数据类型的 UDF,例如将日期字段从一种格式更改为另一种格式的函数,或者您可以创建对一组记录执行高级计算的函数,例如平方和函数。UDF 只能返回整数、字符串或实数值。
  • 您可以创建提供 SELECT 语句中使用的值的函数。

单呼叫 UDF 是最常见的。它们用于对一个或多个参数执行操作。在某些情况下,不使用任何参数。例如,您可以创建一个 UDF,为全局状态或类似的SERVER_STATUS()返回值。这种形式的 UDF 通常用在SELECT语句的字段列表中,或者作为辅助函数用在存储过程中。

聚合 UDF 函数用于GROUP BY子句中。当它们被使用时,它们在表中的每一行被调用一次,在组的末尾再次被调用。

创建 UDF 库的过程是创建一个新项目,该项目公开 UDF 加载/卸载方法(xxx_initxxx_deinit,其中xxx是函数的名称)和函数本身。每条语句调用一次xxx_initxxx_deinit函数。每一行都会调用一次XXX函数。如果您正在创建一个聚合函数,您还需要实现分组函数xxx_clearxxx_add。调用xxx_clear函数来重置值(在组的开始)。对分组中的每一行调用xxx_add函数,在分组处理结束时调用函数本身。因此,聚合被清除,然后为每个 add 调用添加数据。最后,调用函数本身来返回值。

一旦实现了这些函数,您就可以编译该文件并将其复制到服务器安装的插件目录中。您可以使用CREATE FUNCTION命令加载和使用这些功能。清单 7-1 展示了一组用于 UDF 的示例方法。

清单 7-1 样 UDF 战法

/*
  Simple example of how to get a sequences starting from the first
  argument or 1 if no arguments have been given
*/

my_bool sequence_init(UDF_INIT *initid, UDF_ARGS *args, char *message)
{
  if (args->arg_count > 1)
  {
    strmov(message,"This function takes none or 1 argument");
    return 1;
  }
  if (args->arg_count)
    args->arg_type[0]= INT_RESULT; /* Force argument to int */

  if (!(initid->ptr=(char*) malloc(sizeof(longlong))))
  {
    strmov(message,"Couldn't allocate memory");
    return 1;
  }
  memset(initid->ptr, 0, sizeof(longlong));
  /*
    sequence() is a non-deterministic function : it has different value
    even if called with the same arguments.
  */
  initid->const_item=0;
  return 0;
}

void sequence_deinit(UDF_INIT *initid)
{
  if (initid->ptr)
    free(initid->ptr);
}

longlong sequence(UDF_INIT *initid __attribute__((unused)), UDF_ARGS *args,
                  char *is_null __attribute__((unused)),
                  char *error __attribute__((unused)))
{
  ulonglong val=0;
  if (args->arg_count)
    val= *((longlong*) args->args[0]);
  return ++*((longlong*) initid->ptr) + val;
}

Oracle 提供了一个名为udf_example.cc and的示例 UDF 项目,它位于/sql文件夹中,包含您可能想要创建的所有类型的函数的示例。这为添加您自己的函数提供了一个极好的起点。示例函数包括:

  • 在字符串上产生类似 soundex 操作的变音函数
  • 一个示例函数,返回一个 double 值,该值是参数的字符代码值之和除以所有参数的长度之和
  • 示例函数返回一个整数,该整数是参数长度的总和
  • 基于传递的值返回序列中下一个值的序列函数
  • 从整数参数(数量)和双参数(成本)列表中返回平均成本的示例聚合函数

根据您的需要,您可能会发现其中一些示例很有用。

让我们从修改示例 UDF 项目开始。找到位于源代码根目录下的/sql目录中的udf_example.cc文件,并在您喜欢的编辑器中打开它。因为 udf_example 库包含在 cmake 文件中,所以编译它非常容易。完成编辑后,只需执行 make 即可。在 Windows 上,可以使用 Visual Studio 重新生成 mysql.sln 文件。

image 警告 Windows 用户必须从库中删除网络 UDF。Windows 不直接支持这些功能。如果遇到关于缺少头文件或外部函数的错误,请注释掉这些函数。

如果在编译过程中遇到错误,请返回并更正它们,然后再次尝试编译。最可能的原因是键入了错误的名称、不正确的代码替换或不正确的路径。

现在库已经编译好了,让我们测试加载和卸载操作。这将确保库已被正确编译并位于正确的位置。打开一个 MySQL 客户端窗口,发出CREATE FUNCTIONDROP FUNCTION命令来加载库中的所有函数。清单 7-2 显示了加载和卸载前五个函数的命令。该列表显示了用于 Windows 的命令;在 Linux 上用udf_example.so替换udf_example.dll。在执行这些函数的任何平台上,输出都是一样的。

清单 7-2 样本创建和删除功能命令

CREATE FUNCTION metaphon RETURNS STRING SONAME "udf_example.dll";
CREATE FUNCTION myfunc_double RETURNS REAL SONAME "udf_example.dll";
CREATE FUNCTION myfunc_int RETURNS INTEGER SONAME "udf_example.dll";
CREATE FUNCTION sequence RETURNS INTEGER SONAME "udf_example.dll";
CREATE AGGREGATE FUNCTION avgcost RETURNS REAL SONAME "udf_example.dll";

DROP FUNCTION metaphon;
DROP FUNCTION myfunc_double;
DROP FUNCTION myfunc_int;
DROP FUNCTION sequence;
DROP FUNCTION avgcost;

清单 7-37-4 显示了运行前面显示的CREATE FUNCTIONDROP FUNCTION命令时的正确结果。

清单 7-3?? 安装功能

mysql> CREATE FUNCTION metaphon RETURNS STRING SONAME "udf_example.dll";Query OK, 0 rows affected (0.00 sec)

mysql> CREATE FUNCTION myfunc_double RETURNS REAL SONAME "udf_example.dll";Query OK, 0 rows affected (0.00 sec)

mysql> CREATE FUNCTION myfunc_int RETURNS INTEGER SONAME "udf_example.dll";Query OK, 0 rows affected (0.00 sec)

mysql> CREATE FUNCTION sequence RETURNS INTEGER SONAME "udf_example.dll";Query OK, 0 rows affected (0.00 sec)

mysql> CREATE AGGREGATE FUNCTION avgcost RETURNS REAL SONAME "udf_example.dll";Query OK, 0 rows affected (0.00 sec)

清单 7-4?? 卸载功能

mysql> DROP FUNCTION metaphon;Query OK, 0 rows affected (0.00 sec)

mysql> DROP FUNCTION myfunc_double;Query OK, 0 rows affected (0.00 sec)

mysql> DROP FUNCTION myfunc_int;Query OK, 0 rows affected (0.00 sec)

mysql> DROP FUNCTION sequence;Query OK, 0 rows affected (0.00 sec) 
mysql> DROP FUNCTION avgcost;Query OK, 0 rows affected (0.00 sec)

现在,让我们运行命令,看看它们是否有效。回到 MySQL 客户端窗口,再次运行CREATE FUNCTION命令来加载 UDF。清单 7-5 展示了库中前五个 UDF 的执行示例。请随意尝试所示的命令。你的结果应该差不多。

清单 7-5 执行 UDF 命令的例子

mysql> SELECT metaphon("This is a test.");
+−−---------------------------+
| metaphon("This is a test.") |
+−−---------------------------+
| 0SSTS                       |
+−−---------------------------+
1 row in set (0.00 sec)

mysql> SELECT myfunc_double(5.5, 6.1);
+−−-----------------------+
| myfunc_double(5.5, 6.1) |
+−−-----------------------+
| 50.17                   |
+−−-----------------------+
1 row in set (0.01 sec)

mysql> SELECT myfunc_int(5, 6, 8);
+−−-------------------+
| myfunc_int(5, 6, 8) |
+−−-------------------+
| 19                  |
+−−-------------------+
1 row in set (0.00 sec)

mysql> SELECT sequence(8);
+−−-----------+
| sequence(8) |
+−−-----------+
| 9           |
+−−-----------+
1 row in set (0.00 sec)

mysql> CREATE TABLE testavg (order_num int key auto_increment, cost double, qty int);
Query OK, 0 rows affected (0.02 sec) 
mysql> INSERT INTO testavg (cost, qty) VALUES (25.5, 17);Query OK, 1 row affected (0.00 sec) 
mysql> INSERT INTO testavg (cost, qty) VALUES (0.23, 5);Query OK, 1 row affected (0.00 sec) 
mysql> INSERT INTO testavg (cost, qty) VALUES (47.50, 81);Query OK, 1 row affected (0.00 sec) 
mysql> SELECT avgcost(qty, cost) FROM testavg;
+−−------------------+
| avgcost(qty, cost) |
+−−------------------+
| 41.5743            |
+−−------------------+
1 row in set (0.03 sec)

最后几个命令展示了avgcost()聚合函数的一个非常基本的用法。在使用GROUP BY子句时,通常会使用聚合函数。

添加新的用户自定义函数

让我们现在添加一个新的 UDF 到库。如果您正在进行一个集成项目,并且需求要求用 Julian 格式表示日期,该怎么办?儒略历转换只是将一年中的某一天(自前一年的 12 月 31 日以来经过的天数)加上年份形成一个数值,如 DDDYYYY。在这种情况下,您需要添加一个函数,该函数接受月份、日期和年份值,并返回以儒略日表示的日期。该功能应定义为:

longlong julian(int month, int day, int year);

我保持了函数的简单,使用了三个整数。该函数可以以多种方式实现(例如,接受日期或字符串值)。现在让我们将JULIAN函数添加到您刚刚构建的 UDF 库中。

这就是创建你自己的 UDF 库的价值所在。任何时候你需要一个新的函数,你都可以把它添加到现有的库中,而不需要从头开始创建一个新的项目。

添加新 UDF 的过程从将函数声明添加到 UDF 库源代码的extern部分开始,然后实现这些函数。然后可以重新编译这个库,并将其部署到 MySQL 服务器安装的插件目录中。让我们用JULIAN函数来完成这个过程。

image 注意使用SHOW VARIABLES LIKE 'plugin%';发现插件目录。

打开udf_example.cc文件并添加函数声明。回想一下,您需要定义julian_init()julian_deinit()julian()函数。julian_init()函数有三个参数:

  • UDF_INIT,该方法可以用来在方法之间传递信息的结构
  • UDF_ARGS,一个包含参数数量、参数类型和参数本身的结构
  • 发生错误时方法应返回的字符串

julian()方法有四个参数:

  • julian_init()函数完成的UDF_INIT结构
  • 一个包含参数数量、参数类型和参数的UDF_ARGS结构
  • 如果结果为空,则指向设置为 1 的变量的 char 指针
  • 发生错误时发送给调用方的消息

julian_deinit()函数使用由julian_init()函数完成的UDF_INIT结构。

当从服务器调用 UDF 时,一个新的UDF_INIT结构被创建并传递给函数,参数被放在UDF_ARGS结构中,然后调用julian_init()函数。如果那个函数没有错误地返回,那么从julian_init()函数中用UDF_INIT结构调用julian()函数。在julian()函数完成后,调用julian_deinit()函数来清除保存在UDF_INIT结构中的值。清单 7-6 显示了添加了JULIAN函数的文件的declaration部分的摘录。该部分用C_MODE_STARTC_MODE_END宏表示,位于文件的顶部。我们包含了修改标记,以确保其他人(或者将来的我们自己)知道我们有意修改了这个文件。

清单 7-6 朱利安的声明 (udf_example.cc)

C_MODE_START;
...

/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* This section declares the methods for the Julian function */
my_bool julian_init(UDF_INIT *initid, UDF_ARGS *args, char *message);
longlong julian(UDF_INIT *initid, UDF_ARGS *args,
                char *is_null, char *error);
void julian_deinit(UDF_INIT *initid);
/* END CAB MODIFICATION */
...
C_MODE_END;

image 注意我们显示了用椭圆圈出的宏,以指示这些语句应该放在哪里。

现在可以添加这些函数的实现了。我发现复制与我的返回类型匹配的示例函数,然后修改它们以满足我的需要是很有帮助的。julian_init()函数负责初始化变量并检查正确的用法。由于JULIAN函数需要三个整数参数,您需要添加适当的错误处理来实现这一点。清单 7-7 展示了julian_init()功能的实现。您可以在 udf_example.cc 文件的末尾附近插入此方法。

清单 7-7 实现为 julian_init()函数(udf_example.cc)

/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* This section implements the Julian initialization function */
my_bool julian_init(UDF_INIT *initid, UDF_ARGS *args, char *message)
{
  if (args->arg_count != 3) /* if there are not three arguments */
  {
    strcpy(message, "Wrong number of arguments: JULIAN() requires 3 arguments.");
    return 1;
  }
  if ((args->arg_type[0] != INT_RESULT) ||
      (args->arg_type[1] != INT_RESULT) ||
      (args->arg_type[2] != INT_RESULT))
  {
    strcpy(message, "Wrong type of arguments: JULIAN() requires 3 integers.");
    return 1;
  }
  return 0;
}
/* END CAB MODIFICATION */

注意在清单 7-7 中,首先检查参数计数,然后是三个参数的类型检查。这确保了它们都是整数。精明的程序员会注意到代码还应该检查值的范围。由于代码不检查参数的范围,这可能导致异常或无效的返回值。如果您决定在您的库中实现该功能,我将把它留给您来完成。当参数值的定义域和范围已知时,检查范围值始终是一个好的做法。

实际上并不需要julian_deinit()函数,因为没有内存或变量需要清理。您可以实现一个空函数来完成这个过程。即使你不需要这个函数,编写它也是一个好主意。清单 7-8 展示了这个函数的实现。因为我们没有使用任何新的变量或结构,所以实现只是一个空函数。如果已经创建了变量或结构,您可以在这个函数中释放它们。

清单 7-8 实现为 julian_deinit()函数(udf_example.cc)

/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* This section implements the Julian deinitialization function */
void julian_deinit(UDF_INIT *initid)
{
}
/* END CAB MODIFICATION */

JULIAN函数的真正工作发生在julian()实现中。清单 7-9 显示了完整的julian()功能。

image 一些复杂的儒略历方法计算从开始日期(通常是在 18 或 19 世纪)起经过的天数。此方法假设需要儒略日/年值。

清单 7-9 实现为 julian()函数(udf_example.cc)

/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* This section the Julian function */
longlong julian(UDF_INIT *initid, UDF_ARGS *args,
                char *is_null, char *error)
{
  longlong jdate = 0;
  static int DAYS_IN_MONTH[] = {31, 28, 31, 30, 31, 30, 31,
                                31, 30, 31, 30, 31};
  longlong month = 0;
  longlong day = 0;
  longlong year = 0;
  int i;

  /* copy memory from the arguments */
  month = *(longlong *)args->args[0];
  day = *(longlong *) args->args[1];
  year = *(longlong *) args->args[2];

  /* add the days in the month for each prior month */
  for (i = 0; i < month - 1; i++)
    jdate += DAYS_IN_MONTH[i];

  /* add the day of this month */
  jdate += day;

  /* find the year */
  if (((year % 100) != 0) && ((year % 4) == 0))
    jdate++;  /*leap year!*/

  /* shift day of year to left */
  jdate *= 10000;

  /* add the year */
  jdate += year;
  return jdate;
}
/* END CAB MODIFICATION */

注意变量声明后的前几行。这是一个如何将来自args数组的值封送到你自己的局部变量的例子。在本例中,我将前三个参数复制为整数值。源代码的其余部分是返回给调用者的儒略日值的计算。

image 闰年的计算故意显得幼稚。我给你留了一个更正确的计算方法作为练习。提示:jdate 变量应该在什么时候递增?

如果使用的是 Windows,还需要修改udf_example.def文件,添加JULIAN函数的方法。清单 7-10 显示了更新后的udf_example.def文件。

清单 7-10??【UDF _ example . def】源代码

LIBRARY       MYUDF
DESCRIPTION  'MySQL Sample for UDF'
VERSION       1.0
EXPORTS
  metaphon_init
  metaphon_deinit
  metaphon
  myfunc_double_init
  myfunc_double
  myfunc_int
  myfunc_int_init
  sequence_init
  sequence_deinit
  sequence
  avgcost_init
  avgcost_deinit
  avgcost_reset
  avgcost_add
  avgcost_clear
  avgcost
  julian_init
  julian_deinit
  julian

现在你可以编译这个库了。一旦库被编译,将库复制到 MySQL 服务器安装的插件目录中。如果你运行的是 Linux,你将会复制文件udf_example.so;如果你运行的是 Windows,你将从/udf_example/debug目录中复制文件udf_example.dll

我建议在复制文件之前停止服务器,并在复制完成后重新启动它。这是因为目标文件可能与先前的编译不同(取决于您将新函数放在哪里)。每当您对可执行代码进行更改时,遵循这一点总是一个好的做法。

继续复制库并安装函数,然后输入CREATE FUNCTION命令并尝试新函数。清单 7-11 显示了在 Windows 上安装和运行JULIAN函数的例子。

清单 7-11 示例执行 julian()函数

mysql> CREATE FUNCTION julian RETURNS INTEGER SONAME "udf_example.dll";
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT JULIAN(8, 13, 2012);

+−−-------------------+
| JULIAN(8, 13, 2012) |
+−−-------------------+
| 2262012             |
+−−-------------------+
1 row in set (0.00 sec)

如果我想创建自己的库怎么办?

你可以使用udf_example库作为你自己库的开始,或者复制它并创建你的 UDF 库。要在/sql文件夹中编译您自己的 UDF 库,编辑/sql/CMakeLists.txt文件并复制下面的代码块,替换库名的udf_example。您需要在执行make命令之前重新运行cmake命令。

IF(WIN32 OR HAVE_DLOPEN AND NOT DISABLE_SHARED)
  ADD_LIBRARY(udf_example MODULE udf_example.cc)
  SET_TARGET_PROPERTIES(udf_example PROPERTIES PREFIX "")
  # udf_example depends on strings
  IF(WIN32)
    IF(MSVC)
SET_TARGET_PROPERTIES(udf_example PROPERTIES LINK_FLAGS "/DEF:${CMAKE_CURRENT_SOURCE_DIR}/udf_example.def")
    ENDIF()
    TARGET_LINK_LIBRARIES(udf_example strings)
  ELSE()
    # udf_example is using safemutex exported by mysqld
    TARGET_LINK_LIBRARIES(udf_example mysqld)
  ENDIF()
ENDIF()

UDF 库可以帮助您扩展服务器的能力,以满足几乎任何计算需求。这些库很容易创建,并且只需要少量的函数来实现。除了需要 Linux 的动态加载版本之外,UDF 工作得很好,没有什么特殊的配置需求。

添加本地函数

本地函数是作为 MySQL 服务器的一部分编译的函数。它们无需从库中加载就可以使用,因此它们总是可用的。它们还可以直接访问服务器内部,这是 UDF 所不具备的,从而允许本地函数响应或启动系统操作。从ABS()UCASE(),等等,有一长串可用的本地函数。有关当前支持的本机函数集的更多信息,请参考在线 MySQL 参考手册。

如果您想要使用的函数不可用(它不是内置的本地函数之一),您可以通过修改服务器源代码来添加自己的本地函数。现在您有了一个JULIAN函数,如果有一个等价的函数将儒略历日期转换回公历日期不是更好吗?在这一节中,我将向您展示如何添加一个新的本机函数。

添加新的本地函数的过程包括更改mysqld源代码文件。我们需要创建两个类:Item_func_gregorian 和 Create_func_gregorian。服务器为每个调用该函数的 SQL 语句实例化一次 Item _ func _ gregorian 然后它调用这个类的成员函数进行实际的计算,对结果集的每一行进行一次。Create_func_gregorian 仅在服务器启动时实例化一次。这个类只包含一个工厂成员函数,当服务器需要创建一个 Item_func_gregorian 的对象时调用这个函数..您需要更改的文件总结在表 7-1 中。

表 7-1 。对 mysqld 源代码文件的更改,用于添加新的本机函数

文件 变更描述
item_create.cc 添加用于注册函数、帮助器方法和符号定义的函数类定义。
物料 _str_func.h 添加函数类定义。
item_str_func.cc 添加 Gregorian 函数的实现。

image 注意文件位于源代码树根下的/sql目录中。

LEX 1 文件怎么了?

熟悉 MySQL 5 . 6 . 5 之前的早期版本的读者可能还记得词法分析器文件 lex*和 sql_parse.yy。这些文件仍然在源代码文件中,但是 MySQL 开发者已经通过几乎完全消除修改 lex 和 yacc 代码的需要,使添加新的函数和命令变得更加容易。正如我们将在下一节中看到的,对于 SQL 命令,我们仍然必须这样做,但是对于函数和类似的扩展,代码进行了更改,以便更容易修改和删除创建新保留字的限制。新的保留字可以对想要在 SQL 语句中使用保留字的用户施加限制。

让我们开始添加公历函数注册码。打开item_create.cc文件,添加实例化,如清单 7-12 所示。您可以添加这个行号为 2000 的行,就在其他Create_func_*类定义的后面。

清单 7-12?? 添加 Create_func_gregorian 类

/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add gregorian class definition */
class Create_func_gregorian : public Create_func_arg1
{
public:
  virtual Item *create(THD *thd, Item *arg1);

  static Create_func_gregorian s_singleton;

protected:
  Create_func_gregorian() {}
  virtual ∼Create_func_gregorian() {}
};
/* END CAB MODIFICATION */

来自清单 7-12 的代码创建了一个类,解析器可以用它将格里高利函数(稍后定义)与GREGORIAN符号关联起来(参见清单 7-14 )。这里的 Create 函数创建了一个Create_func_gregorian类的 singleton(所有线程都使用的类的单个实例),解析器可以用它来执行 Gregorian 函数。

接下来,我们为Create_function_gregorian方法本身添加代码。清单 7-13 显示了对这段代码的修改。您可以将这段代码放在文件中另一个Create_func_方法之后的第 4700 行。此代码用于返回 singleton 的实例,并执行 Gregorian 函数并返回其结果。这里是调用 Gregorian 函数并将结果返回给用户的地方。

清单 7-13?? 添加 Create_func_gregorian 方法

/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add gregorian singleton create method */
Create_func_gregorian Create_func_gregorian::s_singleton;

Item*
Create_func_gregorian::create(THD *thd, Item *arg1)
{
  return new (thd->mem_root) Item_func_gregorian(arg1);
}
/* END CAB MODIFICATION */

最后,我们必须添加格里高利符号。清单 7-14 显示了定义符号所需的代码。您必须将它放在定义以下数组的部分中。

static Native_func_registry func_array[] = {

我将代码放在了GREATEST符号定义之后,因为该数组旨在按字母顺序定义符号。

清单 7-14 添加公历符号

/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add gregorian symbol */
  { { C_STRING_WITH_LEN("GREGORIAN") }, BUILDER(Create_func_gregorian)},
/* END CAB MODIFICATION */

看一下符号定义。注意清单 7-14 中的是如何用Create_func_gregorian类调用宏BUILDER的。调用宏是解析器将我们的公历代码与GREGORIAN符号关联起来的方式。您可能想知道当检测到符号时,如何使用这种关联来告诉解析器做什么。使用的机制被称为词法哈希

词汇散列是 Knuth 著作中的高级散列查找过程的实现。 2 它是使用实现算法的命令行实用程序生成的。实用程序gen_lex_hash有一个名为gen_lex_hash.cc的源代码文件。这个程序生成一个文件,你可以用它来替换现有的词法哈希头文件(lex_hash.h)。我把对BUILDER宏的探索留给你进一步研究。

既然 create 函数已经实现,您需要创建一个新的类来实现该函数的代码。这是大多数开发者非常困惑的地方。Oracle 提供了大量的Item_xxx_func基类(和派生类)供您使用。例如,对于返回字符串的函数,从Item_str_func派生您的类;对于返回整数的函数,从Item_int_func派生您的类。类似地,对于返回其他类型的函数,也有其他类。这背离了可动态加载的 UDF 接口,也是你选择创建本地函数而不是可动态加载的函数的主要原因。关于有哪些Item_xxx_func类的更多信息,请参见源代码树根下的/sql目录中的item_func.h文件。

由于 Gregorian 函数会返回一个字符串,所以需要从Item_str_func类派生,在item_strfunc.h中定义类,在item_strfunc.cc中实现类。打开item_strfunc.h文件,将类定义添加到头文件中,如清单 7-15 所示。

清单 7-15?? 修改 item_strfunc.h 文件

/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add gregorian Item function code */
class Item_func_gregorian :public Item_str_func
{
  String tmp_value;
public:
  Item_func_gregorian(Item *a) :Item_str_func(a) {}
  const char *func_name() const { return "gregorian"; }
  String *val_str(String *);
  void fix_length_and_dec()
  {
   max_length=30;
  }
};
/* END CAB MODIFICATION */

注意在清单 7-15 中的类只有四个必须声明的函数。所需的最小函数是构造函数(Item_func_gregorian)、包含执行转换的代码的函数(val_str)、返回名称的函数(func_name)以及设置字符串参数最大长度的函数(fix_length_and_dec)。您可以添加您可能需要的任何其他组件,但是这四个组件是返回字符串的函数所必需的。

其他的项目基类(和派生类)可能需要额外的函数,比如val_int()val_double()等等。检查您需要从中派生的类的定义,以便标识必须重写的方法;这些被称为虚函数。

还要注意,我们在清单 7-15 的中实现了一个fix_length_and_dec()方法,服务器用它来设置最大长度。在这种情况下,我们选择 30,这在很大程度上是任意的,但足够大,不会对我们返回的值造成问题。

让我们添加类实现。打开item_strfunc.cc文件,添加如清单 7-16 所示的 Gregorian 类函数的实现。您需要实现主函数val_str(),它完成儒略历到公历的运算。在另一个val_str()实现之后,您可以将它放在文件的第 4030 行。

清单 7-16?? 修改 item_strfunc.cc 文件

/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add gregorian function code */
String *Item_func_gregorian::val_str(String *str)
{
  static int DAYS_IN_MONTH[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
  longlong jdate = args[0]->val_int();
  int year = 0;
  int month = 0;
  int day = 0;
  int i;
  char cstr[30];
  cstr[0] = 0;
  str->length(0);

  /* get date from value (right 4 digits */
  year = jdate - ((jdate / 10000) * 10000);

  /* get value for day of year and find current month*/
  day = (jdate - year) / 10000;
  for (i = 0; i < 12; i++)
    if (DAYS_IN_MONTH[i] < day)
      day = day - DAYS_IN_MONTH[i]; /* remainder is day of current month */
    else
    {
      month = i + 1;
      break;
    }

  /* format date string */
  sprintf(cstr, "%d", month);
  str->append(cstr);
  str->append("/");
  sprintf(cstr, "%d", day);
  str->append(cstr);
  str->append("/");
  sprintf(cstr, "%d", year);
  str->append(cstr);
  if (null_value)
    return 0;
  return str;
}
/* END CAB MODIFICATION */

编译和测试新的本地函数

重新编译你的服务器并重启它。如果您在编译期间遇到错误,请返回并检查您输入的语句是否有错误。一旦错误被纠正,您就有了一个新的可执行文件,停止您的服务器,将新的可执行文件复制到您的 MySQL 安装位置,然后重新启动服务器。现在可以执行本地函数 Gregorian,如清单 7-17清单 7-18 所示。为了测试 Gregorian 函数的正确性,首先运行julian()命令,并将该值作为gregorian()函数的输入。

清单 7-17?? 运行 julian()函数

mysql> select julian(8,15,2012);

+−−-----------------+
| julian(8,15,2012) |
+−−-----------------+
| 2272012           |
+−−-----------------+
1 row in set (0.00 sec)

清单 7-18 运行公历()函数

mysql> select gregorian(2272012);

+−−------------------+
| gregorian(2272012) |
+−−------------------+
| 8/15/2012          |
+−−------------------+
1 row in set (0.00 sec)

添加原生函数就这些了。现在您已经了解了如何创建本地函数,您可以进一步规划与 MySQL 的集成,以包括对服务器源代码的定制。

作为一个练习,考虑添加一个新函数来计算给定日期和时间之前的年数、月数、周数、天数和小时数。这个函数可以用来告诉你需要等待事件发生多长时间。从很多方面来说,这个功能是一种倒计时,比如到你下一个生日、周年纪念或者退休的倒计时。

添加 SQL 命令

如果本地 SQL 命令不能满足您的需求,并且您无法使用用户定义的函数解决问题,则可能需要向服务器添加新的 SQL 命令。本节将向您展示如何向服务器添加您自己的 SQL 命令。

许多人认为添加新的 SQL 命令是对 MySQL 服务器源代码最困难的扩展。正如您将看到的,这个过程并不复杂,也不乏味。要添加新的 SQL 命令,您必须修改解析器(在sql/ql_yacc.yy中)并将命令添加到 SQL 命令处理代码(在sql/sql_parse.cc中),有时称为“大开关”

当客户机发出查询时,会创建一个新线程,并将 SQL 语句转发给解析器进行语法验证(或因错误而拒绝)。MySQL 解析器是使用大型的 Lex-YACC 脚本实现的,该脚本是用 Bison 编译的,使用名为 gen_lex_hash 的 MySQL 实用程序将符号转换成 hash,以便在 C 代码中使用。解析器构建一个查询结构,用于将内存中的查询语句(SQL)表示为可用于执行查询的数据结构。因此,要向解析器添加新命令,您需要一份 GNU Bison。你可以从 GNU 网站 3 下载 Bison 并安装。

什么是莱克斯和 YACC,谁是拜森?

lex 代表“词法分析器生成器”,被用作解析器来识别语言的标记、文字和语法。 YACC 代表“又一个编译器编译器”,用于识别和处理语言的语义定义。这些工具与 Bison(一个 YACC 兼容的解析器生成器,它从 Lex/YACC 代码生成 C 源代码)一起使用,提供了一个丰富的机制来创建可以解析和处理语言命令的子系统。事实上,这正是 MySQL 使用这些技术的方式。

假设您想要向服务器添加一个命令,以显示服务器中所有数据库的当前磁盘使用情况。虽然外部工具可以检索这些信息 4 ,但是您希望有一个 SQL 等价函数,可以在您自己的数据库驱动的应用中轻松使用。我们还假设您想将它添加为一个SHOW命令。具体来说,您希望能够执行命令SHOW DISK_USAGE并检索一个结果集,该结果集将每个数据库列为一行,并以千字节为单位列出所有文件(表)的总大小。

添加新的 SQL 命令包括向词法分析器添加符号,向 YACC 分析器(sql_yacc.yy)添加SHOW DISK_USAGE命令语法。Bison 必须将新的解析器编译成 C 程序,然后使用前面描述的gen_lex_hash实用程序创建新的词法散列。解析器将控制指向新命令的代码放在sql_parse.cc的大 case 语句中,并带有新命令符号的 case。

让我们从向词法分析器添加符号开始。打开lex.h文件,找到static SYMBOL symbols[]数组。你可以把这个符号变成你想要的任何东西,但是它应该是有意义的东西(像所有好的变量名一样)。请确保选择一个尚未使用的符号。在这种情况下,使用符号DISK_USAGE。这对解析器来说就像一个标签,将它标识为一个标记。在数组中放置一条语句,指示词法分析器生成符号,并将其命名为DISK_USAGE_SYM。这个列表是按照字母顺序排列的,所以把它放在合适的位置。清单 7-19 显示了添加了符号的数组的摘录。

清单 7-19 对 SHOW DISK_USAGE 命令的 lex.h 文件的更新

static SYMBOL symbols[] = {
  { "&&",    SYM(AND_AND_SYM)},
...
  { "DISK",    SYM(DISK_SYM)},
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* This section identifies the tokens for the SHOW DISK_USAGE command*/
  { "DISK_USAGE",           SYM(DISK_USAGE_SYM)},
/* END CAB MODIFICATION */
  { "DISTINCT",    SYM(DISTINCT)},
...

接下来您需要做的是添加一个助记符来标识该命令。该助记符将在解析器中用于分配给内部查询结构,并通过sql_parse.cc文件中大型 switch 语句中的 case 来控制执行流。打开sql_cmd.h文件,将新命令添加到enum_sql_command枚举中。清单 7-20 显示了新命令助记符的修改。

清单 7-20??【SHOW DISK _ USAGE 命令对 sql_cmd.h 文件的修改

enum enum_sql_command {
...
  SQLCOM_SHOW_SLAVE_HOSTS, SQLCOM_DELETE_MULTI, SQLCOM_UPDATE_MULTI,
  SQLCOM_SHOW_BINLOG_EVENTS, SQLCOM_DO,
  SQLCOM_SHOW_WARNS, SQLCOM_EMPTY_QUERY, SQLCOM_SHOW_ERRORS,
  SQLCOM_SHOW_STORAGE_ENGINES, SQLCOM_SHOW_PRIVILEGES,
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add SQLCOM_SHOW_DISK_USAGE reference */
  SQLCOM_SHOW_STORAGE_ENGINES, SQLCOM_SHOW_PRIVILEGES, SQLCOM_SHOW_DISK_USAGE,
/* END CAB MODIFICATION */
  SQLCOM_HELP, SQLCOM_CREATE_USER, SQLCOM_DROP_USER, SQLCOM_RENAME_USER,
  SQLCOM_REVOKE_ALL, SQLCOM_CHECKSUM,
  SQLCOM_CREATE_PROCEDURE, SQLCOM_CREATE_SPFUNCTION, SQLCOM_CALL,
...

现在您已经有了新的符号和命令助记符,向sql_yacc.yy文件添加代码来定义您在lex.h文件中使用的新令牌,并添加新的SHOW DISK_USAGE SQL 命令的源代码。打开sql_yacc.yy文件,将新令牌添加到令牌列表中(靠近顶部)。这些都是按字母顺序定义的(粗略地),所以要按正确的顺序放置新的令牌。清单 7-21 显示了对sql_yacc.yy文件的修改。

清单 7-21向 sql_yacc.yy 文件添加令牌

...
%token  DISK_SYM
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add DISK_USAGE symbol */
%token  DISK_USAGE_SYM
/* END CAB MODIFICATION */
%token  DISTINCT                      /* SQL-2003-R */
%token  DIV_SYM
%token  DOUBLE_SYM                    /* SQL-2003-R */
...

您还需要将命令语法添加到解析器 YACC 代码中(也在sql_yacc.yy中)。找到show:标签并添加命令,如清单 7-22 所示。

清单 7-22 解析器语法源代码为 SHOW DISK_USAGE 命令

/* Show things */

show:
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add show disk usage symbol parsing */
       SHOW DISK_USAGE_SYM
          {
              LEX *lex=Lex;
              lex->sql_command= SQLCOM_SHOW_DISK_USAGE;
         }
        |
/* END CAB MODIFICATION */
          SHOW
          {

image 小心别忘了原来SHOW语句前的|

您可能想知道这段代码是做什么的。这看起来相当不错,但重要的是要把这一部分做好。事实上,这是大多数开发者放弃和添加新命令失败的阶段。

每当解析器识别出SHOW标记时,就会执行由show:标签识别的代码集。YACC 代码几乎总是这样写的。5SHOW DISK_USAGE_SYM语句表示出现了SHOWDISK_USAGE标记的唯一有效语法(按此顺序)。如果您浏览代码,您会发现其他类似的语法安排。语法语句后面的代码块获得一个指向lex结构的指针,并将command属性设置为新的命令标记SQLCOM_SHOW_DISK_USAGE。这段代码将SHOWDISK_USAGE_SYM符号与SQLCOM_SHOW_DISK_USAGE命令匹配,以便sql_parse.cc文件中的 SQL 命令开关能够正确地将执行路由到SHOW DISK_USAGE命令的实现。

还要注意,我将这段代码放在了show:定义的开头,并在前面的SHOW语法语句前面使用了竖线符号(|)。竖线用作语法开关的“或”。因此,当且仅当语句满足语法语句定义之一时,该语句才有效。请随意查看这个文件,感受一下代码是如何工作的。不要为学习每一个细节而焦虑。我向您展示的是创建一个新命令所需的最基本的知识。如果您决定实现更复杂的命令,请研究类似命令的示例,看看它们是如何处理令牌和变量的。

接下来,将源代码添加到sql_parse.cc中的大型命令语句开关中。打开文件并向 switch 语句添加一个新的 case,如清单 7-23 所示。

清单 7-23?? 为新命令添加一个案例

...
  case SQLCOM_SHOW_AUTHORS:
    res= mysqld_show_authors(thd);
    break;
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add SQLCOM_SHOW_DISK_USAGE case statement */
 case SQLCOM_SHOW_DISK_USAGE:
   res = show_disk_usage_command(thd);
   break;
/* END CAB MODIFICATION */
  case SQLCOM_SHOW_CONTRIBUTORS:
    res= mysqld_show_contributors(thd);
    break;
...

注意,我刚刚添加了一个对名为show_disk_usage_command()的新函数的调用。您将把这个函数添加到sql_show.cc文件中。该函数的名称与lex.h文件中的标记、sql_yacc.yy文件中的符号以及sql_parse.cc文件中的命令开关相匹配。这不仅清楚地表明了正在发生什么,还有助于将已经很大的 switch 语句限制在一定范围内。请随意查看这个文件,因为它是执行命令语句流的核心。您应该能够找到所有的命令,比如SELECTCREATE等等。

现在,让我们添加执行命令的代码。打开sql_show.h文件,添加新命令的函数声明,如清单 7-24 所示。我将函数声明放在了与在sql_parse.cc文件中定义的相同的函数附近。这不是必需的,但它有助于组织代码。

清单 7-24功能声明为新命令

...
int mysqld_show_variables(THD *thd,const char *wild);
bool mysqld_show_storage_engines(THD *thd);
bool mysqld_show_authors(THD *thd);
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add show disk usage method reference */
bool show_disk_usage_command(THD *thd);
/* END CAB MODIFICATION */
bool mysqld_show_contributors(THD *thd);
bool mysqld_show_privileges(THD *thd);
...

最后的修改是添加了show_disk_usage_command()函数的实现(清单 7-25 )。打开sql_show.cc文件,添加新命令的函数实现。清单 7-25 中的代码被删除。这是为了确保在我添加任何代码之前,新命令能够工作。如果您必须实现复杂的代码,这是一个很好的实践。只实现基本功能有助于确定您的代码更改正在工作,并且遇到的任何错误都与存根代码无关。每当修改或添加新的 SQL 命令时,遵循这一实践尤其重要。

清单 7-25??【show _ disk _ usage _ command】实现

/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add show disk usage method */
bool show_disk_usage_command(THD *thd)
{
  List<Item> field_list;
  Protocol *protocol= thd->protocol;
  DBUG_ENTER("show_disk_usage");

  /* send fields */
  field_list.push_back(new Item_empty_string("Database",50));
  field_list.push_back(new Item_empty_string("Size_in_bytes",30));

  if (protocol->send_result_set_metadata(&field_list,
                            Protocol::SEND_NUM_ROWS |
                            Protocol::SEND_EOF))
    DBUG_RETURN(TRUE);

  /* send test data */
  protocol->prepare_for_resend();
  protocol->store("test_row", system_charset_info);
  protocol->store("1024", system_charset_info);
  if (protocol->write())
    DBUG_RETURN(TRUE);

  my_eof(thd);
  DBUG_RETURN(FALSE);
 }

/* END CAB MODIFICATION */

我想提醒大家注意一下源代码。如果您还记得,在前面的章节中,我提到过一些底层的网络函数,它们允许您构建一个结果集并将其返回给客户端。查看由/* send fields */注释指示的代码行。这段代码为结果集创建字段。在本例中,我创建了两个字段(或列),名为DatabaseSize_in_bytes。当执行该命令时,它们将在 MySQL 客户端实用程序中显示为列标题。

请注意protocol->XXX语句。这是我使用Protocol类向客户端发送行的地方。我首先调用prepare_for_resend()来清空缓冲区,然后尽可能多地调用重载的store()方法来设置每个字段的值(按顺序)。最后,我调用write()方法将缓冲区写入网络。如果有任何错误,我会以 true 值退出函数(这意味着产生了错误)。结束结果集并结束与客户机通信的最后一个语句是my_eof()函数,它向客户机发送一个文件结束信号。您可以使用这些相同的类、方法和函数来发送命令的结果。

磁盘使用 SYM 的编译错误

如果您想编译服务器,您可以,但是您可能会遇到关于DISK_USAGE_SYM符号的错误。如果构建服务器时没有使用 cmake 或者跳过了 cmake 步骤,就会发生这种情况。以下内容将帮助您解决这些问题。

如果你一直在研究 MySQL 源代码,你可能已经注意到有sql_yacc.ccsql_yacc.h文件。这些文件是由 Bison 从sql_yacc.yy文件生成的。让我们使用 Bison 来生成这些文件。打开一个命令窗口,导航到源代码根目录下的/sql目录。运行命令:

bison–y–p MySQL–d SQL _ yacc . YY

这会生成两个新文件:y.tab.cy.tab.h。这些文件将分别取代sql_yacc.ccsql_yacc.h文件。在复制它们之前,请备份原始文件。备份文件后,将y.tab.c复制到sql_yacc.cc,将y.tab.h复制到sql_yacc.h

一旦sql_yacc.ccsql_yacc.h文件正确,通过运行以下命令生成词法哈希:

gen_lex_hash > lex_hash.h

现在已经为编译服务器做好了一切准备。由于您已经修改了许多关键头文件,您可能会遇到比正常情况下更长的编译时间。如果您遇到编译错误,请在继续之前更正它们。

但是,如果使用 debug 编译代码,您可能会在mysqld.cc中遇到编译错误。如果出现这种情况,很可能是调用了一个compile_time_assert()宏。如果是这种情况,修改代码如下,以补偿com_status_vars枚举数的差异。

compile_time_assert(sizeof(com_status_vars)/
                                   sizeof(com_status_vars[0]) - 1 == SQLCOM_END + 8–1);

一旦服务器编译完成,您就有了一个新的可执行文件,停止您的服务器,将新的可执行文件复制到您的 MySQL 安装位置,然后重新启动服务器。现在,您可以在 MySQL 客户端实用程序中执行新命令。清单 7-26 显示了一个SHOW DISK_USAGE命令的例子。

清单 7-26 显示磁盘使用命令的执行示例

mysql> SHOW DISK_USAGE;

+−−--------+−−-------------+
| Database | Size_in_bytes |
+−−--------+−−-------------+
| test_row | 1024          |
+−−--------+−−-------------+
1 row in set (0.00 sec)

现在一切都正常了,打开sql_show.cc文件并添加SHOW DISK_USAGE命令的实际代码,如清单 7-27 所示。

清单 7-27??【最终展示 _ 磁盘 _ 用法 _ 命令】源代码

/* This section adds the code to call the new SHOW DISK_USAGE command. */
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add show disk usage method */
bool show_disk_usage_command(THD *thd)
{
  List<Item> field_list;
  List<LEX_STRING> dbs;
  LEX_STRING *db_name;
  char *path;
  MY_DIR *dirp;
  FILEINFO *file;
  longlong fsizes = 0;
  longlong lsizes = 0;
  Protocol *protocol= thd->protocol;
  DBUG_ENTER("show_disk_usage");

  /* send the fields "Database" and "Size" */
  field_list.push_back(new Item_empty_string("Database",50));
  field_list.push_back(new Item_return_int("Size_in_bytes", 7,
                       MYSQL_TYPE_LONGLONG));
  if (protocol->send_result_set_metadata(&field_list,
                                         Protocol::SEND_NUM_ROWS |
                                         Protocol::SEND_EOF))
    DBUG_RETURN(TRUE);

  /* get database directories */
  find_files_result res = find_files(thd, &dbs, 0, mysql_data_home,0,1);
  if (res != FIND_FILES_OK)
    DBUG_RETURN(1);
  List_iterator_fast<LEX_STRING> it_dbs(dbs);
  path = (char *)my_malloc(PATH_MAX, MYF(MY_ZEROFILL));
  dirp = my_dir(mysql_data_home, MYF(MY_WANT_STAT));
  fsizes = 0;
  for (int i = 0; i < (int)dirp->number_off_files; i++)
  {
    file = dirp->dir_entry + i;
    if (strncasecmp(file->name, "ibdata", 6) == 0)
      fsizes = fsizes + file->mystat->st_size;
    else if (strncasecmp(file->name, "ib", 2) == 0)
      lsizes = lsizes + file->mystat->st_size;
  }

  /* send InnoDB data to client */
  protocol->prepare_for_resend();
  protocol->store("InnoDB TableSpace", system_charset_info);
  protocol->store((longlong)fsizes);
  if (protocol->write())
    DBUG_RETURN(TRUE);
  protocol->prepare_for_resend();
  protocol->store("InnoDB Logs", system_charset_info);
  protocol->store((longlong)lsizes);
  if (protocol->write())
    DBUG_RETURN(TRUE);

  /* now send database name and sizes of the databases */
  while ((db_name = it_dbs++))
  {
    fsizes = 0;
    strcpy(path, mysql_data_home);
    strcat(path, "/");
    strcat(path, db_name->str);
    dirp = my_dir(path, MYF(MY_WANT_STAT));
    for (int i = 0; i < (int)dirp->number_off_files; i++)
    {
      file = dirp->dir_entry + i;
      fsizes = fsizes + file->mystat->st_size;
    }

    protocol->prepare_for_resend();
    protocol->store(db_name->str, system_charset_info);
    protocol->store((longlong)fsizes);
    if (protocol->write())
      DBUG_RETURN(TRUE);
  }
  my_eof(thd);

  /* free memory */
  my_free(path);
  my_dirend(dirp);
  DBUG_RETURN(FALSE);
 }

/* END CAB MODIFICATION */

image 注意在 Windows 上,您可能需要在my_malloc()调用中用MAX_PATH代替PATH_MAX,用strnicmp代替strncasecmp

当您编译并加载服务器,然后运行该命令时,您应该会看到类似于清单 7-28 中的示例。

清单 7-28 新 SHOW DISK_USAGE 命令的执行示例

mysql> show disk_usage;
+−−------------------+−−-------------+
| Database           | Size_in_bytes |
+−−------------------+−−-------------+
| InnoDB TableSpace  |  77594624     |
| InnoDB Logs        |  10485760     |
| mtr                |     33423     |
| mysql              |    844896     |
| performance_schema |    493595     |
| test               |      8192     |
+−−------------------+−−-------------+
6 rows in set (0.00 sec)

mysql>

该列表显示了 MySQL 数据目录中服务器上每个数据库的累积大小。您可能想做的一件事是添加一行,返回所有已用磁盘空间的总和(很像一个WITH ROLLUP子句)。我将这一修改留给您,让您在尝试实现该函数时完成。

我希望这篇关于创建新 SQL 命令的短文已经帮助消除了围绕 MySQL SQL 命令处理源代码的一些困惑和困难。现在您已经有了这些信息,您可以规划您自己的 MySQL 命令扩展来满足您自己的独特需求。

添加到信息模式

本章中我想讨论的最后一个领域是向信息模式中添加信息。信息模式是内存中逻辑表的集合,包含关于服务器及其环境的状态和其他相关数据(也称为元数据)。版本 5.0.2 中引入的信息模式已经成为管理和调试 MySQL 服务器、其环境和数据库的重要工具。 6 例如,通过使用以下 SQL 命令,信息模式可以轻松显示数据库中所有表的所有列:

SELECT table_name, column_name, data_type FROM information_schema.columns
WHERE table_schema = 'test';

元数据被分组到逻辑表中,允许您对它们发出SELECT命令。创建一个INFORMATION_SCHEMA视图的最大优势之一是使用SELECT命令。具体来说,您可以使用一个WHERE子句将输出限制为匹配的行。这提供了一种获取服务器信息的独特而有用的方法。表 7-2 列出了一些逻辑表及其用途。

表 7-2 。信息模式逻辑表

名字 描述
概要 提供有关数据库的信息。
桌子 提供有关所有数据库中的表的信息。
提供有关所有表中的列的信息。
统计数字 提供了有关表索引的信息。
用户 _ 权限 提供了有关数据库权限的信息。它封装了 mysql.db grant 表。
表 _ 权限 提供了有关表权限的信息。它封装了 mysql.tables_priv grant 表。
列 _ 权限 提供了有关列权限的信息。它封装了 mysql.columns_priv grant 表。
校对 提供有关字符集排序规则的信息。
关键字 _ 列 _ 用途 提供有关键列的信息。
例行公事 提供有关过程和函数的信息(不包括用户定义的函数)。
视图 提供有关所有数据库中视图的信息。
扳机 提供有关所有数据库中触发器的信息。

因为 disk-usage 命令属于元数据的范畴,所以我将向您展示如何将它添加到服务器的信息模式机制中。这个过程实际上非常简单,不需要修改sql_yacc.yy代码或词法哈希。相反,您可以在为磁盘使用函数创建数据(行)的函数中为 switch 语句添加一个枚举和一个 case,定义一个结构来保存表的列,然后添加源代码来执行它。

让我们从修改新枚举的头文件开始。打开handler.h文件并找到enum_schema_tables枚举。向列表中添加一个名为SCH_DISKUSAGE的新枚举。清单 7-29 显示了添加了新枚举的枚举摘录。

清单 7-29?? 对 enum_schema_tables 枚举的修改

enum enum_schema_tables
{
...
  SCH_COLLATION_CHARACTER_SET_APPLICABILITY,
  SCH_COLUMNS,
  SCH_COLUMN_PRIVILEGES,
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add case enum for the new SHOW DISK_USAGE view. */
  SCH_DISKUSAGE,
/* END CAB MODIFICATION */
  SCH_ENGINES,
  SCH_EVENTS,
  SCH_FILES,
...

现在您需要在创建新模式表的prepare_schema_tables()函数中添加 switch 命令的案例。打开sql_parse.cc文件,添加清单 7-30 中所示的 case 语句。请注意,我只是添加了没有中断语句的案例。这允许代码落入满足所有情况的代码。这是大多数源代码中冗长的if-then-else-if语句的优雅替代。

清单 7-30 对 prepare_schema_table 函数的修改

int prepare_schema_table(THD *thd, LEX *lex, Table_ident *table_ident,
                         enum enum_schema_tables schema_table_idx)
{
...
  DBUG_ENTER("prepare_schema_table");

  switch (schema_table_idx) {
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add case statement for the new SHOW DISK_USAGE view. */
  case SCH_DISKUSAGE:
/* END CAB MODIFICATION */
  case SCH_SCHEMATA:
#if defined(DONT_ALLOW_SHOW_COMMANDS)
    my_message(ER_NOT_ALLOWED_COMMAND,
...

您可能已经注意到,我将磁盘使用模式表称为DISKUSAGE。我这样做是因为已经在解析器和词法哈希中定义了DISK_USAGE标记。如果我使用DISK_USAGE并发出命令SELECT * FROM DISK_USAGE,我会得到一个错误。这是因为解析器将DISK_USAGE标记与SHOW命令相关联,而不是与SELECT命令相关联。

现在我们到了最后一组代码更改。您需要添加一个结构,信息模式函数可以用它来创建表的字段列表。打开sql_show.cc文件并添加一个类型为ST_FIELD_INFO的新数组,如清单 7-31 所示。请注意,这些列的名称和类型与show_disk_usage_command()中的相同。

清单 7-31??【磁盘使用模式表】新字段信息结构

/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* This section adds the code to call the new SHOW DISK_USAGE command. */
ST_FIELD_INFO disk_usage_fields_info[]=
{
  {"DATABASE", 40, MYSQL_TYPE_STRING, 0, 0, NULL, SKIP_OPEN_TABLE},
  {"Size_in_bytes", 21 , MYSQL_TYPE_LONG, 0, 0, NULL, SKIP_OPEN_TABLE },
  {0, 0, MYSQL_TYPE_STRING, 0, 0, 0, SKIP_OPEN_TABLE}
};
/* END CAB MODIFICATION */

您需要做的下一个更改是在schema_tables数组中添加一行(也在sql_show.cc中)。找到数组并添加一个类似于清单 7-32 所示的语句。这说明新表名为 DISKUSAGE,列定义由 disk_usage_fields_info 指定,Create_schema_table 将用于创建表,fill_disk_usage 将用于填充表。make_old_format告诉代码确保显示列名。最后四个参数是一个指针,指向一个对表进行一些额外处理的函数、两个索引字段和一个表示它是一个隐藏表的bool变量。在示例中,我将指向函数的指针设置为NULL (0)–1表示索引未使用,0表示表格未隐藏。

清单 7-32?? 对 schema_tables 数组的修改

ST_SCHEMA_TABLE schema_tables[]=
{
...
  {"ENGINES", engines_fields_info, create_schema_table,
   fill_schema_engines, make_old_format, 0, -1, -1, 0, 0},
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* This section adds the code to call the new SHOW DISK_USAGE command. */
  {"DISKUSAGE", disk_usage_fields_info, create_schema_table,
   fill_disk_usage, make_old_format, 0, -1, -1, 0, 0},
/* END CAB MODIFICATION */
 #ifdef HAVE_EVENT_SCHEDULER
 {"EVENTS", events_fields_info, create_schema_table,
   fill_schema_events, make_old_format, 0, -1, -1, 0, 0},
...

好了,我们到了最后阶段。剩下的就是实现fill_disk_usage()函数了。从schema_tables数组 7 向上滚动,插入fill_disk_usage()函数的实现,如清单 7-33 所示。

清单 7-33??【fill _ disk _ usage】函数实现

/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add code to fill the output for the new SHOW DISK_USAGE view. */
int fill_disk_usage(THD *thd, TABLE_LIST *tables, Item *cond)
{
  TABLE *table= tables->table;
  CHARSET_INFO *scs= system_charset_info;
  List<Item> field_list;
  List<LEX_STRING> dbs;
  LEX_STRING *db_name;
  char *path;
  MY_DIR *dirp;
  FILEINFO *file;
  longlong fsizes = 0;
  longlong lsizes = 0;
  DBUG_ENTER("fill_disk_usage");

  find_files_result res = find_files(thd, &dbs, 0, mysql_data_home,0,1);
  if (res != FIND_FILES_OK)
    DBUG_RETURN(1);
  List_iterator_fast<LEX_STRING> it_dbs(dbs);
  path = (char *)my_malloc(PATH_MAX, MYF(MY_ZEROFILL));
  dirp = my_dir(mysql_data_home, MYF(MY_WANT_STAT));
  fsizes = 0;
  for (int i = 0; i < (int)dirp->number_off_files; i++)
  {
    file = dirp->dir_entry + i;
    if (strncasecmp(file->name, "ibdata", 6) == 0)
      fsizes = fsizes + file->mystat->st_size;
    else if (strncasecmp(file->name, "ib", 2) == 0)
      lsizes = lsizes + file->mystat->st_size;
  }

  /* send InnoDB data to client */
  table->field[0]->store("InnoDB TableSpace",
                         strlen("InnoDB TableSpace"), scs);
  table->field[1]->store((longlong)fsizes, TRUE);
  if (schema_table_store_record(thd, table))
    DBUG_RETURN(1);
  table->field[0]->store("InnoDB Logs", strlen("InnoDB Logs"), scs);
  table->field[1]->store((longlong)lsizes, TRUE);
  if (schema_table_store_record(thd, table))
    DBUG_RETURN(1);

  /* now send database name and sizes of the databases */
  while ((db_name = it_dbs++))
  {
    fsizes = 0;
    strcpy(path, mysql_data_home);
    strcat(path, "/");
    strcat(path, db_name->str);
    dirp = my_dir(path, MYF(MY_WANT_STAT));
    for (int i = 0; i < (int)dirp->number_off_files; i++)
    {
      file = dirp->dir_entry + i;
      fsizes = fsizes + file->mystat->st_size;
    }
    restore_record(table, s->default_values);

    table->field[0]->store(db_name->str, db_name->length, scs);
    table->field[1]->store((longlong)fsizes, TRUE);
    if (schema_table_store_record(thd, table))
      DBUG_RETURN(1);
  }

  /* free memory */
  my_free(path);
  DBUG_RETURN(0);
}
/* END CAB MODIFICATION */

image 注意在 Windows 上,用MAX_PATH代替my_malloc()调用中的PATH_MAX,用strnicmp代替strncasecmp

我复制了前面的DISK_USAGE命令的代码,删除了创建字段的调用(通过disk_usage_fields_info array处理)和向客户端发送行的代码。相反,我使用了一个TABLE类/结构的实例来存储fields数组中的值,从第一列的零开始。对函数schema_table_store_record()的调用将值转储到网络协议。

现在已经为编译服务器做好了一切准备。由于您已经修改了其中一个关键头文件(handler.h),您可能会遇到比正常情况下更长的编译时间,因为可能需要编译mysqld项目的一些依赖项。如果您遇到编译错误,请在继续之前更正它们。

一旦服务器编译完成,您就有了一个新的可执行文件,停止您的服务器,将新的可执行文件复制到您的 MySQL 安装位置,然后重新启动服务器。现在,您可以在 MySQL 客户端实用程序中执行新命令。清单 7-34 展示了一个使用信息模式的例子,显示所有可用的模式表,并转储新的DISKUSAGE表的内容。

清单 7-34示例信息模式与新磁盘使用模式一起使用表

mysql> use INFORMATION_SCHEMA;
Database changed

mysql> SHOW TABLES LIKE 'DISK%';
+−−------------------------------------+
| Tables_in_information_schema (DISK%) |
+−−------------------------------------+
| DISKUSAGE                            |
+−−------------------------------------+
1 row in set (0.00 sec)

mysql> SELECT * from DISKUSAGE;
+−−------------------+−−-------------+
| DATABASE           | Size_in_bytes |
+−−------------------+−−-------------+
| InnoDB TableSpace  |  77594624     |
| InnoDB Logs        |  10485760     |
| mtr                |     33423     |
| mysql              |    844896     |
| performance_schema |    493595     |
| test               |      8192     |
+−−------------------+−−-------------+
6 rows in set (0.00 sec)

mysql>

既然您已经知道了如何添加到信息模式中,那么您可以添加的内容就没有限制了,这样您的数据库专业人员就可以更密切地监控和调优您的 MySQL 服务器。

摘要

在这一章中,我已经向你展示了如何通过添加你自己的新函数和命令来扩展 MySQL 服务器的功能。

您了解了如何构建一个可以在运行时加载和卸载的 UDF 库,如何向服务器源代码添加一个本机函数,以及如何向解析器和查询执行代码添加一个新的SHOW命令。您还学习了如何向信息模式添加视图。

以这种方式扩展服务器的能力使得 MySQL 非常灵活。UDF 机制是最容易编码的机制之一,它在复杂性和开发速度方面远远超过了竞争对手。该服务器是开源的,这意味着您也可以直接进入源代码,并为您的特定环境添加自己的 SQL 命令。不管您是否使用这些工具,您都应该知道您不会受到“开箱即用”功能和命令的限制。

下一章将探讨 MySQL 最受欢迎的特性之一——MySQL 复制。我将介绍复制的基础知识,并带您浏览复制源代码。接下来是复制的示例扩展,您可以使用这些扩展来了解复制的内部原理,并了解可以用来增强您自己的高可用性解决方案的扩展。

词汇分析器和 yacc 文件——不要和经常被称为 Lexx 的古怪科幻程序混淆。(注:加州大学 YACC 分校?)

2 Knuth,d . e .计算机编程的艺术。第二版。(艾迪森-韦斯利,1997)。

3 Linux/Unix 用户既可以使用他们的软件包管理器并安装它,也可以从 GNU 网站下载(www.gnu.org/software/bison)。Windows 用户可以从http://gnuwin32.sourceforge.net/packages/bison.htm下载 Win32 版本。

4 例如,MySQL Utilities 实用程序 mysqldiskusage。MySQL 工具是 MySQL 工作台的一个子项目。你可以从 dev.mysql.com 的下载 MySQL Workbench。

5 要了解更多关于 YACC 解析器以及如何编写 YACC 代码的信息,请参见http://dinosaur.compilertools.net/

6 关于信息模式的更多信息,请参见在线 MySQL 参考手册。

7 记住,如果你不使用函数声明,你必须把函数的代码放在引用它的代码的前面。

八、扩展 MySQL 高可用性

MySQL 的一个高级特性是它能够提供高可用性数据库解决方案。负责获得高可用性的服务器组件是复制。有些可能包括其他特性,如分区和许多更小的特性,但是支持高可用性的最重要的特性是复制。

在这一章中,你将通过一个关于 MySQL 复制的简短教程来学习什么是复制以及它的基本配置。有了这些基本技能和我们在前面章节中学到的技能,您将会浏览复制源代码,并学习如何通过示例项目来扩展复制。首先,让我们了解什么是复制以及它是如何工作的。

什么是复制?

MySQL 复制是将一台服务器上发生的数据变化复制到另一台服务器上的能力。不是直接复制数据(当应用UPDATEDELETE语句时,这可能会很慢且复杂),而是以元数据和命令的形式传输更改,因此是事件,它们被复制到第二个服务器并在那里执行。

这些事件被写入原始服务器上称为二进制日志的有序顺序文件,第二个服务器通过远程连接读取这些事件,并将它们存储在称为中继日志的相同格式的文件中。然后在第二个服务器上从中继日志中一次读取一个事件并执行。

这提供了从原始服务器精确复制数据更改的能力,因为它保留了事件的顺序,并确保使用通过服务器的相同路径。因此我们称这个过程为复制,因为它在变化发生时复制变化。我们把服务器之间的连接称为复制拓扑。

一个服务器可以执行几个角色。下面简要介绍每一种。

  • 主服务器—该服务器是所有 write 和 DML 语句被发送到的原始服务器。
  • slave—此服务器是通过复制事件维护数据副本的服务器。
  • 中继从服务器—该服务器执行从服务器的角色,同时也是一个或多个从服务器的主服务器。

实际上,Slaves 是只读的。这是为了确保只有一个位置可以引发事件(数据更改)。因此,主机是您的所有写入都应该指向的位置。从机使用两个线程 2 :一个从主机读取事件的输入/输出(IO)线程和一个从中继日志执行事件的 SQL 线程。我将在后面的部分解释这些线程如何工作的细节。

image 注意MySQL 的最新版本包括一个多线程从模块,它使用一个 IO 线程来读取主模块的二进制日志条目,并使用多个 SQL 线程来执行事件。对于允许事件并行运行(例如,由数据库隔离)的复制安装,多线程从属可以提高复制性能。有关多线程从机(mt)的更多详细信息,请参见在线参考手册。

下面几节将解释为什么要使用复制,除了复制日期之外它还提供了什么功能,以及使用复制需要什么。完整地解释复制系统的每个方面和细微差别需要一整本书。我没有试图解释关于复制的所有知识,而是从更广的角度介绍复制,这将使您能够快速入门。

如果您计划试验复制,本章的内容应该足够了。如果您计划将复制用于高级高可用性解决方案,您应该阅读本章,并仔细研究在线参考手册中的复制章节。

为什么使用复制?

使用复制有很多原因。我上面所描述的——一个主机和一个从机——是构建拓扑的最基本的构建模块。这种简单的主从拓扑结构在从设备上提供了数据的冗余副本,使您能够保留一份副本,以防主设备发生问题,或者如果您希望将写入和读取分开以获得更好的应用性能。

但是你能做的远不止这些。如果主服务器出现故障,您可以将从服务器用作热备用服务器,并且可以使用从服务器来运行备份和维护操作,这些操作通常需要使服务器脱机。在这种情况下,您可以暂时停止从属服务器处理事件(一旦条件允许),进行备份,然后重新启动从属服务器。从模块将开始读取事件,从主模块二进制日志中的下一个事件开始。

具有许多客户端的应用可以看到,通过使用多个从设备来允许同时读取,读取操作有了显著的改进。这一过程称为横向扩展,是高可用性解决方案的构造块。

这些只是复制的一些用途。表 8-1 总结了复制可以实现的主要功能。

表 8-1 。复制的多种用途

使用 描述
支持 运行需要使服务器离线(或不离线)的备份操作。
超过尺寸范围 添加更多从机以提高读取吞吐量。
热备用 为主设备提供替代品,以显著减少停机时间。
数据分析 在从设备而不是主设备上执行资源密集型操作,以避免与其他活动应用发生冲突。
排除故障 对复杂查询进行潜在的侵入性诊断,并在不影响生产数据库的情况下优化数据库设计。
发展 为新应用的开发提供接近生产质量的数据,以帮助避免制造可能不代表实际数据值、范围和大小的数据。

正如您所猜测的,复制会对资源提出更多的要求。令人高兴的是,复制可以在任何支持 MySQL 的机器上运行。在采用多层主设备和许多从设备的大型安装中,主设备通常安装在具有更大内存、网络连接和更快磁盘系统的机器上。这是因为在主设备上执行了大量的写操作(在大多数情况下,写操作比读操作花费更多的时间)。

另一方面,从机通常安装在为读操作而优化的机器上。在您可能希望将从服务器用作主服务器的热备用服务器的情况下,您应该使用与主服务器相同的硬件。这使得改变角色不太可能遇到性能问题。

本节用最简洁的术语简要介绍了 MySQL 复制。有关复制及其所有细微差别的更深入的内容,请参见在线参考手册。

复制如何实现高可用性?

您可能想知道复制与高可用性有什么关系,因为大多数人认为高可用性是一种很少停机的状态(除了短暂的、有意计划的事件)。复制通过提供将主节点的角色从主节点切换到另一个有能力的从节点的能力来实现高可用性。我们说有能力,是因为并不是所有的奴隶都适合担当主人的角色。如果从设备的硬件与主设备有很大的不同(较慢),您不会希望选择该从设备作为新的主设备。有多种方法可以将存储作为主服务器的备用服务器,也有多种方法可以切换角色。通常,我们认为有两种改变角色的方法:切换和故障转移。

如果主服务器是健康的,但是您需要改变角色,因为您需要在主服务器上执行维护,或者主服务器上发生了一些事情,但是没有完全禁用它,我们将这种切换称为,因为您正在将角色从主服务器切换到从服务器。

如果主服务器崩溃或脱机,我们必须选择一个从服务器来创建新的主服务器。在这种情况下,我们称之为故障转移,因为失败是改变角色的动力。

这就是高可用性发挥作用的地方。如果您有几个能够接管主服务器的从服务器,特别是当主服务器出现故障时,您可以通过将主服务器的角色快速切换到其中一个从服务器来避免潜在的长时间停机以及更重要的数据丢失。

最近已经投入了一些努力来试图实现几乎零停机时间,即使在完全失去主设备的情况下。Oracle 为复制添加了新功能,使管理员能够设置自动故障转移。这是通过服务器中一个名为全局事务标识符(GTID) 的特性和 MySQL 实用程序套件(见下文)中一个名为mysqlfailover 的脚本的组合来实现的。虽然对 GTID 和自动故障转移的深入研究超出了本书的范围,但是请参阅侧栏“什么是 GTID?”以获得该过程的概述。

什么是 GTID?

GTIDs 使服务器能够为每个事件集或组分配一个唯一的标识符,从而可以知道每个从服务器上应用了哪些事件。要使用 GTIDs 执行故障转移,可以选择最好的从设备(丢失事件最少的设备,并且硬件与主设备最匹配),并使其成为所有其他从设备的从设备。我们称这个从设备为候选从设备。GTID 机制将确保只应用那些没有在候选从设备上执行的事件。通过这种方式,候选从设备成为最新的,因此成为主设备的替代。

mysqlfailover命令行工具监控原来的主服务器,通过执行上述事件序列来执行自动故障转移,并负责将剩余的从服务器重定向到新的主服务器。

因此,GTIDs 使得复制更有能力提供高可用性数据库解决方案。有关 GTIDs 和使用 MySQL 实用程序的自动解决方案的更多信息,请访问:

http://dev.mysql.com/doc/refman/5.6/en/replication-gtids.html

http://dev.mysql.com/doc/workbench/en/mysqlfailover.html

http://drcharlesbell.blogspot.com/2012/04/mysql-utilities-and-global-transaction.html

在 GTIDs 之前,在从设备上执行并存储在从设备的本地二进制日志中的复制事件将被重新应用,因此,如果没有读取和执行这些事件的从设备试图建立复制,则会导致错误。虽然这听起来有点奇怪,但在线参考手册对这个主题的讨论要详细得多。

正如你所看到的,你可以通过复制完成很多事情。到目前为止,我所讨论的实际上只是基础。您可以在联机参考手册中找到有关复制的更深入的信息。如果您想探索复制的高级功能并配置您的系统以获得最大的高可用性,我推荐我的书 MySQL 高可用性,由 O'Reilly Media 出版。

在下一节中,我将演示如何设置复制,并讨论建立复制拓扑所需的一些一般原则和命令。

基本复制设置

您可能认为复制可能很难设置或者很复杂。情况也不是这样。复制非常容易设置,命令也很少。此外,如果设置正确,复制非常稳定,很少出现与数据配置无关的问题、用户引发的问题(例如意外的DROP TABLE)或数据损坏。尽管如此,Oracle 仍在不断改进复制,增加了一些功能以实现更可靠的复制,例如使用校验和检查传输过程中的数据损坏。

在这一节中,我将介绍复制的要求,解释在设置复制时使用的命令,并介绍在主设备和从设备之间建立复制的标准方法。

复制的要求

为了设置复制,您至少需要两台服务器:一台作为应用应用更改的原始数据的主服务器(主服务器),另一台作为应用读取数据的位置(从服务器)。

最严格的要求是拓扑中的每台服务器都必须有唯一的服务器标识符。这可以在选项文件中用server-id=N设置,作为命令行用- server-id=N 设置,或者用SET GLOBAL server_id=N SQL 命令设置。您也可以使用命令的替代形式;SET @@GLOBAL.server_id=N

主机必须启用二进制日志记录。最简单的方法是使用--log-bin选项。此选项允许您指定用于二进制日志的路径和文件名。只包括文件名,不包括扩展名。复制系统将附加一个由六位数字组成的扩展名,代表二进制日志的序列号。每次轮换二进制日志时,该数字都会增加。

使用FLUSH BINARY LOGS命令可以完成二进制日志(和中继日志)的旋转。当发出这个命令时,当前文件被关闭(在事务完成之后),并且创建一个具有递增序列号的新文件。

您可以在标准的 my . CNF(Windows 的 my.ini)文件中找到如何打开二进制日志记录的示例。我在下面提供了一个例子,展示了如何打开二进制日志并设置二进制日志格式。

# Uncomment the following if you want to log updates
log-bin=mysql-bin

# binary logging format - mixed recommended
binlog_format=mixed

循环意味着日志被刷新(缓存的事件被写入磁盘)和关闭,并打开一个新的日志文件。旋转二进制日志可以使用以下命令手动完成:

mysql> FLUSH BINARY LOGS;

您还需要设置一个拥有复制事件权限的特殊用户。更具体地说,它用于从主服务器的二进制日志中读取事件。您可以在主服务器上使用以下命令来实现这一点。您只需要运行这个命令一次。该用户和密码用于从设备上的特殊命令,以连接到主设备。

mysql> GRANT REPLICATION SLAVE ON *.* TO 'rpl'@'%' IDENTIFIED BY 'secret';

你还需要所谓的主二进制日志的坐标。这些包括当前二进制日志的名称和最近事件的位置,通过使用SHOW MASTER STATUS命令发现,如下所示。

mysql> SHOW MASTER STATUS;
+−−----------------+−−--------+−−------------+−−----------------+−−------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set  |
+−−----------------+−−--------+−−------------+−−----------------+−−------------------+
| mysql-bin.000001 |      245 |              |                  |                    |
+−−----------------+−−--------+−−------------+−−----------------+−−------------------+
1 row in set (0.00 sec)

请注意此视图中显示的列。您可以看到当前的二进制日志文件和当前位置。还有两列显示运行中的任何二进制日志过滤器。不鼓励使用二进制日志筛选器,因为在激活时,没有通过筛选器的事件不会写入二进制日志。有关从属服务器上的二进制日志过滤器和复制过滤器的更多信息,请参见联机参考手册。

最后,如果您在主服务器上有任何数据,请将当前状态的数据复制到从服务器;这通常通过备份和恢复过程来完成。如果打开了二进制日志记录,您需要在进行复制时锁定表,以便在复制数据时没有事件被写入二进制日志。如果您以独占方式使用 InnoDB 存储引擎,您可以使用一致的读锁来锁定表,但仍然允许读。从包含数据的主机向从机复制数据的基本过程是:。

  1. FLUSH TABLES WITH READ LOCK锁定主机上的表。
  2. 复制数据。你可以用任何你想用的方法。对于少量数据,可以使用 mysqldbexport(来自 MySQL 实用程序)或 mysqldump 客户端应用。
  3. SHOW MASTER STATUS记录主日志文件和位置。
  4. UNLOCK TABLES解锁主机上的桌子。
  5. 将数据导入从机。例如,使用 mysqldbimport 导入由 msyqldbexport 生成的文件,或者通过 mysql 客户端中的 source 命令读取 mysqldump 输出。
  6. 使用(3)中的值开始复制。

现在我已经解释了设置复制的要求,让我们看看如何配置主服务器和从服务器。以下部分假设您有两台安装了 MySQL 的兼容硬件的服务器。这些例子没有使用 GTIDs,但是我注意到了下面的区别。

还假设您已经将任何现有数据从要配置为主服务器的服务器复制到要配置为从服务器的服务器,并且主服务器不会因为锁定表或因为没有连接客户端而出现写操作(数据更改)。这一点很重要,因为在建立复制时发生写入可能意味着您为从属服务器选择了错误的主日志文件和位置(坐标)。

配置主机

如果服务器正在运行,检查它是否打开了二进制日志记录。执行一个类似' log_bin '的显示变量。您应该会看到类似的结果:

mysql> SHOW VARIABLES LIKE 'log_bin';
+−−---------------------------------------+−−--------------------+
| Variable_name                           | Value                |
+−−---------------------------------------+−−--------------------+
| log_bin                                 | ON                   |
+−−---------------------------------------+−−--------------------+
1 rows in set (0.00 sec)

注意变量log_bin的值。在这个例子中,它被打开(ON)。如果您的服务器的这个值是OFF,关闭服务器并打开二进制日志记录,或者通过命令行,如果您以这种方式启动服务器,或者通过选项文件。对于打算用于应用的服务器,最好将设置放在配置文件中。

要通过命令行设置变量,请在启动服务器时添加以下选项。第一个选项告诉服务器对事件使用行格式(这是可选的,但建议使用),第二个选项是打开二进制日志记录并使用mysql_bin 作为文件名(不带扩展名),第三个选项用于设置唯一的server_id 。一定要检查你的奴隶,以确保它有一个不同的server_id值。

--binlog-format=row --log-bin=mysql_bin --server-id=5

image 注意您可以动态设置server_id,但这不会将值保存到选项文件中。因此,当服务器重新启动时,该值将恢复为默认值或从选项文件中读取的值。

要在配置文件中设置变量,打开名为 my.cnf 的配置文件(或任何您命名的文件)并添加以下行。对于使用预定义配置文件的 MySQL 安装,您可能会看到这些条目被注释掉。在这种情况下,只需取消注释即可。将这些值放入[mysqld]部分。

binlog_format=row
log_bin=mysql_bin
server_id = 5

image 确保找到您的服务器正在使用的选项文件。这通常出现在 Mac、Linux 和 Unix 系统的/etc./my.cnf/etc./mysql/my.cnf/usr/local/mysql/etc./my.cnf∼/.my.cnf中。Windows 系统可能会将该文件命名为 my.ini,它通常位于c:\windows\my.inic:\my.ini>\my.ini或、>\.mylogin.cnf中。有关更多信息,请参考在线参考手册的“使用选项文件”一节。

对配置文件进行更改后,重新启动服务器并再次检查以确保二进制日志记录已打开。

要允许从服务器连接到主服务器并读取事件,您必须定义一个复制并发布适当的权限。该用户必须拥有REPLICATION SLAVE权限,并且可以使用如下GRANT语句创建。请确保根据您的域或使用 IP 地址来设置主机名。此外,根据您的信息安全策略设置密码。

mysql> GRANT REPLICATION SLAVE ON *.* TO 'rpl'@'%.mydomain.com' IDENTIFIED BY 'secret';

稍后,当我们将从服务器连接到主服务器并开始复制时,我们将使用这些凭据。

image 提示如果您已经配置您的服务器不允许使用GRANT语句自动创建用户帐户,您必须在GRANT命令之前发出CREATE USER命令。

您需要在主服务器上做的另一件事是发现二进制日志文件的名称及其当前位置。如果您还没有这样做,或者还没有采取措施来确保主服务器上没有发生写操作,请如下所示锁定表。

mysql> FLUSH TABLES WITH READ LOCK;

一旦您确定不再有写入发生,您可以使用如下所示的SHOW MASTER STATUS命令。当我们将从机连接到主机时,我们将使用这些信息。

mysql> SHOW MASTER STATUS;
+−−----------------+−−--------+−−------------+−−----------------+−−------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set  |
+−−----------------+−−--------+−−------------+−−----------------+−−------------------+
| mysql-bin.000152 |      243 |              |                  |                    |
+−−----------------+−−--------+−−------------+−−----------------+−−------------------+
1 row in set (0.00 sec)

注意,在这个例子中,二进制日志已经增加了很多次。对于长期运行的服务器来说,这种情况并不少见。如果您继续操作并设置一个新的主机,您可能会看到二进制对数的值低得多,位置的值也可能更小。这是完全正常的。只需记录这些值以备后用。

如果在发出 SHOW MASTER STATUS 命令之前锁定了表,现在可以使用以下命令解锁它们:

mysql> UNLOCK TABLES;

image 注意如果你正在使用 GTIDs,你不需要知道主坐标。GTID 特性会自动为您解析二进制日志中的起始位置。

配置从机

配置从机要简单一些。您只需要为从机设置server_id。与主服务器一样,如果以这种方式启动服务器(??),可以通过命令行设置该值,但最好的方法是在配置文件中更改该值。打开从属服务器的选项文件,添加或取消注释以下内容。将这些值放入[mysqld]部分。

[mysqld]
...
server_id = 7

将从设备连接到主设备

现在,您已经用二进制日志配置了您的主服务器,并将两个服务器的server_id设置为一个惟一的值,您已经准备好将从服务器连接到主服务器。这需要按顺序发出两个命令。第一个用于指示从设备如何连接到哪个主设备,第二个用于启动从设备上的 IO 和 SQL 线程。

CHANGE MASTER命令建立从机到主机的连接。您可以指定几种选项和连接形式,包括 SSL 连接。对于这个例子,我们使用最基本的选项,并使用普通的 MySQL 身份验证来连接从服务器。有关 SSL 连接和许多选项的更多信息,请参见在线参考手册。

以下是使用上述SHOW MASTER STATUS中的信息将从机连接到主机的CHANGE MASTER命令示例。当你建立你自己的主人和奴隶时,从你的主人那里得到的值。

mysql> CHANGE MASTER TO MASTER_HOST='localhost', MASTER_USER='rpl',
       MASTER_PASSWORD='pass', MASTER_LOG_FILE='mysql_bin.000152',
       MASTER_LOG_POS=243;

如果使用 GTIDs,可以省略主二进制对数坐标,使用特殊选项,如下所示。这告诉服务器开始协商要在从服务器上执行的起始事务。

mysql> CHANGE MASTER TO MASTER_HOST='localhost', MASTER_USER='rpl',
       MASTER_PASSWORD='pass', MASTER_AUTO_POSITION = 1;

下一个命令用于启动从服务器上的线程,并开始从主服务器复制事件。

mysql> START SLAVE;

伴随命令STOP SLAVE,停止从线程,从而停止复制。还有一个命令RESET SLAVE,,用于删除从服务器上的所有复制文件,并重置主服务器的从服务器连接信息。在需要清除从机连接信息的情况下,很少使用 reset 命令。一个等效的RESET MASTER命令删除所有二进制日志文件并清除二进制日志索引。

image 注意确保您确实想要销毁复制信息(RESET SLAVE)或二进制日志信息(RESET MASTER),并且没有从设备连接到主设备(RESET MASTER)。无意中发出这些命令可能会导致复制拓扑发生错误,数据复制停止。

当发出START SLAVE命令时,您可能会看到警告,或者在某些情况下会看到错误。不幸的是,这并不总是那么有用。精明的数据库管理员使用一个特殊的命令——SHOW SLAVE STATUS命令——来检查从命令的状态,该命令在语法上类似于SHOW MASTER命令。这个命令将生成一个单行的非常长(宽)的视图,其中包含许多关于从属服务器内部正在进行的复制的确切信息。查看这些信息的最佳方式是以垂直格式显示(使用\G)。清单 8-1 显示了一个没有错误的从机的典型输出。我用粗体突出显示了要检查的更重要的属性。

清单 8-1。 显示奴隶状态报告

mysql> SHOW SLAVE STATUS \G
*************************** 1\. row ***************************
               Slave_IO_State:
Waiting for master to send event
                  Master_Host: localhost
                  Master_User: rpl
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File:
my_log.000152
          Read_Master_Log_Pos:
243
               Relay_Log_File: clone-relay-bin.000002
                Relay_Log_Pos: 248
        Relay_Master_Log_File: my_log.000152
             Slave_IO_Running:
Yes
            Slave_SQL_Running:
Yes
              Replicate_Do_DB:
          Replicate_Ignore_DB:
           Replicate_Do_Table:
       Replicate_Ignore_Table:
      Replicate_Wild_Do_Table:
  Replicate_Wild_Ignore_Table:
                   Last_Errno: 0
                   Last_Error:
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 243
              Relay_Log_Space: 403
              Until_Condition: None
               Until_Log_File:
                Until_Log_Pos: 0
           Master_SSL_Allowed: No
           Master_SSL_CA_File:
           Master_SSL_CA_Path:
              Master_SSL_Cert:
            Master_SSL_Cipher:
               Master_SSL_Key:
        Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error:
               Last_SQL_Errno: 0
               Last_SQL_Error:
1 row in set (0.00 sec)

养成在设置复制时总是发出这个命令的习惯。它将提供丰富的信息,包括任何问题和错误的更多细节。

后续步骤

如果您已经发出了上面的所有命令,并且在SHOW SLAVE STATUS的输出或命令本身中没有错误,那么恭喜您!您刚刚建立了您的第一个复制拓扑。

如果您想要测试您的复制设置,那么在主服务器上创建一个新的数据库和表,并检查从服务器以确保这些事件在那里被重复。用 SHOW SLAVE STATUS 检查从站,直到它报告它正在等待主站的新事件。否则,您可能会在从属服务器完成从中继日志中读取事件之前检查它。我没有通过示例向您展示这一点,而是将测试您的复制设置作为一个练习。

在下一节中,我将更深入地研究复制最重要的支持特性—二进制日志。

但是等等,没有更好的办法吗?

如果您已经阅读了前面的部分并在您自己的服务器上执行了这些步骤,您可能会认为虽然这个过程很简单,但是有很多要执行的步骤和要检查的内容。您可能想知道为什么没有一个简单的命令来“照做”复制。

我有好消息。有这样一个命令,但它是以与 MySQL Workbench 捆绑在一起的 Python 实用程序的形式。被称为 MySQL 实用程序,它包括两个非常有用的命令:mysqlreplicate,自动设置主服务器和从服务器;和mysqlrplcheck,,其在复制建立之前或之后检查主设备和从设备的先决条件。我将在下面更详细地解释这些工具和 MySQL 工具。

二进制日志

如果不详细查看二进制日志,关于复制的任何讨论都是不完整的。在本节中,我们将进一步了解二进制日志是如何工作的,以及如何使用外部客户端和特殊的 SHOW 命令来读取二进制日志和中继日志。

有些人可能认为二进制日志是复制,但这并不完全正确。任何服务器,包括充当从属服务器的服务器,都可以启用二进制日志记录。在从属服务器的情况下,服务器将包含中继日志和二进制日志。这使得将其他服务器作为从服务器的从服务器可以执行主服务器和从服务器的角色(有时称为中间从服务器)。

二进制日志最初有另一个目的。它可用于数据丢失时的恢复。例如,如果服务器启用了二进制日志,则可以重放二进制日志以将数据恢复到丢失点。这被称为时间点恢复,之所以成为可能,是因为 mysql 客户端可以读取事件并执行它们。您既可以获取文件,也可以将单个事件(或一系列事件)复制并粘贴到 mysql 客户端中。有关时间点恢复的更多信息,请参见在线参考手册。

image 提示要发现用于二进制日志记录的所有变量,发出命令SHOW VARIABLES LIKE '%binlog%'。类似地,要发现用于中继日志的变量,发出命令SHOW VARIABLES LIKE '%relay%'

如前所述,用户可以使用--log-bin选项 来设置二进制日志的名称,通过该选项可以指定一个用于所有二进制日志文件的文件名。旋转二进制日志时,会附加一个六位数递增的新文件作为文件扩展名。还有一个索引文件,其名称是为二进制日志指定的,扩展名为.index 。该文件维护当前的二进制日志文件。服务器在启动时使用这个文件来知道从哪里开始附加事件。因此,二进制日志是文件的集合,而不是单个文件。每当您执行维护或希望归档二进制日志(或中继日志)时,请包含所有相关的文件,包括索引文件。

image 提示您可以使用- relay-log 启动选项 更改中继日志的名称。

二进制日志是使复制成为可能的机制,也是管理员在出现问题时关注的地方。但是首先,让我们检查二进制日志的格式。

行格式

二进制日志是一个顺序文件,其中事件(对数据的更改)被写入文件的末尾。文件位置用于确定下一个二进制日志事件在文件中的偏移量。因此,服务器维护当前二进制日志的名称和下一个位置指针(如上面的SHOW MASTER STATUS所示)。这样,服务器就可以知道下一个事件写在哪里。

虽然有点用词不当,但二进制日志可以被认为是两种主要格式之一:基于语句的或基于行的。还有一个混合版本叫做混合格式。虽然这听起来像是文件的格式不同,但这种格式指的是事件本身。无论事件的格式如何,二进制日志文件本身仍然是一个顺序文件,并且使用简单的文件名加偏移量的概念来读写它。不同的格式有:

  • 基于语句的复制(SBR)—事件包含在主服务器上执行的实际 SQL 语句。这些代码被打包,运送到从机,并在那里执行。
  • 基于行的复制(RBR)—事件包含更改后产生的二进制行。这允许从属服务器简单地将事件应用于行,而不是通过 SQL 接口执行语句。
  • 混合格式 SBR 和 RBR 的这种组合由存储引擎和服务器的其他元素以及正在执行的命令类型控制。默认情况下,混合格式使用 SBR。例如,如果存储引擎支持 RBR,则事件将采用 RBR 格式。同样,如果有理由不使用 RBR,SBR 将被用于该事件。

请参阅在线参考手册,了解哪些命令强制事件采用 SBR 或 RBR 格式。

mysqlbinlog 客户端

所有 MySQL 安装都包括一个用于读取二进制日志的特殊客户端。客户端被令人信服地命名为 mysqlbinlog。客户端的目的是以人类可读的形式显示二进制日志的内容。对于 SBR 格式,实际的查询包含在事件的有效负载中,因此易于阅读。RBR 格式事件是二进制形式的。对于 RBR 事件,客户端将显示任何人类可读的信息,以 ASCII 形式显示行格式。

mysqlbinlog 客户端有许多控制输出的选项。您可以以十六进制格式显示输出,跳过前 N 个事件,在一系列位置显示事件,或者使用更多选项。要读取二进制日志中的一系列事件,可以指定开始日期时间和结束日期时间或者开始和停止位置。对于客户机来说,最强大的特性可能是连接到远程服务器并读取其二进制日志的能力。此功能使管理多台服务器变得更加容易。

清单 8-2 展示了一个针对典型的二进制日志文件运行mysqlbinlog客户端的例子。在这种情况下,该文件来自一个其日志已经轮换过一次的主服务器。我们可以通过用于二进制日志文件扩展名的序列号来判断这一点。每次旋转日志时,该值都会递增。回想一下,这个过程是由FLUSH LOGS启动的,导致现有的二进制日志文件被关闭,一个新文件被打开,一个新的头被写入新文件。

清单 8-2。mysqlbinlog 客户端示例输出

$ mysqlbinlog /usr/local/mysql/data/mysql-bin.000002
/*!40019 SET @@session.max_insert_delayed_threads=0*/;
/*!50003 SET @OLD_COMPLETION_TYPE=@@COMPLETION_TYPE,COMPLETION_TYPE=0*/;
DELIMITER /*!*/;
# at 4
#121016 20:50:47 server id 1  end_log_pos 107 Start: binlog v 4, server v 5.6.7-log created 121016 20:50:47 at startup
ROLLBACK/*!*/;
BINLOG '
5wB+UA8BAAAAZwAAAGsAAAABAAQANS41LjIzLWxvZwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAADnAH5QEzgNAAgAEgAEBAQEEgAAVAAEGggAAAAICAgCAA==
'/*!*/;
# at 107
#121016 20:54:23 server id 1  end_log_pos 198 Query       thread_id=108       exec_time=0       error_code=0
SET TIMESTAMP=1350435263/*!*/;
SET @@session.pseudo_thread_id=108/*!*/;
SET @@session.foreign_key_checks=1, @@session.sql_auto_is_null=0, @@session.unique_checks=1, @@session.autocommit=1/*!*/;
SET @@session.sql_mode=0/*!*/;
SET @@session.auto_increment_increment=1, @@session.auto_increment_offset=1/*!*/;
/*!\C utf8 *//*!*/;
SET @@session.character_set_client=33,@@session.collation_connection=33,@@session.collation_server=8/*!*/;
SET @@session.lc_time_names=0/*!*/;
SET @@session.collation_database=DEFAULT/*!*/;
CREATE DATABASE example1
/*!*/;
# at 198
#121016 20:54:42 server id 1  end_log_pos 303 Query       thread_id=108       exec_time=0       error_code=0
SET TIMESTAMP=1350435282/*!*/;
CREATE TABLE example1.t1(a int, b varchar(20))
/*!*/;
# at 303
#121016 20:55:05 server id 1  end_log_pos 367 Query       thread_id=108       exec_time=0       error_code=0
SET TIMESTAMP=1350435305/*!*/;
BEGIN
/*!*/;
# at 367
#121016 20:55:05 server id 1  end_log_pos 493 Query       thread_id=108       exec_time=0       error_code=0
SET TIMESTAMP=1350435305/*!*/;
insert into example1.t1 values (1, 'one'), (2, 'two'), (3, 'three')
/*!*/;
# at 493
#121016 20:55:05 server id 1  end_log_pos 520 Xid = 141
COMMIT/*!*/;
# at 520
#121016 20:55:17 server id 1  end_log_pos 584 Query       thread_id=108       exec_time=0       error_code=0
SET TIMESTAMP=1350435317/*!*/;
BEGIN
/*!*/;
# at 584
#121016 20:55:17 server id 1  end_log_pos 682 Query       thread_id=108       exec_time=0       error_code=0
SET TIMESTAMP=1350435317/*!*/;
DELETE FROM example1.t1 where b = 'two'
/*!*/;
# at 682
#121016 20:55:17 server id 1  end_log_pos 709 Xid = 148
COMMIT/*!*/;
DELIMITER ;
# End of log file
ROLLBACK /* added by mysqlbinlog */;
/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;

请注意,在输出中,我们看到了许多关于每个事件的元数据,以及一个包含二进制日志文件信息的标题。另外,请注意,每个事件(本例中为 SBR 格式)都显示了日志位置、线程 id 和其他相关信息。时间戳条目是一种特殊形式的事件,服务器使用它来维护日志中的时间戳数据(因此在从属服务器上)。

image 提示二进制日志和中继日志具有相同的布局,因此两者都可以被mysqlbinlog客户端读取。

使用 mysqlbinlog 客户端从二进制或中继日志中读取事件是非常强大的,但是还有一个方便的 SHOW 命令,它以表格形式显示事件。

显示 BINLOG 事件命令

MySQL 服务器包含一个特殊的 SHOW 命令 SHOW BINLOG EVENTS,它允许您查看位于二进制日志中的最新事件。让我们来看看这个命令的运行情况。清单 8-3 显示了 SHOW BINLOG 事件运行的结果,该服务器与前一节中 mysqlbinlog 示例所用的服务器相同。我使用竖排格式是为了更容易阅读。

清单 8-3。 展示 BINLOG 事件示例 1

cbell$ mysql -uroot
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 508
Server version: 5.6.7-m9 MySQL Community Server (GPL)

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> SHOW BINLOG EVENTS \G
*************************** 1\. row ***************************
   Log_name: mysql-bin.000001
        Pos: 4
 Event_type: Format_desc
  Server_id: 1
End_log_pos: 107
       Info: Server ver: 5.6.7-m9, Binlog ver: 4
*************************** 2\. row ***************************
   Log_name: mysql-bin.000001
        Pos: 107
 Event_type: Query
  Server_id: 1
End_log_pos: 245
       Info: SET PASSWORD FOR 'root'@'localhost'='*81F5E21E35407D884A6CD4A731AEBFB6AF209E1B'
*************************** 3\. row ***************************
   Log_name: mysql-bin.000001
        Pos: 245
 Event_type: Stop
  Server_id: 1
End_log_pos: 264
       Info:
3 rows in set (0.00 sec)

您可能想知道前一个示例中的事件发生了什么。这揭示了不熟悉二进制日志记录的用户经常犯的错误。默认情况下,SHOW BINLOG EVENTS命令显示第一个二进制日志中的事件。在上一节中,我使用了第二个二进制日志。要查看二进制日志而不是第一个日志中的事件,可以使用IN子句,如下所示。我使用竖排格式是为了更容易阅读。

清单 8-4。 展示 BINLOG 事件示例 2

mysql> SHOW BINLOG EVENTS IN 'mysql-bin.000002' \G
*************************** 1\. row ***************************
   Log_name: mysql-bin.000002
        Pos: 4
 Event_type: Format_desc
  Server_id: 1
End_log_pos: 107
       Info: Server ver: 5.6.7-m9, Binlog ver: 4
*************************** 2\. row ***************************
   Log_name: mysql-bin.000002
        Pos: 107
 Event_type: Query
  Server_id: 1
End_log_pos: 198
       Info: CREATE DATABASE example1
*************************** 3\. row ***************************
   Log_name: mysql-bin.000002
        Pos: 198
 Event_type: Query
  Server_id: 1
End_log_pos: 303
       Info: CREATE TABLE example1.t1(a int, b varchar(20))
*************************** 4\. row ***************************
   Log_name: mysql-bin.000002
        Pos: 303
 Event_type: Query
  Server_id: 1
End_log_pos: 367
       Info: BEGIN
*************************** 5\. row ***************************
   Log_name: mysql-bin.000002
        Pos: 367
 Event_type: Query
  Server_id: 1
End_log_pos: 493
       Info: insert into example1.t1 values (1, 'one'), (2, 'two'), (3, 'three')
*************************** 6\. row ***************************
   Log_name: mysql-bin.000002
        Pos: 493
 Event_type: Xid
  Server_id: 1
End_log_pos: 520
       Info: COMMIT /* xid=141 */
*************************** 7\. row ***************************
   Log_name: mysql-bin.000002
        Pos: 520
 Event_type: Query
  Server_id: 1
End_log_pos: 584
       Info: BEGIN
*************************** 8\. row ***************************
   Log_name: mysql-bin.000002
        Pos: 584
 Event_type: Query
  Server_id: 1
End_log_pos: 682
       Info: DELETE FROM example1.t1 where b = 'two'
*************************** 9\. row ***************************
   Log_name: mysql-bin.000002
        Pos: 682
 Event_type: Xid
  Server_id: 1
End_log_pos: 709
       Info: COMMIT /* xid=148 */
9 rows in set (0.00 sec)

mysql>

该命令显示日志名称、日志中事件的开始和结束位置(文件偏移量)、事件类型和负载(SBR 事件的查询)。

看起来这些工具 msyqlbinlog 客户端和 SHOW BINLOG EVENTS 命令——就像一个有限的集合,但事实是它们是成为运行二进制日志记录和复制拓扑的服务器的优秀管理员的关键。优先学习这些工具。下面列出了几个资源(除了在线参考手册之外),您可以使用它们来了解有关二进制日志记录和事件复制的更多信息。

额外资源

任何想了解二进制日志及其格式的详细信息的人都可能会对一些相当晦涩的资源感兴趣。当然,在线参考手册有大量的文档,应该是您的主要来源,但是下面包含一些在其他来源找不到的关键信息。

在接下来的小节中,我将深入探讨复制的细节,首先是复制架构的概述,然后是复制源代码的简要介绍。

复制架构

复制源代码非常大,事实上,它是服务器代码中最大的部分之一。由于复制渗透到服务器代码的这么多层次,而且它已经发展了很多年,所以有时很难看出它是如何适应服务器的其他部分的。

因此,花一点时间查看架构本身的高级视图来了解复制中的各个部分是如何工作的是很有帮助的。这也是介绍复制源代码的主要部分的好方法。图 8-1 显示了复制架构的简化框图。该图后面的部分更详细地解释了这些组件。

9781430246596_Fig08-01.jpg

图 8-1。复制架构

上图中带编号的箭头显示了描述复制体系结构的简化事件序列。首先,主机将事件写入二进制日志。其次,从设备的IO_THREAD通过专用网络连接从主设备的二进制日志中读取事件,并将其写入其中继日志。第三,从服务器的SQL_THREAD从中继日志中读取事件,并将其应用(执行)到数据库。

让我们放慢一点,更详细地检查序列。下面是代码序列的概括。在主服务器中,有些事件遵循稍微不同的路径,但一般来说,这就是事件被传送到从服务器的方式。

当用户或应用在主服务器上发出写入二进制日志类型的 SQL 语句时(SHOW命令是一个不被记录的命令),服务器代码调用代码写入binlog.cc,中的二进制日志,然后调用rpl_master.cc中的代码创建一个日志事件类的实例。当写入二进制日志时,调用 log-event 类的pack_info()方法来格式化数据块,以便存储在二进制日志中。这里特别有趣的是,二进制日志类更新它的日志位置,以便服务器和用户知道二进制日志文件中下一个事件的位置。

当从机通过rpl_slave.cc中的代码向主机请求更多事件时,它连接到主机并发出特殊命令COM_BINLOG_DUMP,,该命令通过sql_parse.cc中的大开关执行,并导致 rpl_master.cc 中的代码发送自从机读取的最后一个位置以来二进制日志中的任何事件。读取这些事件时,通过 rpl_slave.cc 中的代码将它们写入从机的二进制日志文件。该过程由从机通过 IO 线程执行。同时,从服务器有另一个线程,SQL 线程,它从中继日志中读取事件并执行它们。这个代码也在rpl_slave.cc里面。

虽然这种解释忽略了许多细节,但它准确地描述了复制体系结构如何实现其目标。这个过程涉及许多行代码,因此将其概括为简单是一种保守的说法。复制源代码一点也不简单,但是它执行起来很优雅,掩盖了它的复杂性。

在下一节中,我将简要介绍复制源代码。由于组成复制功能的元素很多,所以我将重点放在有助于您理解复制工作原理的一个关键方面——日志事件。

复制源代码简介

正如我前面提到的,复制源涉及到服务器代码的许多部分。考虑一下在二进制日志中记录一个事件所涉及的所有内容。显然,我们希望复制的每个命令都必须调用复制代码(在本例中是二进制日志记录代码)来启动事件并将其记录在二进制日志中。将该事件发送到从属服务器并在那里执行,引入代码将该事件放入服务器以供执行——通过执行查询(SBR)或应用结果行(RBR)。

在接下来的几节中,我将带您浏览复制源代码,重点是围绕日志事件的代码。在我们开始这个旅程之前,让我们看一下复制的源代码文件。

复制源代码文件

此外,超过 48 个单独的源代码文件组成了复制源代码。可以想象,这是一个巨大的代码量,因此,很难记住(或学习)所有文件做什么以及每段代码驻留在哪里。

我最近采访了一些开发者(匿名),发现了一个有趣的趋势。虽然有些人对大多数源文件及其功能了如指掌,但即使是最有经验的人也不得不搜索他们的记忆来回忆每个文件包含的内容。可以肯定地说,只有 Oracle 复制开发者自己可以被视为这方面的专家。

我在这里展示的是复制代码最重要的一个方面的私人旅行。对所有代码以及每个类和方法所做的事情的完整浏览将会消耗一整本书,并且需要作者和复制开发者的共同努力。

这里的好消息是,我已经安排了这个教程,为您提供什么是日志事件、它们是如何编码的、它们在哪里编码以及它们如何工作以支持复制的实用知识。

在我们踏上这条荆棘之路之前,通读一下表 8-2 中 48 个复制源文件的概述以及每个文件包含的内容。我相信你会同意这是一份令人生畏的清单。这些文件中的大多数都在源代码树的/sql 文件夹中。

表 8-2 。复制源文件列表

源文件 描述
binlog.h/。直流(DC) 包含二进制日志代码
log_event.h/。复写的副本 为每个定义日志事件和操作
rpl_constants.h/。直流(DC) 复制的全局常数和定义
rpl_filter.h/.cc 版 实现用于二进制日志记录和复制的过滤器
rpl_gtid_cache.cc 类 Gtid_cache,保存线程当前提交的事务中的 Gtid。
rpl_gtid_execution.cc 从机如何重新执行 GTIDs 的逻辑。
rpl_gtid.h 所有 GTID 事物的类定义。
rpl_gtid_misc.cc 将 GTID 转换为字符串,反之亦然。
rpl_gtid_mutex_cond_array.cc 数据结构‘class Mutex _ cond _ array’,这是一个可增长的数组,其中每个元素包含一个互斥体和一个与该互斥体相关联的条件变量。
rpl_gtid_owned.cc 数据结构‘类 Gtid _ owned’,它保存 Gtid 所有权的当前状态。
rpl_gtid_set.cc 数据结构“类 Gtid_set”,它包含一组 Gtid。
rpl_gtid_sid_map.cc 数据结构“类 Sid_map”,它保存 Sid 和数字之间的双向映射。
rpl_gtid_specification.cc 数据结构“类 Gtid_specification”,它保存 GTID_NEXT 的数据类型,即 Gtid 或“匿名”或“自动”。
rpl_gtid_state.cc 数据结构“类 gtid_state”,它保存 Gtid 的全局状态,即提交的 Gtid 集(@ @ global . Gtid _ done/Gtid _ state::logged _ Gtid)、Gtid 所有权状态(@ @ global . Gtid _ owned/Gtid _ state::owned _ Gtid)、丢失的 Gtid(@ @ global . Gtid _ lost/Gtid _ state::lost _ Gtid)
rpl _ handler . h/cc 处理程序接口的帮助函数
rpl_info.h/。直流(DC) 用于存储从属主机信息的基类
rpl_info_dummy.h/.cc 版 rpl_info.h 的虚拟版本
rpl_info_factory.h/。复写的副本 工厂,用于生成工作线程和对类的引用,以操作从属主机的信息
rpl_info_file.h/。直流(DC) 主信息文件操作
rpl _ info _ handler . h/cc 对从设备的主设备信息进行存储、文件刷新和类似操作
rpl_info_table_access.h/。复写的副本 对从机主信息的表级访问
rpl_info_table.h/。直流(DC) 从属主机信息的表级 I/O 操作
rpl_info_values.h/。复写的副本 用于处理从从属主机信息中读取的值的类
rpl_injector.h/。复写的副本 由 NDB(集群)存储引擎用于将事件注入二进制日志
rpl_master.h/.cc 版 定义主角色的复制操作
rpl_mi.h/.直流(DC) 封装从设备的主设备信息
rpl_record.h/。复写的副本 行记录方法
rpl_record_old.h/。复写的副本 5.1 基于行的事件正式发布前旧格式的行记录方法
rpl_reporting.h/。复写的副本 用于在二进制日志文件中报告错误和显示从属状态输出的基类
rpl_rli.h/.直流(DC) 中继日志信息处理
S7-1200 可编程控制器。直流(DC) 中继日志帮助器类,包括哈希和队列
rpl_slave.h/。直流(DC) 定义从属角色的复制操作
rpl_tblmap.h/.直流(DC) 实现表映射事件类型
rpl_utility.h/。复写的副本 包含用于复制的杂项帮助器方法
sql_binlog.h/。直流(DC) SQL 语句 BINLOG,由 mysqlbinlog 生成,用于执行行事件和格式化描述日志事件。

您将在其中工作的主要文件是log_event.h/.cc文件。这些包含日志事件的所有代码。我将在下一节解释主要的日志事件类。还需要研究的是rpl_master.h/.cc文件。这是主角色的代码所在的位置,包括将事件写入二进制日志的代码。如果您正在开发一个解决方案来扩展主服务器角色的复制,请从这些文件开始研究。你可能也想检查一下rpl_slave.h/.cc的文件。这是在从属服务器上执行事件的代码所在的位置。当处理 slave 角色的扩展时,从这些文件开始研究。现在让我们开始浏览日志事件。

日志事件 解释

我们首先检查所有日志事件的基类- Log_event。该类包含通过二进制日志和中继日志存储、传送和执行事件所需的所有方法和属性。打开文件/sql/log_event.h,向下滚动到大约第 962 行,到Log_event类的开头。清单 8-5 显示了log_event.h文件中log-event类的摘录。

为了便于阅读,我省略了一些细节。代码中有很多文档,从枚举的解释到日志事件的字节布局的描述。如果您对这些和类似的细节感兴趣,请参阅源代码中的优秀文档。花点时间浏览一下这段代码。在这一章的后面,我将解释一些关于创建你自己的日志事件类时使用的关键方法的细节。从上面的清单中可以看出,这个类非常复杂。

image 注意有两个log_event*头文件和类文件。log_event_old*文件是那些用于 RBR 事件格式的旧格式的日志事件类。在这一章中,我们将重点讨论更新的日志事件格式。作为练习,您可以自由检查旧的格式。

清单 8-5。 Log_event 类声明

class Log_event
{
public:
enum enum_skip_reason {
    EVENT_SKIP_NOT,
    EVENT_SKIP_IGNORE,
    EVENT_SKIP_COUNT
  };

protected:
  enum enum_event_cache_type
  {
    EVENT_INVALID_CACHE= 0,
    EVENT_STMT_CACHE,
    EVENT_TRANSACTIONAL_CACHE,
    EVENT_NO_CACHE,
    EVENT_CACHE_COUNT
  };

  enum enum_event_logging_type
  {
    EVENT_INVALID_LOGGING= 0,
    EVENT_NORMAL_LOGGING,
    EVENT_IMMEDIATE_LOGGING,
    EVENT_CACHE_LOGGING_COUNT
  };

public:
  typedef unsigned char Byte;
  my_off_t log_pos;
  char *temp_buf;
  struct timeval when;
  ulong exec_time;
  ulong data_written;
  uint32 server_id;
  uint32 unmasked_server_id;
  uint16 flags;
  ulong slave_exec_mode;
  enum_event_cache_type event_cache_type;
  enum_event_logging_type event_logging_type;
  ha_checksum crc;
  ulong mts_group_idx;
  Relay_log_info *worker;
  ulonglong future_event_relay_log_pos;

#ifdef MYSQL_SERVER
  THD* thd;
  db_worker_hash_entry *mts_assigned_partitions[MAX_DBS_IN_EVENT_MTS];
  Log_event(enum_event_cache_type cache_type_arg= EVENT_INVALID_CACHE,
            enum_event_logging_type logging_type_arg= EVENT_INVALID_LOGGING);
  Log_event(THD* thd_arg, uint16 flags_arg,
            enum_event_cache_type cache_type_arg,
            enum_event_logging_type logging_type_arg);
  static Log_event* read_log_event(IO_CACHE* file,
                                   mysql_mutex_t* log_lock,
                                   const Format_description_log_event
                                   *description_event,
                                   my_bool crc_check);
  static int read_log_event(IO_CACHE* file, String* packet,
                            mysql_mutex_t* log_lock, uint8 checksum_alg_arg);

  static void init_show_field_list(List<Item>* field_list);
#ifdef HAVE_REPLICATION
  int net_send(Protocol *protocol, const char* log_name, my_off_t pos);

  virtual int pack_info(Protocol *protocol);

#endif /* HAVE_REPLICATION */
  virtual const char* get_db()
  {
    return thd ? thd->db : 0;
  }
#else // ifdef MYSQL_SERVER
  Log_event(enum_event_cache_type cache_type_arg= EVENT_INVALID_CACHE,
            enum_event_logging_type logging_type_arg= EVENT_INVALID_LOGGING)
  : temp_buf(0), event_cache_type(cache_type_arg),
    event_logging_type(logging_type_arg)
  { }
    /* avoid having to link mysqlbinlog against libpthread */
  static Log_event* read_log_event(IO_CACHE* file,
                                   const Format_description_log_event
                                   *description_event, my_bool crc_check);
  /* print*() functions are used by mysqlbinlog */
  virtual void print(FILE* file, PRINT_EVENT_INFO* print_event_info) = 0;
  void print_timestamp(IO_CACHE* file, time_t* ts);
  void print_header(IO_CACHE* file, PRINT_EVENT_INFO* print_event_info,
                    bool is_more);
  void print_base64(IO_CACHE* file, PRINT_EVENT_INFO* print_event_info,
                    bool is_more);
#endif // ifdef MYSQL_SERVER ... else
  uint8 checksum_alg;

  static void *operator new(size_t size)
  {
    return (void*) my_malloc((uint)size, MYF(MY_WME|MY_FAE));
  }

  static void operator delete(void *ptr, size_t)
  {
    my_free(ptr);
  }

  static void *operator new(size_t, void* ptr) { return ptr; }
  static void operator delete(void*, void*) { }
  bool wrapper_my_b_safe_write(IO_CACHE* file, const uchar* buf,
                               ulong data_length);

#ifdef MYSQL_SERVER
  bool write_header(IO_CACHE* file, ulong data_length);
  bool write_footer(IO_CACHE* file);
  my_bool need_checksum();

  virtual bool write(IO_CACHE* file)
  {
    return(write_header(file, get_data_size()) ||
          write_data_header(file) ||
          write_data_body(file) ||
          write_footer(file));
  }
  virtual bool write_data_header(IO_CACHE* file)
  { return 0; }
  virtual bool write_data_body(IO_CACHE* file __attribute__((unused)))
  { return 0; }
  inline time_t get_time()
  {
    if (!when.tv_sec && !when.tv_usec) /* Not previously initialized */
    {
      THD *tmp_thd= thd ? thd : current_thd;
      if (tmp_thd)
        when= tmp_thd->start_time;
      else
        my_micro_time_to_timeval(my_micro_time(), &when);
    }
    return (time_t) when.tv_sec;
  }
#endif
  virtual Log_event_type get_type_code() = 0;
  virtual bool is_valid() const = 0;
  void set_artificial_event() { flags |= LOG_EVENT_ARTIFICIAL_F; }
  void set_relay_log_event() { flags |= LOG_EVENT_RELAY_LOG_F; }
  bool is_artificial_event() const { return flags & LOG_EVENT_ARTIFICIAL_F; }
  bool is_relay_log_event() const { return flags & LOG_EVENT_RELAY_LOG_F; }
  bool is_ignorable_event() const { return flags & LOG_EVENT_IGNORABLE_F; }
  bool is_no_filter_event() const { return flags & LOG_EVENT_NO_FILTER_F; }
  inline bool is_using_trans_cache() const
  {
    return (event_cache_type == EVENT_TRANSACTIONAL_CACHE);
  }
  inline bool is_using_stmt_cache() const
  {
    return(event_cache_type == EVENT_STMT_CACHE);
  }
  inline bool is_using_immediate_logging() const
  {
    return(event_logging_type == EVENT_IMMEDIATE_LOGGING);
  }
  Log_event(const char* buf, const Format_description_log_event
            *description_event);
  virtual ∼Log_event() { free_temp_buf();}
  void register_temp_buf(char* buf) { temp_buf = buf; }
  void free_temp_buf()
  {
    if (temp_buf)
    {
      my_free(temp_buf);
      temp_buf = 0;
    }
  }
  virtual int get_data_size() { return 0;}
  static Log_event* read_log_event(const char* buf, uint event_len,
                                   const char **error,
                                   const Format_description_log_event
                                   *description_event, my_bool crc_check);
  static const char* get_type_str(Log_event_type type);
  const char* get_type_str();

#if defined(MYSQL_SERVER) && defined(HAVE_REPLICATION)

private:

  enum enum_mts_event_exec_mode
  {
    EVENT_EXEC_PARALLEL,
    EVENT_EXEC_ASYNC,
    EVENT_EXEC_SYNC,
    EVENT_EXEC_CAN_NOT
  };

  bool is_mts_sequential_exec()
  {
    return
      get_type_code() == START_EVENT_V3          ||
      get_type_code() == STOP_EVENT              ||
      get_type_code() == ROTATE_EVENT            ||
      get_type_code() == LOAD_EVENT              ||
      get_type_code() == SLAVE_EVENT             ||
      get_type_code() == CREATE_FILE_EVENT       ||
      get_type_code() == DELETE_FILE_EVENT       ||
      get_type_code() == NEW_LOAD_EVENT          ||
      get_type_code() == EXEC_LOAD_EVENT         ||
      get_type_code() == FORMAT_DESCRIPTION_EVENT||

      get_type_code() == INCIDENT_EVENT;
  }

  enum enum_mts_event_exec_mode get_mts_execution_mode(ulong slave_server_id,
                                                   bool mts_in_group)
  {
    if ((get_type_code() == FORMAT_DESCRIPTION_EVENT &&
         ((server_id == (uint32) ::server_id) || (log_pos == 0))) ||
        (get_type_code() == ROTATE_EVENT &&
         ((server_id == (uint32) ::server_id) ||
          (log_pos == 0    /* very first fake Rotate (R_f) */
           && mts_in_group /* ignored event turned into R_f at slave stop */))))
      return EVENT_EXEC_ASYNC;
    else if (is_mts_sequential_exec())
      return EVENT_EXEC_SYNC;
    else
      return EVENT_EXEC_PARALLEL;
  }

  Slave_worker *get_slave_worker(Relay_log_info *rli);

  virtual List<char>* get_mts_dbs(MEM_ROOT *mem_root)
  {
    List<char> *res= new List<char>;
    res->push_back(strdup_root(mem_root, get_db()));
    return res;
  }

  virtual void set_mts_isolate_group()
  {
    DBUG_ASSERT(ends_group() ||
                get_type_code() == QUERY_EVENT ||
                get_type_code() == EXEC_LOAD_EVENT ||
                get_type_code() == EXECUTE_LOAD_QUERY_EVENT);
    flags |= LOG_EVENT_MTS_ISOLATE_F;
  }

public:

  bool contains_partition_info(bool);
  virtual uint8 mts_number_dbs() { return 1; }
  bool is_mts_group_isolated() { return flags & LOG_EVENT_MTS_ISOLATE_F; }
  virtual bool starts_group() { return FALSE; }
  virtual bool ends_group()   { return FALSE; }
  int apply_event(Relay_log_info *rli);
  int update_pos(Relay_log_info *rli)
  {
    return do_update_pos(rli);
  }
  enum_skip_reason shall_skip(Relay_log_info *rli)
  {
    return do_shall_skip(rli);
  }
  virtual int do_apply_event(Relay_log_info const *rli)
  {
    return 0;                /* Default implementation does nothing */
  }
  virtual int do_apply_event_worker(Slave_worker *w);

protected:

  enum_skip_reason continue_group(Relay_log_info *rli);
  virtual int do_update_pos(Relay_log_info *rli);
  virtual enum_skip_reason do_shall_skip(Relay_log_info *rli);
#endif
};

首先,请注意这段代码中的条件编译指令。这些是必要的,因为 MySQL 源代码的其他部分使用了log-event类。例如,mysqlbinlog客户端应用将针对这段代码进行编译。因此,有些部分被标记为专门在服务器中使用(MYSQL_SERVER),有些部分被标记为如果不使用复制就要删除(HAVE_REPLICATION)。 3 编译mysqlbinlog等外部代码时,使用不符合这些指令的部分(#else)。

该类包括许多帮助函数,例如获取事件数据库的方法、创建和销毁实例(新建、删除)的方法等等。一些更有趣的助手方法是write_*()方法。这些用于将事件(也称为序列化)写入二进制日志,与负责将有效负载嵌入事件的pack_info()方法结合使用。

同样,也有从二进制日志文件中读取事件的方法。注意read_event()方法的重复出现。其中几个位于这个类中。这是因为,根据您读取事件的位置或方式,您可能需要不同形式的此方法。因此,此方法是重载的,调用哪个取决于参数的上下文。我们最感兴趣的是从二进制日志中读取后返回事件实例的版本。在本节的稍后部分,我们将看到这一点。

最后,服务器使用do_apply_event() 来执行事件。每种日志事件类型都有该方法的特定实现,因此需要使它和许多其他方法成为虚拟的。

请注意其他被标记为虚拟的方法。当您检查不同的日志事件时,您会看到这些事件的类实现要小得多,并且通常只包括那些在这里标记为虚拟的方法。

在下一节中,我将描述一些类型的日志事件以及每种日志事件的功能。接下来简单看一下日志事件的执行路径。

日志事件的类型

为从主设备向从设备复制命令定义了 30 多个日志事件。如果您检查二进制日志或中继日志,很可能无法找到所有这些事件。事实上,有些类是为特定格式(SBR 或 RBR)定义的,除非您使用混合二进制日志格式,否则您不会找到这两类事件。

SBR 赛事的基本看点是Query_log_event 。这包含用户在主服务器上发出的 SQL 语句。当应用此事件时,SQL 语句按编写的那样执行。查询使用的任何变量——包括用户定义的变量、随机数或与时间相关的值——在Query_log_event之前被写入二进制日志,因此在查询本身之前在从机上执行。这就是复制在主服务器和从服务器上保持时间戳、随机数、增量列和类似的特殊事件或代码相同的方式。例如,在随机数的情况下,主节点上使用的种子被传输到从节点,导致RAND函数在从节点上返回与主节点相同的值。

RBR 的情况有所不同。这些使用一个基类Rows_log_event 来封装所有 RBR 事件的基本功能。因为 RBR 事件只包含行图像或在主服务器上应用查询的结果,所以每种类型都有唯一的日志事件。有针对插入、更新(有一个 before image 事件)和删除的日志事件。

特别值得注意的是Incident_log_event 。此事件存储主服务器上遇到的任何异常情况或状态,如生成事件的错误、另一个错误或警告,从轻微到严重不等。创建此事件是为了在主服务器上发生异常情况时,从服务器在应用(执行)该事件时,可以决定该事件是否严重到需要停止复制。对复制进行故障排除时,请检查事件事件以了解更多详细信息。

表 8-3 显示了更频繁遇到的日志事件列表。我包括了对每个事件的描述,以及在这些事件可能出现的地方使用的二进制日志格式。

表 8-3。重要日志事件类型

事件类型 描述 格式
Ignorable_log_event 忽略从属服务器上的事件,并且不将其写入中继日志以供执行。 全部
Incident_log_event 记录异常事件,如主服务器上发生的错误或警告。会导致从属服务器停止复制。排除复制故障时,请检查这些事件。 全部
Intvar_log_event 在 Query_log_event 之前创建,以包含查询使用的任何变量,如 LAST_INSERT_ID。 小触须(同 small bristles)
User_var_log_event 在 Query_log_event 之前创建,以包含查询定义和使用的任何用户变量。 小触须(同 small bristles)
Query_log_event 用户在主服务器上发出的查询。 小触须(同 small bristles)
Rand_log_event 从主服务器计算或传输一个随机种子。 小触须(同 small bristles)
Rotate_log_event 启动原木旋转。 全部
Rows_log_event RBR 事件的基类。 规则的推理
Write_rows_log_event 包括表的一个或多个插入或更新行。 规则的推理
Update_rows_log_event 包括一个或多个行更新。 规则的推理
Delete_rows_log_event 包括要删除的行图像。 规则的推理
Table_map_log_event 信息性事件,包含有关当前正在被前面的事件修改或操作的表的信息。这些信息包括数据库名、表名和列定义。 规则的推理
Unknown_log_event 用于捕获未知事件或在以后版本中定义的事件的虚拟事件。 全部

在接下来的部分中,我们将使用一个新的日志事件,该事件旨在将信息嵌入到二进制日志中,以便进行诊断。这个事件与从机无关,所以我们将创建一个类似于Ignorable_log_event的事件。

现在我们已经看到了一些更重要的事件类型,让我们来看看事件是如何在从属服务器上执行的。

日志事件的执行

通过首先从中继日志中读取日志事件并实例化类实例,从模块执行日志事件。这就是日志事件类型至关重要的地方。从机必须知道它正在读取什么类型的日志事件。当我们探索扩展复制时,您将会看到,每个日志事件都被分配了一个特殊的枚举(代码)。这就是从属服务器知道实例化哪个类的方法。读取日志事件并实例化它们的代码位于Log_event::next_event()方法 中的 rpl_slave.cc 中。

该方法包含一个无限循环,其工作是读取事件、检查先决条件、错误和管理中继日志的特殊命令(如 purge 和 rotate)以及实例化事件。

事件通过Log_event::read_log_event()方法实例化。该方法还将在创建事件实例之前进行错误检查(如校验和验证)。该代码位于log_event.cc中第 1386 行附近。如果您打开该文件并向下滚动该方法,您将看到用于实例化事件的 switch 语句。清单 8-6 显示了突出 switch 语句的方法摘录

清单 8-6。Log _ event::read _ Log _ event()方法

Log_event* Log_event::read_log_event(const char* buf, uint event_len,
                                    const char **error,
                                     const Format_description_log_event *description_event,
                                     my_bool crc_check)
{
  Log_event* ev;
...

    if (alg != BINLOG_CHECKSUM_ALG_UNDEF &&
        (event_type == FORMAT_DESCRIPTION_EVENT ||
         alg != BINLOG_CHECKSUM_ALG_OFF))
      event_len= event_len - BINLOG_CHECKSUM_LEN;

    switch(event_type) {
    case QUERY_EVENT:
      ev  = new Query_log_event(buf, event_len, description_event,
                                QUERY_EVENT);
      break;
    case LOAD_EVENT:
      ev = new Load_log_event(buf, event_len, description_event);
      break;
    case NEW_LOAD_EVENT:
      ev = new Load_log_event(buf, event_len, description_event);
      break;
    case ROTATE_EVENT:
      ev = new Rotate_log_event(buf, event_len, description_event);
      break;
    case CREATE_FILE_EVENT:
      ev = new Create_file_log_event(buf, event_len, description_event);
      break;
    case APPEND_BLOCK_EVENT:
      ev = new Append_block_log_event(buf, event_len, description_event);
      break;
    case DELETE_FILE_EVENT:
      ev = new Delete_file_log_event(buf, event_len, description_event);
      break;
    case EXEC_LOAD_EVENT:
      ev = new Execute_load_log_event(buf, event_len, description_event);
      break;
    case START_EVENT_V3: /* this is sent only by MySQL <=4.x */
      ev = new Start_log_event_v3(buf, description_event);
      break;
    case STOP_EVENT:
      ev = new Stop_log_event(buf, description_event);
      break;
    case INTVAR_EVENT:
      ev = new Intvar_log_event(buf, description_event);
      break;
    case XID_EVENT:
      ev = new Xid_log_event(buf, description_event);
      break;
    case RAND_EVENT:
      ev = new Rand_log_event(buf, description_event);
      break;
    case USER_VAR_EVENT:
      ev = new User_var_log_event(buf, description_event);
      break;
    case FORMAT_DESCRIPTION_EVENT:
      ev = new Format_description_log_event(buf, event_len, description_event);
      break;
#if defined(HAVE_REPLICATION)
    case PRE_GA_WRITE_ROWS_EVENT:
      ev = new Write_rows_log_event_old(buf, event_len, description_event);
      break;
    case PRE_GA_UPDATE_ROWS_EVENT:
      ev = new Update_rows_log_event_old(buf, event_len, description_event);
      break;
    case PRE_GA_DELETE_ROWS_EVENT:
      ev = new Delete_rows_log_event_old(buf, event_len, description_event);
      break;
    case WRITE_ROWS_EVENT_V1:
      ev = new Write_rows_log_event(buf, event_len, description_event);
      break;
    case UPDATE_ROWS_EVENT_V1:
      ev = new Update_rows_log_event(buf, event_len, description_event);
      break;
    case DELETE_ROWS_EVENT_V1:
      ev = new Delete_rows_log_event(buf, event_len, description_event);
      break;
    case TABLE_MAP_EVENT:
      ev = new Table_map_log_event(buf, event_len, description_event);
      break;
#endif
    case BEGIN_LOAD_QUERY_EVENT:
      ev = new Begin_load_query_log_event(buf, event_len, description_event);
      break;
    case EXECUTE_LOAD_QUERY_EVENT:
      ev= new Execute_load_query_log_event(buf, event_len, description_event);
      break;
    case INCIDENT_EVENT:
      ev = new Incident_log_event(buf, event_len, description_event);
      break;
    case ROWS_QUERY_LOG_EVENT:
      ev= new Rows_query_log_event(buf, event_len, description_event);
      break;
    case SLAVE_CONNECT_LOG_EVENT:
      ev= new Slave_connect_log_event(buf, event_len, description_event);
      break;
    case GTID_LOG_EVENT:
    case ANONYMOUS_GTID_LOG_EVENT:
      ev= new Gtid_log_event(buf, event_len, description_event);
      break;
    case PREVIOUS_GTIDS_LOG_EVENT:
      ev= new Previous_gtids_log_event(buf, event_len, description_event);
      break;
#if defined(HAVE_REPLICATION)
    case WRITE_ROWS_EVENT:
      ev = new Write_rows_log_event(buf, event_len, description_event);
      break;
    case UPDATE_ROWS_EVENT:
      ev = new Update_rows_log_event(buf, event_len, description_event);
      break;
    case DELETE_ROWS_EVENT:
      ev = new Delete_rows_log_event(buf, event_len, description_event);
      break;
#endif
    default:
      /*
        Create an object of Ignorable_log_event for unrecognized sub-class.
        So that SLAVE SQL THREAD will only update the position and continue.
      */
      if (uint2korr(buf + FLAGS_OFFSET) & LOG_EVENT_IGNORABLE_F)
      {
        ev= new Ignorable_log_event(buf, description_event);
      }
      else
      {
        DBUG_PRINT("error",("Unknown event code: %d",
                            (int) buf[EVENT_TYPE_OFFSET]));
        ev= NULL;
      }
      break;
    }
  }
...
  DBUG_RETURN(ev);
}

一旦事件被读取,从属代码调用Log_event::apply_event()方法,该方法又调用 log-event-class 实例的*_log_event::do_apply_event()。此方法负责执行事件。如前一节所述,所有日志事件类都有一个*_log_event::apply_event()方法。下面的清单 8-7 显示了该事件的一个示例实现。这显示了执行Intvar_log_event时执行的代码。

清单 8-7。 示例 do_apply_event()方法

/*
  Intvar_log_event::do_apply_event()
*/

int Intvar_log_event::do_apply_event(Relay_log_info const *rli)
{
  /*
    We are now in a statement until the associated query log event has
    been processed.
   */
  const_cast<Relay_log_info*>(rli)->set_flag(Relay_log_info::IN_STMT);

  if (rli->deferred_events_collecting)
    return rli->deferred_events->add(this);

  switch (type) {
  case LAST_INSERT_ID_EVENT:
    thd->stmt_depends_on_first_successful_insert_id_in_prev_stmt= 1;
    thd->first_successful_insert_id_in_prev_stmt= val;
    break;
  case INSERT_ID_EVENT:
    thd->force_one_auto_inc_interval(val);
    break;
  }
  return 0;
}

请注意代码是如何设计来操作自动递增值的属性的。这就是从机如何确保为执行的查询正确设置自动增量值。这主要用于 SBR,因为 RBR 包含了主数据行的结果图像。

既然我们已经了解了日志事件是如何在从属服务器上执行的,现在是卷起袖子,再次深入源代码进行修改复制源代码的实验的好时机。接下来的部分将向您展示一些基本的方法,您可以通过这些方法来扩展复制以满足您独特的高可用性需求。

扩展复制

本节提供了一些示例项目,您可以使用它们来研究 MySQL 复制源代码。虽然有些人可能认为这些示例本质上是学术性的,但是您可能会发现它们很有用,可以作为您环境中实际应用的模板。

image 注意修改复制代码应该非常认真。它是最复杂的子系统之一,也是最健壮和可靠的子系统之一。确保你的修改是合理的。如果您的代码带来了副作用,或者更糟糕的是,导致服务器崩溃,那么复制的数据可能会过期或损坏。在任何生产环境中尝试使用它们之前,最好仔细计划您的修改并广泛测试它们。

如果上面的警告让你害怕,那是好的,因为复制子系统设计得很好,具有长期稳定的历史。显然,如果您想要使用复制或者依赖复制来实现备用、备份或高可用性,那么谨慎是合理的。

不要让这阻止你探索这些例子。事实上,有时候了解一件事的最好方法就是先打破它。如果你已经研究过前面章节的例子,你可能已经体验过了。

既然已经陈述了必要的告诫,让我们修改一些代码吧!

全局从站停止命令

我将从一个不太复杂的扩展开始。假设您有一个包含许多从属服务器的复制拓扑,这时您需要停止复制。在这种情况下,你必须访问每个从机并发出STOP SLAVE命令 。在主服务器上使用一个命令来告诉所有从服务器停止复制不是更容易吗?

这并不是说没有办法做到这一点——有几种方法。您可以锁定主服务器上的所有表,从而停止事件流。您也可以关闭二进制日志,但是这可能会导致从属服务器抛出错误。

然而,拜访每个奴隶可能还不够。假设有一个主动主服务器不断从客户端接收更新。还要考虑到不可能同时向每个从机发出停止从机命令。在访问每个从服务器之前,您仍然需要停止主服务器上的事件复制。

如果主设备上有一个命令同时被复制到每个从设备,这将确保所有从设备在复制过程中的同一点停止。

因此,如果有一个 SQL 命令——比方说,STOP ALL SLAVES——将STOP SLAVE命令复制到所有从站,那么您可以确保所有从站同时停止。让我们看看如何着手制定这样的命令。

代码修改

这个扩展需要修改解析器,并为 sql_parse.cc 中的大开关添加 case 语句。

表 8-4 。停止所有从属命令的文件已更改

文件 变更摘要
sql/lex.h 为新命令添加新符号
sql/sql_cmd.h 添加新的枚举
sql/sql_yacc.yy 为新命令添加新的解析器规则
sql/sql_parse.cc 为大开关添加新案例,以发送从属停止命令

现在我们知道了哪些文件需要更改,让我们开始修改。首先,打开sql/lex.h文件并添加以下代码。我们正在为命令添加新符号。该文件包含按字母顺序存储的符号数组。因此,我们将在第 523 行附近添加SLAVES符号。清单 8-8 描述了上下文中的修改。

清单 8-8。 向 sql/lex.h 添加奴隶符号

{ "SIGNED",            SYM(SIGNED_SYM)},
{ "SIMPLE",            SYM(SIMPLE_SYM)},
{ "SLAVE",            SYM(SLAVE)},
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add SLAVES keyword */
{ "SLAVES",           SYM(SLAVES)},
/* END CAB MODIFICATION */
{ "SLOW",             SYM(SLOW)},
{ "SNAPSHOT",         SYM(SNAPSHOT_SYM)},
{ "SMALLINT",          SYM(SMALLINT)},

接下来,我们需要修改 sql_cmd.h 文件,为大交换机添加一个新的枚举。打开 sql_cmd.h 文件,找到文件顶部附近的 enum enum_sql_command 定义。清单 8-9 显示了为新命令添加新枚举的代码。

清单 8-9。 为停止所有奴隶命令添加枚举


SQLCOM_FLUSH, SQLCOM_KILL, SQLCOM_ANALYZE,
SQLCOM_ROLLBACK, SQLCOM_ROLLBACK_TO_SAVEPOINT,
SQLCOM_COMMIT, SQLCOM_SAVEPOINT, SQLCOM_RELEASE_SAVEPOINT,
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add SQLCOM_STOP_SLAVES enum */
SQLCOM_SLAVE_START, SQLCOM_SLAVE_STOP, SQLCOM_STOP_SLAVES,
/* END CAB MODIFICATION */
SQLCOM_BEGIN, SQLCOM_CHANGE_MASTER,
SQLCOM_RENAME_TABLE,
SQLCOM_RESET, SQLCOM_PURGE, SQLCOM_PURGE_BEFORE, SQLCOM_SHOW_BINLOGS,

接下来,我们需要添加一个新令牌用于新规则。同样,令牌列表是按字母顺序排列的。打开 sql_yacc.yy 文件,找到定义新标记的部分。在这种情况下,我们需要为新命令添加一个令牌定义。我们将把它命名为奴隶。清单 8-10 显示了要添加的上下文中的代码。您可以在第 1497 行附近找到此代码。

清单 8-10。 添加代币

%token  SIGNED_SYM
%token  SIMPLE_SYM                    /* SQL-2003-N */
%token  SLAVE
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add SLAVES token */
%token  SLAVES
/* END CAB MODIFICATION */
%token  SLOW
%token  SMALLINT                      /* SQL-2003-R */
%token  SNAPSHOT_SYM

接下来,修改%type 定义所在的部分。我们需要将新的令牌添加到这个定义中。您可以在第 1813 行附近找到这一部分。清单 8-11 展示了上下文中的代码。

清单 8-11。 向无定义类型添加令牌

/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add stop to list of NONE types */
     repair analyze check start stop checksum
/* END CAB MODIFICATION */
      field_list field_list_item field_spec kill column_def key_def
      keycache_list keycache_list_or_parts assign_to_keycache
      assign_to_keycache_parts

...

我们快完成了。接下来,我们向命令列表中添加一个新的命令定义,这样解析器就可以将控制指向新的规则。同样,这个列表是按字母顺序排列的。您可以在第 2035 行附近找到要修改的位置。清单 8-12 显示了修改。请注意,我们向要评估的新规则添加了一个新的“or”条件映射。在这种情况下,我们将规则命名为 stop。

清单 8-12。 添加新的规则定义

      | show
      | slave
      | start
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add stop to list of statement targets */
     | stop
/* END CAB MODIFICATION */
      | truncate
      | uninstall
      | unlock

最后,我们将添加新规则来处理 STOP ALL SLAVES 命令。我将这段代码放在第 8027 行附近现有的 start 规则附近。清单 8-13 展示了新的规则。该规则只是将新的枚举保存到lex->sql_command属性中。这就是代码如何将规则的结果(以及命令的处理)映射到 big switch 到等于枚举值的情况。

清单 8-13。 向解析器添加停止所有奴隶规则

}
      ;

/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add rule for STOP ALL SLAVES command */
stop:
       STOP_SYM ALL SLAVES
       {
         LEX *lex= Lex;
         lex->sql_command= SQLCOM_STOP_SLAVES;
       }
       ;
/* END CAB MODIFICATION */

start:
        START_SYM TRANSACTION_SYM opt_start_transaction_option_list
        {

完成对 YACC 文件的更改后,我们可以为大开关添加一个新的案例,以确保命令一旦被解析器捕获,就被定向到代码,以将事件写入二进制日志。正常情况下,停止从命令不会被复制。我们的代码也需要覆盖这个限制。让我们添加 case 语句。打开 sql_parse.cc 文件,找到包含复制语句的部分。这是在 3054 号线附近。清单 8-14 显示了新的 case 语句。

清单 8-14。 增加了备自投大开关

mysql_mutex_unlock(&LOCK_active_mi);
  break;
}

/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add case statement for STOP ALL SLAVES command */
case SQLCOM_STOP_SLAVES:
{
  if (!lex->no_write_to_binlog)
    res= write_bin_log(thd, TRUE, "STOP SLAVE IO_THREAD", 20);
 break;
}
/* END CAB MODIFICATION */

#endif /* HAVE_REPLICATION */

case SQLCOM_RENAME_TABLE:

花点时间检查一下这段代码。第一条语句旨在检查服务器是否能够写入二进制日志。如果是这样,我们添加一个新的日志事件,传递给它一个 SQL 命令STOP SLAVE。注意,我们使用了特定版本的STOP SLAVE命令。在这种情况下,我们只停止 IO 线程。这有两个原因。首先,我们只需要停止 IO 线程来停止将事件复制到从属线程。这仍然允许从线程处理从主线程读取的任何事件(就像SQL线程所做的那样)。第二,STOP SLAVE命令更复杂,因为它停止两个线程,这样做时,它有几个关键部分需要被互斥体保护。在执行日志事件(日志事件本身就是STOP SLAVE)的过程中执行 STOP SLAVE 命令会导致与互斥体的冲突。因此,只停止 IO 线程是在从机上成功执行命令的唯一方法。

编译代码

要编译新代码,只需从源代码树的根执行 make。由于 cmake 文件中没有新文件或更改,我们只需要重新构建可执行文件。如果有错误,返回并修复它们,直到服务器代码编译成功。

示例执行

执行全局 slave-stop 命令需要一个至少有一个从属服务器的复制拓扑。为了演示该命令的工作情况,我将创建一个使用三个从设备的拓扑。

第一步是建立一个简单的复制拓扑。我使用 MySQL 实用程序命令来快速设置我的测试条件,而不是按照上面列出的所有步骤来设置主服务器和从服务器。

我首先为主服务器和每个从服务器分别克隆一个正在运行的服务器的实例。mysqlserverclone 实用程序旨在创建一个关闭的或运行中的从服务器的新实例。我们只需使用 server 选项连接到服务器,为新服务器(mysqld)传递任何选项,并定义新的数据目录、端口和 server_id。清单 8-15 显示了这些步骤的结果。

清单 8-15。 设置简单的复制拓扑

cbell@ubuntu:$ mysqlserverclone.py --basedir=/source/mysql-5.6 \
  --mysqld="--log-bin=mysql-bin"  --new-port=3310 –new-data=/source/temp_3310 \
  --new-id=100 --delete-data
# WARNING: Root password for new instance has not been set.
# Cloning the MySQL server located at /source/mysql-5.6.
# Creating new data directory...
# Configuring new instance...
# Locating mysql tools...
# Setting up empty database and mysql tables...
# Starting new instance of the server...
# Testing connection to new instance...
# Success!
# Connection Information:
#  -uroot --socket=/source/temp_3310/mysql.sock
#...done.

cbell@ubuntu:$ mysqlserverclone.py --basedir=/source/mysql-5.6 \
  --mysqld="--log-bin=mysql-bin --report-port=3311 --report-host=localhost" \
  --new-port=3311 –new-data=/source/temp_3311 \
  --new-id=101 --delete-data
# WARNING: Root password for new instance has not been set.
# Cloning the MySQL server located at /source/mysql-5.6.
# Creating new data directory...
# Configuring new instance...
# Locating mysql tools...
# Setting up empty database and mysql tables...
# Starting new instance of the server...
# Testing connection to new instance...
# Success!
# Connection Information:
#  -uroot --socket=/home/cbell/source/temp_3311/mysql.sock
#...done.

cbell@ubuntu:$ mysqlserverclone.py --basedir=/source/mysql-5.6 \
  --mysqld="--log-bin=mysql-bin --report-port=3312 --report-host=localhost" \
  --new-port=3312 –new-data=/source/temp_3312 \
  --new-id=102 --delete-data
# WARNING: Root password for new instance has not been set.
# Cloning the MySQL server located at /source/mysql-5.6.
# Creating new data directory...
# Configuring new instance...
# Locating mysql tools...
# Setting up empty database and mysql tables...
# Starting new instance of the server...
# Testing connection to new instance...
# Success!
# Connection Information:
#  -uroot --socket=/home/cbell/source/temp_3311/mysql.sock
#...done.

cbell@ubuntu:$ mysqlserverclone.py --basedir=/source/mysql-5.6 \
  --mysqld="--log-bin=mysql-bin --report-port=3313 --report-host=localhost" \
  --new-port=3313 –new-data=/source/temp_3313 \
  --new-id=103 --delete-data
# WARNING: Root password for new instance has not been set.
# Cloning the MySQL server located at /source/mysql-5.6.
# Creating new data directory...
# Configuring new instance...
# Locating mysql tools...
# Setting up empty database and mysql tables...
# Starting new instance of the server...
# Testing connection to new instance...
# Success!
# Connection Information:
#  -uroot --socket=/home/cbell/source/temp_3311/mysql.sock
#...done.

一旦我们运行了主设备和从设备,我们就可以在主设备和每个从设备之间建立复制。mysqlreplicate 实用程序使从设备到主设备的连接只需一步。清单 8-16 显示了结果。请注意,该实用程序只需要一个到主机的连接和一个到从机的连接。

清单 8-16。 设置复制

cbell@ubuntu:$ mysqlreplicate.py --master=root@localhost:3310 \
  --slave=root@localhost:3311
# master on localhost: ... connected.
# slave on localhost: ... connected.
# Checking for binary logging on master...
# Setting up replication...
# ...done.

cbell@ubuntu:$ mysqlreplicate.py --master=root@localhost:3310 \
  --slave=root@localhost:3312
# master on localhost: ... connected.
# slave on localhost: ... connected.
# Checking for binary logging on master...
# Setting up replication...
# ...done.

cbell@ubuntu:$ mysqlreplicate.py --master=root@localhost:3310 \
  --slave=root@localhost:3313
# master on localhost: ... connected.
# slave on localhost: ... connected.
# Checking for binary logging on master...
# Setting up replication...
# ...done.

什么是 MYSQL 实用程序?

MySQL 工具是 MySQL 工作台工具的子项目。MySQL Utilities 包含许多有用的命令行工具,用于管理 MySQL 服务器,重点是复制。

当您下载 Workbench 时,您还会获得 MySQL 实用程序。您可以通过 Workbench 中的插件访问这些实用程序。也可以直接从 Launchpad 下载 MySQL 实用程序。

MySQL 工作台下载:http://dev.mysql.com/downloads/workbench/5.2.html

从 Launchpad 下载 MySQL 实用程序:https://launchpad.net/mysql-utilities

有关 MySQL 实用程序的更多信息,请参见联机文档:

http://dev.mysql.com/doc/workbench/en/mysql-utilities.html

让我们回顾一下到目前为止的设置。上面的命令允许我从源代码树(由--basedir指定)中创建一个服务器的运行实例,向它传递参数,比如要使用的端口及其数据目录的位置。mysqlserverclone 实用程序将为我做所有的工作,然后告诉我如何连接到服务器。我这样做了两次:一次用于主设备,一次用于从设备,使用不同的端口和数据目录。然后,我使用自动化复制设置实用程序mysqlreplicate,在主设备和三个从设备之间设置复制。注意,我在--mysqld选项中为每个奴隶设置了--report-port--report-host。此选项允许您指定服务器启动的选项。

正如你所看到的,这是非常容易和非常快速的设置。您甚至可以将这些命令放在脚本中,以便您可以在任何时候创建测试拓扑。

现在我们有了测试拓扑,让我们检查每个从机 IO 线程的状态,然后发出命令并再次检查状态。第一步是查看附属于主设备的从设备列表。为此,我使用了mysqlrplshow命令。清单 8-17 显示了该命令的输出;它打印出了我们的拓扑结构的漂亮图形。在此过程中,我们还显示了每个从机的从机状态,以确保每个从机都在主动复制主机的数据。为了简洁起见,我提供了输出的摘录。

清单 8-17。 检查拓扑

cbell@ubuntu:$ mysqlrplshow.py --master=root@localhost:3310
# master on localhost: ... connected.
# Finding slaves for master: localhost:3310

# Replication Topology Graph
localhost:3310 (MASTER)
   |
   +−−- localhost:3311 - (SLAVE)
   |
   +−−- localhost:3312 - (SLAVE)
   |
   +−−- localhost:3313 - (SLAVE)

cbell@ubuntu: $ mysql -uroot -h 127.0.0.1 --port=3311
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 5
Server version: 5.6.6-m9-log Source distribution

Copyright (c) 2000, 2011, 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> SHOW SLAVE STATUS \G
*************************** 1\. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: localhost
                  Master_User: rpl
                  Master_Port: 3310
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000001
          Read_Master_Log_Pos: 325
               Relay_Log_File: clone-relay-bin.000002
                Relay_Log_Pos: 283
        Relay_Master_Log_File: mysql-bin.000001
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
...
1 row in set (0.00 sec)

cbell@ubuntu: $ mysql -uroot -h 127.0.0.1 --port=3312
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 3
Server version: 5.6.6-m9-log Source distribution

Copyright (c) 2000, 2011, 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> SHOW SLAVE STATUS \G
*************************** 1\. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: localhost
                  Master_User: rpl
                  Master_Port: 3310
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000001
          Read_Master_Log_Pos: 325
               Relay_Log_File: clone-relay-bin.000002
                Relay_Log_Pos: 283
        Relay_Master_Log_File: mysql-bin.000001
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
...
1 row in set (0.00 sec)

cbell@ubuntu: $ mysql -uroot -h 127.0.0.1 --port=3313
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.6.6-m9-log Source distribution

Copyright (c) 2000, 2011, 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> SHOW SLAVE STATUS \G
*************************** 1\. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: localhost
                  Master_User: rpl
                  Master_Port: 3310
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000001
          Read_Master_Log_Pos: 325
               Relay_Log_File: clone-relay-bin.000002
                Relay_Log_Pos: 283
        Relay_Master_Log_File: mysql-bin.000001
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
...
1 row in set (0.00 sec)

既然我们已经建立了新的拓扑并验证了一切正常,我们可以测试新的 STOP ALL SLAVES 命令了。清单 8-18 显示了结果。

清单 8-18。 演示停止所有奴隶的命令

cbell@ubuntu$ mysql -uroot -h 127.0.0.1 --port=3310
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 10
Server version: 5.6.6-m9-log Source distribution

Copyright (c) 2000, 2011, 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> STOP ALL SLAVES;
Query OK, 0 rows affected (0.00 sec)

你可能在想,“是这样吗?”对主人来说,是。什么也没发生,因为(回想一下大开关的代码)我们只将命令写入二进制日志。在主服务器上不做任何其他事情,它会不间断地继续执行。

当然,想要的效果是让奴隶停下来。让我们检查从设备上的从设备状态,看看是否确实发生了这种情况。记住,我们正在停止 IO 线程,所以我们应该在 slave-status 输出中寻找它。清单 8-19 显示了每个从状态的输出,为简洁起见,摘录如下。请注意,在每种情况下,从属 IO 线程确实已经停止(Slave_IO_Running = No)。

清单 8-19。 结果对奴隶发出停止所有奴隶的命令

cbell@ubuntu: $ mysql -uroot -h 127.0.0.1 --port=3311
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 6
Server version: 5.6.6-m9-log Source distribution

Copyright (c) 2000, 2011, 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> SHOW SLAVE STATUS \G
*************************** 1\. row ***************************
               Slave_IO_State:
                  Master_Host: localhost
                  Master_User: rpl
                  Master_Port: 3310
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000001
          Read_Master_Log_Pos: 408
               Relay_Log_File: clone-relay-bin.000002
                Relay_Log_Pos: 366
        Relay_Master_Log_File: mysql-bin.000001
             Slave_IO_Running: No
            Slave_SQL_Running: Yes
...
1 row in set (0.00 sec)

cbell@ubuntu: $ mysql -uroot -h 127.0.0.1 --port=3312
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 6
Server version: 5.6.6-m9-log Source distribution

Copyright (c) 2000, 2011, 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> SHOW SLAVE STATUS \G
*************************** 1\. row ***************************
               Slave_IO_State:
                  Master_Host: localhost
                  Master_User: rpl
                  Master_Port: 3310
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000001
          Read_Master_Log_Pos: 408
               Relay_Log_File: clone-relay-bin.000002
                Relay_Log_Pos: 366
        Relay_Master_Log_File: mysql-bin.000001
             Slave_IO_Running: No
            Slave_SQL_Running: Yes
...
1 row in set (0.00 sec)

cbell@ubuntu: $ mysql -uroot -h 127.0.0.1 --port=3313
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 6
Server version: 5.6.6-m9-log Source distribution

Copyright (c) 2000, 2011, 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> SHOW SLAVE STATUS \G
*************************** 1\. row ***************************
               Slave_IO_State:
                  Master_Host: localhost
                  Master_User: rpl
                  Master_Port: 3310
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000001
          Read_Master_Log_Pos: 408
               Relay_Log_File: clone-relay-bin.000002
                Relay_Log_Pos: 366
        Relay_Master_Log_File: mysql-bin.000001
             Slave_IO_Running: No
            Slave_SQL_Running: Yes
...
1 row in set (0.00 sec)

此示例演示了如何创建一个独特的复制命令,该命令可以在主服务器上执行,并发送到所有从服务器,以停止复制事件流。如您所见,这个扩展很容易添加,并显示了二进制日志事件的强大功能。如果你发挥你的想象力,你可以想到其他有用的命令,你可能想发送给你所有的奴隶。

在下一个例子中,难度水平显著增加。我将向您展示如何创建一个新的日志事件,在二进制日志中记录信息。在这种情况下,我们创建一个新的日志事件,记录从设备何时连接到主设备。如果您需要确定何时或者是否有新的从站加入拓扑,这可能会很有用。

从机连接日志

精明的管理员知道定期检查复制拓扑是否有错误。这可以包括检查SHOW SLAVE STATUS 的结果,以及检查错误、从延迟和类似事件的值。遇到错误或必须诊断复制问题的管理员缺少一个关键数据点。在大型拓扑中,如果有许多从属服务器为了向外扩展而联机,则复制可能会脱机。这个过程可能一周发生几次,在某些情况下,可能一天发生多次。小型甚至中型复制拓扑的管理员可能会在从属服务器连接和断开时保留一个日志(至少在心里),但是对于大型复制拓扑来说,这可能是不可能的。在这种情况下,可能无法确定从设备何时连接到主设备,只能确定从设备是否连接到主设备。

在诊断和修复复制的情况下,了解从机连接的时间有助于将该事件的时间映射到同时发生的其他事件。也许这些事件是有关联的。如果你不知道从机何时连接到主机,你可能永远也不会知道。

在本节中,我们将创建一个新的日志事件,记录从设备何时连接到主设备。这将作为永久条目写入二进制日志。虽然它也被发送到从机,但我们使该事件成为一个可忽略的事件,允许从机跳过该事件。因此,如果使用了二进制日志记录,从属服务器将不会采取任何行动,不会将事件写入其中继日志,也不会将事件写入其二进制日志。

让我们从查看需要修改的源文件开始。虽然事件本身在设计和目的上非常简单,但是创建新事件所需的代码非常广泛,并且分散在整个复制源文件中。正如您将看到的,在主服务器和从服务器中有许多地方都处理了事件。

代码修改

此扩展将需要修改许多复制源文件。表 8-5 列出了需要修改的文件。所有这些文件都位于/sql 文件夹中。

表 8-5。从机连接事件的文件已更改

文件 变更摘要
日志 _ 事件. h 新的Slave_connect_log_event类声明
log_event.cc 新的Slave_connect_log_event类定义
binlog . h .- 新的MYSQL_BIN_LOG::write_slave_connect()方法声明
binlog.cc 新的MYSQL_BIN_LOG::write_slave_connect()定义
rpl_master.cc 修改register_slave()以调用write_slave_connect()
rpl_rli(消歧义) 添加对Slave_connect_log_event类的引用
rpl_rli.cc 添加删除Slave_connect_log_event类的代码以丢弃它
rpl_rli_pdb.cc 添加删除Slave_connect_log_event类的代码以丢弃它
rpl_slave.cc 添加删除Slave_connect_log_event类的代码以丢弃它
sql_binlog.cc 添加删除Slave_connect_log_event类的代码以丢弃它

乍看之下,这份修改清单似乎令人望而生畏。幸运的是,大多数源文件只需要很小的改动。主要工作是创建新的 log-event 类,并将其连接到主服务器的register_slave()方法。为了使它更容易,并保持与服务器代码(服务器的主要源文件)相同的一般隔离,不要直接创建日志事件;而是使用在MYSQL_BIN_LOG类中定义的方法。我将创建一个新的方法来做到这一点。

源代码中有几个地方我们需要丢弃新事件。我们镜像了Rows_query_log_event动作,这样我们就可以确保覆盖所有需要忽略和删除事件的情况。

现在我们知道了哪些文件需要更改,让我们开始修改。我们从新的日志事件开始。打开位于中的 log_event.h 文件。/sql 文件夹。首先,将新事件的新枚举添加到enum Log_event_type列表中。我们需要将枚举添加到列表末尾的ENUM_END_EVENT标记之前。将新条目命名为SLAVE_CONNECT_LOG_EVENT ,并赋予它列表中的下一个值。清单 8-20 显示了上下文中的修改。

image 提示这是源代码中大多数枚举列表中的标准机制。结束标记的使用允许循环限制以及越界检查。

清单 8-20。 为 log_event.h 中的新事件添加新的枚举

enum Log_event_type
{
  /*
    Every time you update this enum (when you add a type), you have to
    fix Format_description_log_event::Format_description_log_event().
  */
  UNKNOWN_EVENT= 0,
  START_EVENT_V3= 1,
  QUERY_EVENT= 2,
  STOP_EVENT= 3,
  ROTATE_EVENT= 4,
  INTVAR_EVENT= 5,
  LOAD_EVENT= 6,

...

/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add new log event enumeration */
   SLAVE_CONNECT_LOG_EVENT= 36,
/* END CAB MODIFICATION */
   ENUM_END_EVENT /* end marker */
 };

同样在 log_event.h 文件中,我们需要为新事件添加一个新的类声明。将事件命名为 Slave_connect_log_event,并从 log_event 基类派生它。将此代码添加到文件中包含其他日志事件类声明的部分。

您可以复制任何其他日志事件声明,并相应地更改名称。使用“搜索和替换”时要小心,因为您不想更改原始日志事件。清单 8-21 显示了完整的类声明。我将在本节后面更详细地描述每个元素。

清单 8-21。 在 log_event.h 中声明 Slave_connect_log_event 类

/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add new log event class declaration */
class Slave_connect_log_event : public Log_event {
public:
#ifndef MYSQL_CLIENT
  Slave_connect_log_event(THD *thd_arg, const char * query, ulong query_len)
      : Log_event(thd_arg, LOG_EVENT_IGNORABLE_F,
                  Log_event::EVENT_STMT_CACHE,
                  Log_event::EVENT_IMMEDIATE_LOGGING)
  {
    DBUG_ENTER("Slave_connect_log_event::Slave_connect_log_event");
    if (!(m_slave_connect= (char*) my_malloc(query_len + 1, MYF(MY_WME))))
      return;
    my_snprintf(m_slave_connect, query_len + 1, "%s", query);
    DBUG_PRINT("enter", ("%s", m_slave_connect));
    DBUG_VOID_RETURN;
  }
#endif

#ifndef MYSQL_CLIENT
  int pack_info(Protocol*);
#endif

  Slave_connect_log_event(const char *buf, uint event_len,
                          const Format_description_log_event *descr_event);

  virtual ∼Slave_connect_log_event();
  bool is_valid() const { return 1; }

#ifdef MYSQL_CLIENT
  virtual void print(FILE *file, PRINT_EVENT_INFO *print_event_info);
#endif
  virtual bool write_data_body(IO_CACHE *file);

  virtual Log_event_type get_type_code() { return SLAVE_CONNECT_LOG_EVENT; }

  virtual int get_data_size()
  {
    return IGNORABLE_HEADER_LEN + 1 + (uint) strlen(m_slave_connect);
  }
#if defined(MYSQL_SERVER) && defined(HAVE_REPLICATION)
  virtual int do_apply_event(Relay_log_info const *rli);
#endif

private:

  char *m_slave_connect;
};
/* END CAB MODIFICATION */

请注意,有些部分受到条件编译标志的保护。这是因为该代码与服务器源代码的其他部分共享。例如,它用在 mysqlbinlog 客户端工具中。显然,该应用不需要某些代码。因此,我们屏蔽掉条件编译不需要的部分。

我想提醒大家注意一面非常重要但不为人知的旗帜。注意 LOG_EVENT_IGNORABLE_F 标志是基类构造函数的第二个参数。顾名思义,这个标志告诉服务器忽略这个事件。二进制日志和中继日志代码旨在忽略任何带有此标志的事件。这意味着我们只需要处理写入二进制日志(和打印事件)的普通方法。在许多地方,我们还必须添加代码来跳过销毁事件实例,但幸运的是,我们有一个模型可以遵循。我将在本节稍后描述这些。

在类声明的底部,我放置了一个指针来包含将作为有效负载添加到日志事件中的消息。需要在类中定义的关键方法包括构造函数和析构函数、pack_info()print()write_body()apply_event()。大多数剩下的方法都是不言自明的。注意get_type_code()get_data_size(方法。当我们将代码添加到 log_event.cc 文件中时,我将解释其中的每一个。

现在我们有了类声明,打开位于。/sql 文件夹。首先,定位Log_event::get_type_str()方法 ,并为Slave_connect_log_event添加一个新的 case 语句。case 语句使用我们之前创建的枚举。这种方法在很多地方被用来描述视图中的事件,比如SHOW BINLOG EVENTS。清单 8-22 显示了上下文的变化。该代码位于第 686 行附近。

清单 8-22。Log _ event . cc 中 Log_event::get_type_str()的 Case 语句

   case INCIDENT_EVENT: return "Incident";
   case IGNORABLE_LOG_EVENT: return "Ignorable";
   case ROWS_QUERY_LOG_EVENT: return "Rows_query";
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add case to return name of new log event */
   case SLAVE_CONNECT_LOG_EVENT: return "Slave_connect";
/* END CAB MODIFICATION */
   case WRITE_ROWS_EVENT: return "Write_rows";
   case UPDATE_ROWS_EVENT: return "Update_rows";
   case DELETE_ROWS_EVENT: return "Delete_rows";

我们还需要为新事件添加一个 case 语句到Log_event::read_log_event() 。这个方法负责创建一个新事件。它还使用前面定义的新枚举。添加一个新的 case 语句来创建一个Slave_connect_log_event类的新实例。清单 8-23 显示了上下文的变化。该代码位于第 1579 行附近。请注意,除了类名之外,它与其他事件的代码相同。

清单 8-23。Log _ event . cc 中 Log_event::read_log_event()的 Case 语句

     case ROWS_QUERY_LOG_EVENT:
       ev= new Rows_query_log_event(buf, event_len, description_event);
       break;
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add case to create new log event */
     case SLAVE_CONNECT_LOG_EVENT:
       ev= new Slave_connect_log_event(buf, event_len, description_event);
       break;
/* END CAB MODIFICATION */
     case GTID_LOG_EVENT:
     case ANONYMOUS_GTID_LOG_EVENT:

还有一个方法需要修改。Format _ description _ log _ event()返回每个日志事件的头长度。在这种情况下,我们需要返回新日志事件的头长度。清单 8-24 在上下文中显示了这段代码。该代码位于行号 5183 附近。在这种情况下,我们返回代码中已经定义的可忽略日志事件头的长度。

清单 8-24。log _ event . cc 中 Format_description_log_event()的新查找

       post_header_len[HEARTBEAT_LOG_EVENT-1]= 0;
       post_header_len[IGNORABLE_LOG_EVENT-1]= IGNORABLE_HEADER_LEN;
       post_header_len[ROWS_QUERY_LOG_EVENT-1]= IGNORABLE_HEADER_LEN;
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Return header length for the new log event */
       post_header_len[SLAVE_CONNECT_LOG_EVENT-1]= IGNORABLE_HEADER_LEN;
/* END CAB MODIFICATION */
       post_header_len[WRITE_ROWS_EVENT-1]=  ROWS_HEADER_LEN_V2;
       post_header_len[UPDATE_ROWS_EVENT-1]= ROWS_HEADER_LEN_V2;
       post_header_len[DELETE_ROWS_EVENT-1]= ROWS_HEADER_LEN_V2;

现在我们可以为类方法本身添加代码。我列出了清单 8-25 中的所有代码,然后解释了每个方法。

清单 8-25。??【Slave _ connect _ log _ event 方法 in log_event.cc

/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Class method definitions for the new log event */
Slave_connect_log_event::Slave_connect_log_event(const char *buf,
                              uint event_len,
                              const Format_description_log_event *descr_event)
  : Log_event(buf, descr_event)
{
  DBUG_ENTER("Slave_connect_log_event::Slave_connect_log_event");
  uint8 const common_header_len= descr_event->common_header_len;
  uint8 const post_header_len=
    descr_event->post_header_len[SLAVE_CONNECT_LOG_EVENT-1];

  DBUG_PRINT("info",
             ("event_len: %u; common_header_len: %d; post_header_len: %d",
                     event_len, common_header_len, post_header_len));

  /*
   m_slave_connect length is stored using only one byte, but that length is
   ignored and the complete query is read.
  */
  int offset= common_header_len  post_header_len  1;
  int len= event_len - offset;
  if (!(m_slave_connect= (char*) my_malloc(len1, MYF(MY_WME))))
    return;
  strmake(m_slave_connect, buf  offset, len);
  DBUG_PRINT("info", ("m_slave_connect: %s", m_slave_connect));
  DBUG_VOID_RETURN;
}

Slave_connect_log_event::∼Slave_connect_log_event()
{
  my_free(m_slave_connect);
}

#ifndef MYSQL_CLIENT
int Slave_connect_log_event::pack_info(Protocol *protocol)
{
  char *buf;
  size_t bytes;
  ulong len= sizeof("# SLAVE_CONNECT = ")  (ulong) strlen(m_slave_connect);
  if (!(buf= (char*) my_malloc(len, MYF(MY_WME))))
    return 1;
  bytes= my_snprintf(buf, len, "# SLAVE_CONNECT = %s", m_slave_connect);
  protocol->store(buf, bytes, &my_charset_bin);
  my_free(buf);
  return 0;
}
#endif

#ifdef MYSQL_CLIENT
void
Slave_connect_log_event::print(FILE *file,
                            PRINT_EVENT_INFO *print_event_info)
{
  IO_CACHE *const head= &print_event_info->head_cache;
  IO_CACHE *const body= &print_event_info->body_cache;
  char *slave_connect_copy= NULL;
  if (!(slave_connect_copy= my_strdup(m_slave_connect, MYF(MY_WME))))
    return;

  my_b_printf(head, "# Slave Connect:\n# %s\n", slave_connect_copy);
  print_header(head, print_event_info, FALSE);
  my_free(slave_connect_copy);
  print_base64(body, print_event_info, true);
}
#endif

bool
Slave_connect_log_event::write_data_body(IO_CACHE *file)
{
  DBUG_ENTER("Slave_connect_log_event::write_data_body");
  /*
   m_slave_connect length will be stored using only one byte, but on read
   that length will be ignored and the complete query will be read.
  */
  DBUG_RETURN(write_str_at_most_255_bytes(file, m_slave_connect,
              (uint) strlen(m_slave_connect)));
}

#if defined(MYSQL_SERVER) && defined(HAVE_REPLICATION)
int Slave_connect_log_event::do_apply_event(Relay_log_info const *rli)
{
  DBUG_ENTER("Slave_connect_log_event::do_apply_event");
  DBUG_ASSERT(rli->info_thd == thd);
  /* Set query for writing Slave_connect log event into binlog later.*/
  thd->set_query(m_slave_connect, (uint32) strlen(m_slave_connect));

  DBUG_ASSERT(rli->slave_connect_ev == NULL);

  const_cast<Relay_log_info*>(rli)->slave_connect_ev= this;

  DBUG_RETURN(0);
}
#endif

以下部分将更详细地描述每种方法。我解释了为什么我们需要这个方法,如何使用这个方法,以及代码中实现的任何细节。

Slave _ connect _ log _ event::Slave _ connect _ log _ event()

这是类实例的构造函数。这里需要特别注意的是,当写入二进制日志时,我们为事件的消息或负载分配内存。它也有设置头长度的代码。

Slave _ connect _ log _ event::∞Slave _ connect _ log _ event()

这是类实例的析构函数。这里,我们只是释放了在构造函数中分配的字符串。

slave _ connect _ log _ event::pack _ info()

此方法用于存储要写入二进制日志的事件数据。我们格式化一个字符串,使其包含一个描述事件的标签,以及一个包含连接到主服务器的从服务器的主机名、端口和 server_id 的字符串。

奴隶 _ 连接 _ 日志 _ 事件::打印()

客户端使用此方法以人类可读的形式打印事件。请注意条件编译指令,它确保代码只在客户端编译。这告诉我们这个方法与 pack_info()事件非常不同。

在这种方法中,打印头,后面是包含主机名、端口和 server_id 的类似格式的事件数据。在这个方法中,我们还复制了类中的字符串,以便客户端应用可以在类之外使用它。

slave _ connect _ log _ event::write _ data _ body()

此方法与 pack_info()方法结合使用。它会将事件数据写入二进制日志。

slave _ connect _ log _ event::do _ apply _ event()??

此方法用于将事件写入中继日志。由于该事件被忽略,因此不会被写入中继日志。我没有将这个方法留空,而是将它作为一个例子来完成,以防您希望创建自定义事件。在这种情况下,您可以使用这里的代码作为示例。

现在我们已经定义了类,我们可以向二进制日志类添加一个方法,可以更容易地从主代码中调用该方法。我们首先向类中添加一个新的方法声明。打开 binlog.h 文件,并在行号 541 附近找到 write_incident()事件。添加一个名为 write_slave_connect()的新方法,该方法带有当前线程实例、主机名、端口和 server_id 的参数。我们将使用最后三个参数作为 Slave_connect_log_event 类的信息性消息或负载。清单 8-26 显示了新的方法声明。

清单 8-26。binlog . h 中用于 write_slave_connect() 的方法声明

   bool write_incident(Incident_log_event *ev, bool need_lock_log,
                       bool do_flush_and_sync= true);

/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Method declaration for writing the slave connect event to the binlog */
   bool write_slave_connect(THD *thd, char *host, int port, int server_id);
/* END CAB MODIFICATION */

   void start_union_events(THD *thd, query_id_t query_id_param);
   void stop_union_events(THD *thd);
   bool is_query_in_union(THD *thd, query_id_t query_id_param);

现在我们可以将方法定义添加到 binlog.cc 文件中。与头文件一样,我们将把新方法放在 write_incident()方法附近。打开此文件,将此方法放在行号 5154 附近。清单 8-27 显示了完整的方法。

清单 8-27。binlog . cc 中的 write_slave_connect()方法定义

/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add new method definition to write slave connect event to binlog */
bool MYSQL_BIN_LOG::write_slave_connect(THD *thd, char *host, int port, int server_id)
{
  char buffer[255];
  binlog_cache_data* cache_data= NULL;
  DBUG_ENTER("MYSQL_BIN_LOG::write_slave_connect");

  /* Record slave connection in the binary log */
  sprintf(buffer, "Host: %s Port: %d Server_Id: %d", host, port, server_id);
  Slave_connect_log_event ev(thd, buffer, (int)strlen(buffer));

  if (thd->binlog_setup_trx_data())
    DBUG_RETURN(1);
  cache_data= &thd_get_cache_mngr(thd)->trx_cache;
  if (cache_data->write_event(thd, &ev))
    DBUG_RETURN(1);
  cache_data->finalize(thd, NULL);
  ordered_commit(thd, true);

  DBUG_RETURN(0);
}
/* END CAB MODIFICATION */

在方法中,我们首先创建新事件的新实例,然后调用方法来设置事务,检索缓存管理器(用于将事件写入二进制日志缓存的设备),并写入事件。finalize()方法确保为事件设置了正确的标志,ordered_commit()方法完成事务,从而确保在将缓存刷新到磁盘时将事件写入二进制日志。

接下来,我们将为调用新的二进制日志方法的主服务器更改代码。rpl_master.cc 文件包含主设备的代码,包括从设备连接时使用的代码。当从机连接时,调用 register_slave()方法。我们将使用这个方法作为通过 write_slave_connect()方法启动 Slave_connect_log_event 的起点。

打开 rpl_master.cc 文件,在第 147 行附近找到 register_slave()方法。添加清单 8-28 中的代码。这段代码应该出现在互斥锁调用之后和 unregister_slave()调用之前。

清单 8-28。 对 rpl_master.cc 中的 register_slave()的修改

   mysql_mutex_lock(&LOCK_slave_list);
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Write a new Slave_connect_log_event to binary log when slave connects */
   /* If this is a new slave registration, log the slave connect message. */
   if (my_hash_search(&slave_list, (uchar*)&thd->server_id, 4) == NULL)
   {
     DBUG_PRINT("info", ("Logging slave connect for host: %s", si->host));
     mysql_bin_log.write_slave_connect(thd, si->host, si->port, si->server_id);
   }
/* END CAB MODIFICATION */
   unregister_slave(thd, false, false/*need_lock_slave_list=false*/);
   res= my_hash_insert(&slave_list, (uchar*) si);
   mysql_mutex_unlock(&LOCK_slave_list);

您可能会注意到这段代码有些奇怪。如果你和我一样,当我第一次查看register_slave()方法时,我得出结论,每次从设备连接到主设备时,该方法都会被调用一次。然而,我没有意识到的是,每次从设备向主设备请求数据时,都会调用这个方法。因此,register_slave()方法可以被多次调用。如果我们希望仅在从设备第一次连接时记录从设备连接事件,我们必须首先搜索从设备散列。如果我们找不到,我们可以打电话给write_slave_connect()

现在我们有了新的事件类和二进制日志类中的新方法来编写事件,并将其链接到主代码,我们可以处理扩展的次要部分来完成这个特性。

我们从中继日志代码开始。打开rpl_rli.h文件并添加一个新的成员变量来包含该类的一个实例。当事件被忽略时,它仍然被从机的 IO 线程读取,并被继电器日志代码询问。因此,中继日志代码需要创建一个实例,以确定它是否可忽略。清单 8-29 显示了添加新变量的代码。这位于 480 号线附近,但是在Relay_log_info级的任何地方都可以。

清单 8-29。 为 rpl_rli.h 中的 Slave_connect_log_event 类添加新变量

   bool deferred_events_collecting;

/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add a new variable for a Slave_connect_log_event instance */

  Slave_connect_log_event* slave_connect_ev;

/* END CAB MODIFICATION */
   /*****************************************************************************

接下来,我们添加代码,以便在 IO 线程读取事件时初始化新变量。打开 rpl_rli.cc 文件,找到构造函数 Relay_log_info::Relay_log_info()。添加新变量的初始化,并将其设置为 NULL。我们这样做是为了防止在尚未初始化(实例化)的类实例上调用方法或属性。这样做很好,但是需要在调用任何方法或属性之前检查变量是否为 NULL。清单 8-30 显示了添加到构造函数中的代码。

清单 8-30。rpl _ rli . cc 中 slave_connect_ev 变量的初始化

    retried_trans(0),
    tables_to_lock(0), tables_to_lock_count(0),
    rows_query_ev(NULL), last_event_start_time(0), deferred_events(NULL),
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Initialize the slave_connect_ev variable */
    slave_connect_ev(NULL), slave_parallel_workers(0),
/* END CAB MODIFICATION */
    recovery_parallel_workers(0), checkpoint_seqno(0),
    checkpoint_group(opt_mts_checkpoint_group),
    recovery_groups_inited(false), mts_recovery_group_cnt(0),

接下来要修改的几个文件主要是将新事件添加到现有的销毁实例和错误处理代码中。我选择将这段代码放在与 Rows_query_log_event 相同的位置。这是一个不错的选择,因为 Rows_query_log_event 也是一个可忽略的事件。

同样在 rpl_rli.cc 文件中,我们需要添加代码来销毁实例(如果它之前被实例化的话)。我们在 cleanup_context()方法 中这样做。清单 8-31 显示了删除(销毁)实例的代码。注意,我们首先检查实例是否已经被实例化。这避免了在试图销毁不存在的类实例时出现特别严重的错误。

清单 8-31。 代码销毁 rpl_rli 中 cleanup_context()中的变量..复写的副本

     rows_query_ev= NULL;
     info_thd->set_query(NULL, 0);
   }
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Destroy the Slave_connect_log_event instance */
   if (slave_connect_ev)
   {
     delete slave_connect_ev;
     slave_connect_ev= NULL;
     info_thd->set_query(NULL, 0);
   }
/* END CAB MODIFICATION */
   m_table_map.clear_tables();
   slave_close_thread_tables(thd);

下一个要更改的文件是rpl_rli_pdb.cc。打开文件,找到文件中的最后一个方法slave_worker_exec_job() 。我们需要在该方法的错误处理代码中添加另一个排除。清单 8-32 显示了新的变化。注意,我们以与Rows_query_log_event 事件相同的方式添加了另一个条件。

清单 8-32。 添加条件删除 rpl_rli_pdb.cc 中的 Slave_connect_log_event

   // todo: simulate delay in delete
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Destroy the Slave_connect_log_event instance */
   if (ev && ev->worker && ev->get_type_code() != ROWS_QUERY_LOG_EVENT &&
       ev->get_type_code() != SLAVE_CONNECT_LOG_EVENT)
/* END CAB MODIFICATION */
   {
     delete ev;
   }

接下来,我们将新事件添加到 rpl_slave.cc 代码中的另一个排除中。打开文件,并在 exec_relay_log_event()方法中找到执行后破坏事件的条件。这位于 3654 号线附近。清单 8-33 显示了这种情况下所需的改变。注意,我们在 if 语句中添加了另一个条件。

清单 8-33。 从 rpl_slave.cc 中的销毁条件中排除 Slave_connect_log_event

         clean-up routine.
       */
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Exclude the Slave_connect_log_event from destruction */
       if (ev->get_type_code() != FORMAT_DESCRIPTION_EVENT &&
           ev->get_type_code() != ROWS_QUERY_LOG_EVENT &&
           ev->get_type_code() != SLAVE_CONNECT_LOG_EVENT)
/* END CAB MODIFICATION */
       {
         DBUG_PRINT("info", ("Deleting the event after it has been executed"));
         delete ev;

最后要修改的文件是 sql_binlog.cc,有两个地方需要修改代码。首先,我们必须将 SLAVE_CONNECT_LOG_EVENT 添加到排除项中,以便删除已经执行的事件。其次,我们必须添加代码,以便在出现错误时销毁新的事件实例。

打开 sql_binlog.cc 文件,找到第 292 行附近的 Rows_query_log_event 的错误条件,并进行如清单 8-34 所示的更改。请注意,我们只是在 if 语句中添加了另一个条件。

清单 8-34。 从 sql_binlog.cc 的销毁条件中排除 Slave_connect_log_event

         of the event.
       */
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Exclude the Slave_connect_log_event from destruction */
       if (ev->get_type_code() != FORMAT_DESCRIPTION_EVENT &&
           ev->get_type_code() != ROWS_QUERY_LOG_EVENT &&
           ev->get_type_code() != SLAVE_CONNECT_LOG_EVENT)
/* END CAB MODIFICATION */
       {
         delete ev;
         ev= NULL;

我们必须在错误条件中添加一个条件,在出现错误的情况下销毁新事件。和前面的例子一样,我们将模拟 Rows_query_log_event 的代码。这位于 320 号线附近。清单 8-35 显示了新的代码。我们检查错误,如果有错误并且新事件存在(指针不为空或 0),我们删除实例。

清单 8-35。 如果 sql_binlog.cc 中的错误条件则销毁 Slave_connect_log_event

       delete rli->rows_query_ev;
       rli->rows_query_ev= NULL;
     }
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Destroy the Slave_connect_log_event instance if there is an error */
     if ((error || err) && rli->slave_connect_ev)
     {
       delete rli->slave_connect_ev;
       rli->slave_connect_ev= NULL;
     }
/* END CAB MODIFICATION */
     rli->slave_close_thread_tables(thd);
   }
   thd->variables.option_bits= thd_options;

这就完成了对代码的修改,将新的可忽略事件添加到二进制日志的事件列表中。显然,这不是一个微不足道的变化,日志事件涉及到几个源代码文件。

您现在已经准备好编译代码了。检查你修改的所有文件的正确性。您还应该从源代码树的根开始编译服务器,这样所有组件都可以根据更改进行构建。这是必要的,以便 mysqlbinlog 客户端应用可以解密新事件。

编译代码

现在,编译服务器代码。如果所有的修改都如清单所示,代码应该可以编译了。如果没有,返回并检查修改。下一节演示新事件的运行。

示例执行

在本节中,我们将看到新日志事件的运行演示。无论在主服务器还是从服务器上,都没有多少输出,因为日志事件隐藏在复制中。但是,有一个 SQL 命令 SHOW BINLOG EVENTS ,可以用来显示服务器上二进制日志事件的摘录。我们之前讨论过的一个客户端工具 mysqlbinlog 可以用来检查二进制或中继日志的内容。

我们正在建立一个测试环境。不像在本章的第一个例子中那样使用实用程序,我将向您展示一个有趣的技巧,使用现有的服务器测试环境快速设置主服务器和从服务器。

第 4 章中描述的测试环境,mysql-test-run,可用于设置主服务器和从服务器。我们通过使用两个关键选项来做到这一点::-启动和退出和-套件。start-and-exit 选项告诉 mysql-test-run 启动一个新的服务器并退出。这与 mysqlserverclone 实用程序具有相同的效果,但与该实用程序不同,mysql-test-run 使用 mysqld 进程的预定义选项。在大多数情况下,这很好,但是该实用程序允许您创建专门的服务器实例。

我们同时也想开始一个奴隶。如果您提供一个测试套件,特别是复制套件,mysql-test-run 将创建额外的服务器。对于- suite=rpl,mysql-test-run 将创建两个预配置用于复制的服务器。

显然,这比为主服务器和从服务器启动一个新实例要容易得多。对于需要一个主机和一个从机的情况,我们可以使用一个命令,而不是两个命令——这已经比手动启动 mysqld 实例更容易了。清单 8-36 显示了一个使用 mysql-test-run 来启动主服务器和从服务器的例子。

清单 8-36。 开始一个新的主从使用 MTR

cbell@ubuntu:$ ./mysql-test-run.pl --start-and-exit --suite=rpl
Logging: ./mysql-test-run.pl  --start-and-exit --suite=rpl
121003 18:26:01 [Note] Plugin 'FEDERATED' is disabled.
121003 18:26:01 [Note] Binlog end
121003 18:26:01 [Note] Shutting down plugin 'CSV'
121003 18:26:01 [Note] Shutting down plugin 'MyISAM'
MySQL Version 5.6.6
Checking supported features...
 - skipping ndbcluster
 - SSL connections supported
 - binaries are debug compiled
Using suites: rpl
Collecting tests...
Checking leftover processes...
Removing old var directory...
Creating var directory '/mysql-test/var'...
Installing system database...
Using server port 54781

==============================================================================

TEST                                      RESULT   TIME (ms) or COMMENT
------------------------------------------------------------------------------

worker[1] Using MTR_BUILD_THREAD 300, with reserved ports 13000..13009
worker[1]
Started [mysqld.1 - pid: 2300, winpid: 2300] [mysqld.2 - pid: 2330, winpid: 2330]
worker[1] Using config for test rpl.rpl_000010
worker[1] Port and socket path for server(s):
worker[1] mysqld.1  13000  /mysql-test/var/tmp/mysqld.1.sock
worker[1] mysqld.2  13001  /mysql-test/var/tmp/mysqld.2.sock
worker[1] Server(s) started, not waiting for them to finish

我们现在可以使用 mysqlreplicate 实用程序快速地将从设备连接到主设备,并开始复制数据。清单 8-37 显示了运行这个实用程序的输出。

清单 8-37。 设置复制

cbell@ubuntu:$ python ./scripts/mysqlreplicate.py  --master=root@localhost:13000 --slave=root@localhost:13001
# master on localhost: ... connected.
# slave on localhost: ... connected.
# Checking for binary logging on master...
# Setting up replication...
# ...done.

你在期待什么吗?也许是一个信息或无处不在的“冰”声?这些都不应该发生。复制背后的代码是广泛而稳定的,并且大部分是完全静默的。要了解复制发生了什么,以及在这种情况下刚刚发生了什么,您必须查询主服务器或从服务器的状态。

如果您还记得,当从机连接时,扩展应该向主机上的二进制日志中写入一个日志事件。因为我们用 mysqlreplicate 设置了复制,所以没有显示任何内容。我们必须去大师那里看看这个事件是否真的发生了。

image 提示如果你在控制台模式下启动服务器,你会在控制台中看到任何消息和错误。这包括来自复制的错误和警告。例如,如果从主机读取或执行事件时出现问题,从主机将显示与连接到主机相关的消息和一个错误。通过直接启动 mysqld 可执行文件,可以在 Linux 和 Mac 系统上以控制台模式启动服务器。在 Windows 上,您必须使用- console 选项。

检查从机的错误可能是个好主意。在从机上执行SHOW SLAVE STATUS并检查错误。我把这个留给你作为练习。你应该看到的是一个正常健康的奴隶。

让我们回到主人身上。SHOW BINLOG EVENTS命令将显示写入二进制日志的最新事件。如果Slave_connect_log_event工作正常,您应该在视图中看到它。清单 8-38 显示了在主服务器上运行SHOW BINLOG EVENTS的结果。记住,这个命令只显示第一个二进制日志中的事件。使用IN子句指定特定的二进制日志。

image 注意在任何没有启用二进制日志记录的服务器上运行 SHOW BINLOG 事件将导致空的结果集。如果您在对这段代码运行测试时看到这种情况,请检查以确保您不是在从属代码上运行。

清单 8-38。 二进制主机上的日志事件

cbell@ubuntu:$ mysql -uroot -h 127.0.0.1 --port=13000
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 4
Server version: 5.6.6-m9-debug-log Source distribution

Copyright (c) 2000, 2011, 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> show binlog events \G
*************************** 1\. row ***************************
   Log_name: master-bin.000001
        Pos: 4
 Event_type: Format_desc
  Server_id: 1
End_log_pos: 121
       Info: Server ver: 5.7.0-m10-debug-log, Binlog ver: 4
*************************** 2\. row ***************************
   Log_name: master-bin.000001
        Pos: 121
 Event_type: Query
  Server_id: 1
End_log_pos: 326
       Info: GRANT REPLICATION SLAVE ON *.* TO 'rpl'@'localhost'
*************************** 3\. row ***************************
   Log_name: master-bin.000001
        Pos: 326
 Event_type: Slave_connect
  Server_id: 2
End_log_pos: 390
       Info: # SLAVE_CONNECT = Host: 127.0.0.1 Port: 13001 Server_Id: 2
3 rows in set (0.00 sec)

mysql>

注意,上面的清单在输出中显示了新的日志事件。我们看到事件的有效负载(Info 字段)包括从属服务器的主机名、端口和 server_id。它显示了环回地址,因为复制拓扑(主和从)运行在本地机器上(localhost = 127.0.0.1)。

现在,让我们检查 mysqlbinlog 客户端应用的输出。仅使用主服务器的二进制日志作为唯一参数来运行应用。因为我使用 mysql-test-run 设置了服务器,所以我在/mysql-test/var 文件夹下找到了这些文件。在本例中,我在寻找主服务器的二进制文件,主服务器是第一台服务器,因此,文件夹名为 mysqld.1,数据目录名为 data。因此,mysql-test 的相对路径是。/var/mysqld.1/data。

清单 8-39 显示了 mysqlbinlog 客户端应用在测试拓扑中转储主服务器的二进制日志的输出。

清单 8-39。 用 mysqlbinlog 检查主服务器上的二进制日志

cbell@ubuntu:$ ../client/mysqlbinlog ./var/mysqld.1/data/master-bin.000001
/*!40019 SET @@session.max_insert_delayed_threads=0*/;
/*!50003 SET @OLD_COMPLETION_TYPE=@@COMPLETION_TYPE,COMPLETION_TYPE=0*/;
DELIMITER /*!*/;
# at 4
#121003 16:03:59 server id 1  end_log_pos 121 CRC32 0x83c06bae Start: binlog v 4, server v 5.6.6-m9-debug-log created 121003 16:03:59 at startup
# Warning: this binlog is either in use or was not closed properly.
ROLLBACK/*!*/;
BINLOG '
X8RsUA8BAAAAdQAAAHkAAAABAAQANS43LjAtbTEwLWRlYnVnLWxvZwAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAABfxGxQEzgNAAgAEgAEBAQEEgAAXQAEGggAAAAICAgCAAAACgoKGRkAAAGu
a8CD
'/*!*/;
# at 121
#121003 16:05:23 server id 1  end_log_pos 326 CRC32 0x559a612a Query       thread_id=1       exec_time=0       error_code=0
SET TIMESTAMP=1349305523/*!*/;
SET @@session.pseudo_thread_id=1/*!*/;
SET @@session.foreign_key_checks=1, @@session.sql_auto_is_null=0, @@session.unique_checks=1, @@session.autocommit=1/*!*/;
SET @@session.sql_mode=1073741824/*!*/;
SET @@session.auto_increment_increment=1, @@session.auto_increment_offset=1/*!*/;
/*!\C latin1 *//*!*/;
SET @@session.character_set_client=8,@@session.collation_connection=8,@@session.collation_server=8/*!*/;
SET @@session.lc_time_names=0/*!*/;
SET @@session.collation_database=DEFAULT/*!*/;
GRANT REPLICATION SLAVE ON *.* TO 'rpl'@'localhost'
/*!*/;
# at 326
# Slave Connect:
# Host: 127.0.0.1 Port: 13001 Server_Id: 2
#121003 16:05:23 server id 2  end_log_pos 390 CRC32 0x262bb144 DELIMITER ;
# End of log file
ROLLBACK /* added by mysqlbinlog */;
/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;

输出可能看起来像一堆随机的、神奇的字符串和位,但是相信我,这一切都是有意义的。在列表底部附近,您可以看到 Slave_connect_log_event 的结果。请记住,mysqlbinlog 客户端应用使用 Slave_connect_log_event::print()方法来显示二进制日志事件。print()方法被设计为为事件打印两行:标题和有效负载,显示从服务器的主机名、端口和 server_id。

如果这个二进制日志来自一个有许多从服务器的主服务器,其中一些从服务器已经被添加、删除和再次添加,那么运行 mysqlbinlog 客户端应用的输出将显示所有的从服务器连接事件。事实上,您可以通过管道将输出传递给诸如grep这样的程序,以便在文件中找到' # Slave Connect:'行的所有位置。

除了这个实用程序之外,复制特性的这个扩展还演示了日志事件代码的位置,以及如何将日志事件写入二进制日志和从二进制日志中读取日志事件。我希望这个练习已经为您提供了一些解决您自己独特的高级复制挑战的思路。

摘要

在这一章中,我介绍了 MySQL 复制的一个简短教程,包括为什么你会使用它,它的架构,以及复制源代码之旅。您了解了如何设置复制以及几种扩展 MySQL 复制特性集的方法。

在下一章中,我将展示 MySQL 的另一个强大特性——可插拔架构。我通过探索另一个高级特性——可插拔身份验证来探索这个体系结构。我将简要介绍 MySQL 中的可插拔工具,并展示一个使用 RFID 标签进行用户验证的示例身份验证插件。

有些人觉得这些角色的名字带有贬义,并暗示它们在某些文化中可能具有攻击性。因此,在未来,这些名称可能会改变,但服务器的角色或工作不太可能改变。

2 MySQL 有一个多线程从机特性,实现多线程读取中继日志,从而提高从机在某些用例中的性能。关于这个特性的更深入的讨论超出了本文的范围,但是可以在在线参考手册中找到。

3HAVE _ REPLICATION条件编译主要用于嵌入式服务器。

九、开发 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 02110–1301  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-4cmakelists . 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 级引擎。事务处理不需要索引。然而,唯一性应该是一个问题,性能会受到影响。

十一、数据库系统内部原理

本章介绍了一些数据库系统内部的概念,为更深入地研究数据库系统内部做准备。我将更深入地介绍查询在服务器内部是如何表示的,以及查询是如何执行的。我从一个更一般的角度来探讨这些主题,然后以讨论如何使用 MySQL 系统来进行自己的 MySQL 系统内部实验来结束这一章。最后,我将介绍数据库系统的内部实验项目。

查询执行

大多数数据库系统使用迭代或解释执行策略。迭代方法 提供了产生可用于处理离散操作(例如,连接、项目等)的调用序列的方法。),但其设计并不包含内部表示的特征。将查询转换成迭代方法使用了函数式编程和程序转换技术。一些算法从基于关系代数的查询规范生成迭代程序。

查询执行机制的实现创建了一组使用高级语言形成的已定义的已编译功能原语,然后通过调用堆栈或过程调用序列将它们链接在一起。当创建并选择执行查询执行计划时,编译器(通常与创建数据库系统的编译器相同)用于将过程调用编译成二进制可执行文件。由于迭代方法的高成本,编译后的执行计划通常会被存储起来,以供类似或相同的查询重用。

解释性方法 另一方面,使用基本操作的现有编译抽象来执行查询执行。所选择的查询执行计划被重新构造为一个方法调用队列,每个方法调用都从队列中取出并进行处理;然后将结果存储在内存中,供下一次或后续调用使用。这种策略的实现通常被称为惰性评估、,因为可用的编译方法没有针对最佳性能进行优化;相反,它们是为通用性而优化的。

MySQL 查询执行 再探

MySQL 中的查询处理和执行是解释性的。它是使用线程架构实现的,每个查询都有自己的执行线程。图 11-1 描绘了一个描述 MySQL 查询处理方法的框图。

9781430246596_Fig11-01.jpg

图 11-1。 MySQL 查询执行

当客户机发出查询时,会创建一个新线程,并将 SQL 语句转发给解析器进行语法验证(或因错误而拒绝)。正如您在前一章中看到的,MySQL 解析器是使用一个用 Bison 编译的大型莱克斯-YACC 脚本实现的。解析器构造了一个用来保存查询的数据结构。这种数据结构或查询结构可用于执行查询。一旦创建了查询结构,控制就传递给查询处理器,查询处理器执行诸如验证表完整性和安全访问之类的检查。一旦授予了所需的访问权限并打开了表(如果查询是更新,则表被锁定),控制权就传递给执行基本查询操作的各个方法,如 select、restrict 和 project。通过对表和操作的列表进行排序来对数据结构进行优化,以形成基于常见实践的更有效的查询。这种形式的优化被称为选择项目连接查询处理器。使用已建立的通信协议和访问方法将查询操作的结果返回给客户端。

什么是编译查询 ?

理解“编译”的意思时,一个经常混淆的领域。编译查询是迭代查询执行计划的实际编译,但一些研究人员(如 C. J. Date)认为编译查询是经过优化并存储以供将来执行的查询。因此,在考虑使用编译后的查询时,您必须小心。在这项工作中,避免使用单词 compiled ,因为查询优化器和执行引擎不存储查询执行计划供以后重用,查询执行也不需要任何编译或汇编工作。

image 注意存储过程的概念是一个保存的计划——它被编译或优化,以便以后执行,并且可以对满足其输入参数的数据运行多次。

探索 MySQL 内部

你怎样才能教一个人优化器是如何工作的,而不允许他参与项目呢?此外,在没有实际看到数据库系统的内部情况下,您怎么能期望了解它们呢?在本节中,我将通过讨论如何将 MySQL 用作专业人士和学者的实验平台来回答这些问题。

开始使用 MySQL 进行实验

使用 MySQL 进行实验有几种方法。例如,您可以使用交互式调试器来研究内部组件,或者您可以使用 MySQL 系统作为您自己的内部数据库技术实现的宿主。查看服务器内部行为的另一个有用的方法是打开跟踪,并读取使用调试打开时编译的服务器生成的跟踪。关于调试跟踪的更多细节,参见第 5 章。

如果您要进行实验,请使用专用服务器进行实验。如果您计划使用 MySQL 开发自己的扩展,这一点很重要。你不想冒实验污染你的开发服务器的风险。

试用 MySQL 源代码

实验 MySQL 最具侵入性的方法是修改源代码。这包括在系统运行时观察它,然后通过用另一个算法或代码段替换另一个算法或代码段来设计改变部分的实验,然后观察行为的变化。虽然这种方法将使您能够研究 MySQL 的内部工作方式,但是以这种方式对源代码进行更改可能会导致服务器变得太不稳定而无法使用——特别是如果您突破了算法和数据结构的极限,或者更糟的是,违反了内存引用。然而,学习源代码最好的方法就是观察它的运行。以这种方式进行的测试可以用来为其他形式的实验收集数据。

使用 MySQL 作为实验技术的主机

使用 MySQL 进行实验的一种侵入性较小的方法是使用 MySQL 作为自己实验代码的宿主。这使您可以专注于优化器和执行引擎,而不用担心系统的其他部分。数据库系统有许多部分。仅举几个例子,MySQL 中有用于网络通信、数据输入和访问控制的子组件,甚至还有用于使用和管理文件和内存的实用程序。您可以在自己的代码中使用 MySQL 的资源,而不是创建自己的子组件。

我已经用这种方法实现了本书中描述的实验项目。我将向您展示如何连接到 MySQL 解析器,并使用 MySQL 解析器来读取、测试和接受有效命令,并将代码重定向到实验项目优化器和执行例程。

解析器和词法分析器 识别已经在解析器或词法散列中定义的字母数字字符串(也称为记号)。解析器用位置信息(在流中出现的顺序)标记所有的标记,并使用识别非标记字符串的特定模式的逻辑来识别文字和数字。一旦解析器完成,控制返回到词法分析器。MySQL 中的词法分析器被设计用来识别标记和非标记的特定模式。一旦有效命令被识别,控制就被传递给每个命令的执行代码。MySQL 解析器和词法分析器可以进行修改,以包括新的令牌或关键字,用于实验。关于如何修改解析器和词法分析器的更多细节,参见第 7 章。这些命令可以设计成模拟 SQL 的命令,代表典型的数据操作命令,如 select、update、insert 和 delete,以及典型的数据定义命令,如 create 和 drop。

一旦控制传递给实验优化/执行引擎,就可以使用 MySQL 内部查询表示结构运行实验,或者转换成另一种结构。从那里,可以运行查询优化和执行的实验实现,并使用 MySQL 系统将结果返回给客户机。这允许您在实现自己的内部数据库组件时使用网络通信和解析子组件。

运行多个 MySQL 实例

关于 MySQL 服务器的一个鲜为人知的事实是,可以在一台机器上运行服务器的多个实例。这允许您在与开发安装相同的机器上运行修改后的 MySQL 系统。如果您的资源有限,或者为了进行比较,您希望将修改后的服务器作为另一个安装在同一台计算机上运行,那么您可能需要这样做。运行多个 MySQL 实例需要在命令行或配置文件中指定某些参数。

虚拟机拯救世界

您还可以利用虚拟机来进一步隔离您的实验。为了充分利用这个选项,您可以用一个基本的 MySQL 服务器安装配置一个虚拟机 ,然后克隆这个虚拟机。这允许您运行 MySQL 的一个实例,就像它在一台单独的机器上一样。一些虚拟机环境还允许您在机器运行时拍摄快照,这允许您在拍摄快照时重新启动会话。这可以节省您设置测试环境的大量时间。当面对复杂或冗长的测试环境设置时,我经常选择这种方法。

有许多虚拟环境可供选择。开源的 Oracle VirtualBox 是学者和希望省钱的人的绝佳选择。VirtualBox 可以在大多数平台上运行,并提供设置虚拟 MySQL 服务器主机所需的所有功能。VMWare 的软件很贵,但是它提供了更多的功能。如果您运行 Mac OS X,VMWare 的 Fusion 和 Parallel Desktop 都是出色的低成本替代产品。

至少,您需要为服务器通信指定不同的 TCP 端口或套接字,并为数据库文件指定不同的目录。在 Windows 上启动第二个 MySQL 实例的一个例子是:

mysqld-debug.exe --port=3307 --datadir="c:/mysql/test_data" --console

在这个例子中,我告诉服务器使用 TCP 端口 3307 (默认为 3306),并使用不同的数据目录,作为控制台应用运行。为了连接到服务器的第二个实例,我必须告诉客户机使用与服务器相同的端口。例如,为了连接到我的第二个实例,我将启动 MySQL 客户端实用程序,它具有:

mysql.exe -uroot --port=3307

--port参数也可以与mysqladmin实用程序一起使用。例如,要关闭在端口 3307 上运行的第二个实例,请发出以下命令:

mysqladmin.exe -uroot --port=3307 shutdown

这项技术存在潜在的问题。很容易忘记您连接的是哪台服务器。防止混淆或避免向错误的服务器发出查询(如DELETEDROP)的一种方法是更改 MySQL 客户端实用程序上的提示,以指示您所连接的服务器。

例如,发出命令prompt DBXP->为连接到实验服务器的 MySQL 客户机设置提示符,为连接到开发服务器的 MySQL 客户机设置提示符prompt Development->。这种技术使您一眼就能看出您将要向哪个服务器发出命令。在 MySQL 客户端使用prompt命令的例子如清单 11-1 所示。

清单 11-1。 实验服务器改变 MySQL 客户端提示的例子

mysql> prompt DBXP->
PROMPT set to 'DBXP->'
DBXP->show databases;
+−−------------------+
| Database           |
+−−------------------+
| information_schema |
| mysql              |
| test               |
+−−------------------+
3 rows in set (0.01 sec)

DBXP->

image 提示您也可以使用\d选项在提示中设置当前数据库。例如,要在连接到实验服务器的客户机中设置提示符,发出命令prompt DBXP:\d->。这将设置提示,指示您连接到实验服务器和当前数据库(由最后一个use命令指定),用冒号(例如DBXP:TEST->)隔开。

您可以使用这种技术来限制对修改后的服务器的访问。如果更改端口号或套接字,只有知道正确参数的人才能连接到服务器。这将使您能够将修改暴露给用户群的风险降至最低。如果您的开发环境是多样化的,有许多共享相同资源的实验和研究项目(这在学术界是常见的),您可能还想采取这些步骤来保护您自己的实验免受其他项目的污染。这通常不是问题,但它有助于采取预防措施。

image 注意如果使用二进制、查询或慢速查询日志,还必须为 MySQL 服务器的每个实例指定日志文件的备用位置。否则可能会导致日志文件和/或数据损坏。

局限性和关注点

也许使用 MySQL 进行实验最具挑战性的方面是修改解析器来识别 SQL 命令的新关键字(见第 7 章)。虽然不完全是一种复杂或新的实现语言,但修改 YACC 文件需要仔细关注最初开发者的意图。解决方案包括在每个解析器命令定义的顶部放置新命令的 SQL 语法定义的副本。这允许您拦截解析器的流,以便重定向查询执行。

最常见也是最重要的挑战是跟上 MySQL 代码库的不断变化。不幸的是,升级的频率是不可预测的。如果您想跟上特性变化的步伐,实验性技术的集成需要在每次发布源代码时重新插入对 MySQL 源文件的修改。对于任何想尝试 MySQL 的人来说,这可能不是一个问题。如果您发现自己因为正在编写的扩展而想要跟上变化,您可能应该使用源代码管理工具或构建第二个服务器进行实验,并在原始服务器上进行开发。

image 提示您最有可能遇到的挑战是检查 MySQL 代码库,并发现各种内部数据表示的含义、布局和用途。克服这一点的唯一方法是通过熟悉。我鼓励您访问和阅读文档(在线 MySQL 参考手册)以及 MySQL 网站、博客和消息论坛上的文章。它们是丰富的信息。虽然有些很难理解,但是每读一遍,这些概念就会变得更加清晰。抵制对文档感到沮丧的诱惑。休息一会儿,然后回头再看一遍。每当我(重读)阅读技术资料时,我都能找到有用的信息。

数据库系统内部实验

我构建了数据库实验项目(DBXP ) 来让您探索 MySQL 的内部,并让您探索一些替代数据库系统的内部实现。您可以通过这个实验了解更多关于数据库系统是如何构建的,以及它们是如何工作的。

为什么是实验?

DBXP 是一个实验而不是一个解决方案,因为它是不完整的。也就是说,这些技术以最小的错误处理、有限的特征集和低鲁棒性来实现。

这并不意味着 DBXP 技术不能被修改来替代 MySQL 系统的内部组件;更确切地说,DBXP 是为探索而设计的,而不是为生产而设计的。

实验项目概述

DBXP 项目是一系列实现内部查询表示、查询优化、查询执行和文件访问的替代算法和机制的类。这不仅让您有机会探索查询优化理论 的高级实现,而且还使 DBXP 技术的核心能够在不修改 MySQL 内部操作的情况下执行。这提供了额外的安全性,本地 MySQL 核心可执行代码不会受到 DBXP 技术的影响。这个额外的好处可以帮助减轻修改现有系统的一些风险。

MySQL 解析器(见sql_parse.cc)的实现通过调用为每个 SQL 命令实现的函数,将控制指向执行子进程的特定实例。例如,SHOW命令被重定向到在sql_show.cc文件中实现的函数。需要修改sql_parse.cc中的 MySQL 解析器代码,将处理重定向到 DBXP 查询处理器。

DBXP 查询处理器的第一步是将 MySQL 内部查询表示转换成实验性的内部表示。选择的内部表示被称为查询树,其中每个节点包含一个原子关系操作(选择、投影、连接等)。)和链接代表数据流。图 11-2 显示了一个查询树的概念示例。在示例中,我使用了符号:投影/选择(π)、限制(σ)和连接(φ)。箭头表示数据如何从表流向根。一个连接操作被表示为一个有两个子节点的节点。当数据从每个子节点呈现时,join 操作能够处理该数据并将结果传递给树中的下一个节点(其父节点)。每个节点可以有零个、一个或两个子节点,并且只有一个父节点。

9781430246596_Fig11-02.jpg

图 11-2。查询树概念

选择查询树是因为它允许 DBXP 查询优化器使用树操作算法。也就是说,优化使用树结构和树操作算法来以更有效的执行顺序排列树中的节点。此外,优化查询的执行是通过将树遍历到叶节点,执行由节点指定的操作,并将信息传递回链接来完成的。这种技术也使得以流水线方式执行成为可能,数据从叶节点传递到根节点,一次一个数据项。

将树向下遍历到一个数据项的叶子,并将其返回到树上(这个过程称为脉冲)允许每个节点处理一个数据项,在结果集中一次返回一行。树的这种脉动或轮询允许流水线的执行。结果是查询结果的更快的初始返回和感觉到的查询结果到客户端的更快的传输时间。见证查询结果更快地返回——尽管不是一下子全部返回——给用户一种查询更快的感觉。

使用 MySQL 托管 DBXP 实现始于 MySQL 解析器,其中 DBXP 代码接管查询的优化和执行,然后使用 MySQL 网络通信实用程序将结果一次一行地返回给客户机。

实验项目的组成部分

实验项目旨在向您介绍数据库系统内部实现的替代方案,并允许您通过向项目添加自己的修改来探索实现。DBXP 是使用一组简单的 C++类实现的,这些类表示数据库系统中的对象。

有用于元组、关系、索引和查询树的类。添加了额外的类来管理多用户对表的访问。图 11-3 显示了一个 DBXP 的高层架构的例子。

项目主要类别的完整列表见表 11-1 。这些类以与类相同的名称存储在源文件中(例如,Attribute类分别在名为attribute.hattribute.cc的文件中定义和实现)。

表 11-1。数据库内部实验项目类

描述
查询 _ 树 提供查询的内部表示形式。还包含查询优化器。
Expression 提供了一个表达式评估机制。
Attribute 存储和操作元组(行)的属性(列)的操作。

这些类代表了数据库系统的基本构件。第 12 章到第 14 章包含了对查询树、启发式优化器和流水线执行算法的完整解释。这些章节还包括该实用程序的概述。我将向您展示 DBXP 实现的某些部分(最复杂的部分)的实现细节,剩下的部分留给您作为练习来实现。

9781430246596_Fig11-03.jpg

图 11-3。实验项目的高层图

本书的引言部分介绍了如何在课堂环境中使用实验项目的建议。

进行实验

运行实验需要为新项目修改 cmake 文件,并用 MySQL 服务器编译它们。没有一个项目文件需要任何特殊的编译或库。对 MySQL 配置和 cmake 文件的修改细节将在下一章中讨论,在这一章中,我将向您展示如何在实验中删除 SQL 命令。

如果您没有尝试过前几章的示例程序,我在下面包含了构建和运行 DBXP 实验项目的基本过程。

  1. 修改/sql 文件夹中的CMakeLists.txt file
  2. 运行'cmake.’ from the root of the source tree.
  3. 运行'make’ from the root of the source tree
  4. 停止服务器,将可执行文件复制到二进制目录中。
  5. 重新启动服务器并通过 MySQL 客户端连接以运行 DBXP SQL 命令。

摘要

在这一章中,我介绍了一些更复杂的数据库内部技术。您了解了查询在服务器内部是如何表示的以及如何执行的。更重要的是,您发现了如何使用 MySQL 来进行自己的数据库内部实验。这些技术的知识应该让您更好地理解 MySQL 系统是如何构建的,以及它是如何执行的。

在下一章中,我将通过一个查询树结构的示例实现向您展示更多关于内部查询表示的内容。下一章是一系列章节的开始,旨在为您实现自己的查询优化器和执行引擎提供基础。如果你想知道构建一个数据库系统需要什么,接下来的章节将向你展示如何开始使用你自己的查询引擎。

十二、内部查询表示

本章介绍了数据库实验项目(DBXP)的高级数据库技术的第一部分。我首先介绍查询树结构的概念,它用于在内存中存储查询。接下来,我将展示这个项目使用的查询树结构,以及实现 DBXP 代码的一系列简短项目中的第一个。本章以一组练习结束,您可以使用这些练习来学习更多关于 MySQL 和查询树的知识。

查询树

查询树是对应于查询的树结构,其中树的叶节点包含访问关系的节点和具有零个、一个或多个子节点的内部节点。内部节点包含关系运算符。这些操作符包括 project(描述为π)、restrict(描述为σ)和 join(描述为θ或image)。 1 树的边表示自下而上的数据流——即从对应于读取数据库中数据的树叶到生成查询结果的最终操作符根。图 12-1 描述了一个查询树的例子。

9781430246596_Fig12-01.jpg

图 12-1。示例查询树 2

对查询树的评估包括在操作数可用时评估内部节点操作,并将评估操作的结果沿树向上传递给父节点。当根节点被评估并被形成查询结果的元组替换时,评估终止。以下部分给出了用于在存储器中存储查询表示的查询树结构的变体。使用这种机制相对于关系演算内部表示的优势在表 12-1 中显示。

表 12-1 。使用查询树相对于关系演算的优势

作战需求 查询树 关系演算
可以减吗? 是的。可以在评估查询计划之前修剪查询树。 只有通过应用代数运算。
它能支持执行吗? 是的。该树可用于通过沿树向上传递数据来执行查询。 否。需要翻译成另一种形式。
它能支持关系代数表达式吗? 是的。这棵树非常适合关系代数。 否。需要转换。
它能在数据库系统中实现吗? 是的。树形结构是一种常见的数据结构。 只有通过模拟微积分的设计。
它能包含数据吗? 是的。树节点可以包含数据、操作和表达式。 没有。只有构成表达式的文字和变量。

显然,查询树内部表示优于现代数据库系统中采用的更传统的机制。例如,MySQL 中的内部表示是一组类和结构的表示,这些类和结构旨在包含查询及其元素,以便于(快速)遍历。它为优化和执行组织数据。 3

查询树内部表示有一些缺点。大多数优化器不是为在树形结构中工作而设计的。如果您想将查询树与优化器一起使用,就必须改变优化器。类似地,查询执行将与大多数查询处理实现非常不同。在这种情况下,查询执行引擎将从树中运行,而不是作为一个单独的步骤运行。这些缺点将在后面的章节中讨论,我将探索另一种优化器和执行引擎。

DBXP 查询树是使用节点结构的树数据结构,该节点结构包含表示这些操作所需的所有参数 :

  • 限制:允许您包含匹配属性表达式的结果。
  • Projection :提供选择包含在结果集中的属性的能力。
  • Join :允许您组合两个或多个关系,在结果集中形成一组复合属性。
  • Sort (order by) :允许您对结果集进行排序。
  • Distinct :提供将结果集缩减为唯一元组的能力。

image 注意 Distinct 是一个被添加来完成关系操作的操作,它不被大多数 SQL 实现所支持,也不是关系代数的固有属性。

投影、限制和连接是基本操作。Sort 和 distinct 作为附加的实用操作提供,有助于形成完整的查询树(所有可能的操作都表示为节点)。连接操作可以有连接条件(θ连接)或没有条件(等连接)。连接操作细分为以下操作:

  • Inner :返回元组的两个关系在匹配处的连接。
  • Outer (left,right,full) :返回FROM子句中提到的至少一个表或视图中的所有行,只要这些行满足任何WHERE搜索条件。所有行都是从用左外连接引用的左表中检索的,而右表中的所有行都在右外连接中引用。两个表中的所有行都在完全外部联接中返回。不匹配行的属性值作为空值返回。
  • Leftouter :两个关系的连接,返回匹配的元组,加上左边指定的关系中的所有元组,留下另一个关系中指定的不匹配属性为空(null)。
  • Rightouter :两个关系的连接,返回匹配的元组,加上右边指定的关系中的所有元组,留下另一个关系中指定的不匹配属性为空(null)。
  • Fullouter :两个关系的连接从两个关系中返回所有元组,将另一个关系中指定的不匹配属性留空(null)。
  • 交叉积:两个关系的连接,将第一个关系中的每个元组映射到另一个关系中的所有元组。

查询树还支持一些集合操作。支持的集合操作包括:

  • Intersect :集合运算,只返回模式相同的两个关系的匹配。
  • Union :集合运算,其中只返回具有相同模式的两个关系的不匹配。

什么是θ连接?

你可能想知道为什么有些连接被称为等连接,而有些被称为θ连接 。相等联接是联接条件为相等(=)的联接。θ连接是一种连接条件是不等式的连接(>、<、> =、< =、< >)。从技术上讲,所有的连接都是θ连接。θ连接很少使用,而相等连接很常见。

虽然 DBXP 查询树提供了 union 和 intersect 操作,但大多数数据库系统都支持结果集串联形式的 union。尽管 MySQL 解析器目前不支持交集操作,但是它支持联合。需要进一步修改 MySQL 解析器来实现 intersect 操作。以下部分描述了将 MySQL 查询表示转换为 DBXP 查询树的主要代码实现和类。

查询转换

必须修改 MySQL 解析器来识别和解析 SQL 命令。然而,我们需要一种方法来告诉解析器我们想要使用 DBXP 实现,而不是现有的查询引擎。为了简化更改,我简单地在 SQL 命令中添加了一个关键字(例如 DBXP ),将解析重定向到将 MySQL 内部表示转换为 DBXP 内部表示的代码。尽管这个过程增加了一些执行时间,并且需要少量的额外计算工作,但是这个实现简化了对解析器的修改,并且提供了一个通用的机制来比较 DBXP 数据结构和 MySQL 数据结构。我将带有 DBXP 关键字的 SQL 命令简称为 DBXP SQL 命令。

转换 4 的过程始于 MySQL 解析器,它将命令识别为 DBXP 命令。然后,系统将控制权交给一个名为sql_dbxp_parse.cc的类,该类管理解析后的查询从 MySQL 形式到 DBXP 内部表示(查询树)的转换。这是通过名为buid_query_tree的方法完成的。这个方法只对SELECTEXPLAIN SELECT语句调用。

DBXP 查询树

DBXP 查询优化器的核心是 DBXP 内部表示数据结构。它用于在 SQL 命令被解析和转换后表示查询。

这个结构被实现为一个树形结构(因此命名为查询树,其中每个节点有零个、一个或两个孩子。没有孩子的节点是树的叶子,有一个孩子的节点表示对数据执行一元操作的内部节点,有两个孩子的节点是连接或集合操作。源代码中的实际节点结构如清单 12-1 所示。

清单 12-1 DBXP 查询树节点

/*
  STRUCTURE query_node

  DESCRIPTION
    This this structure contains all of the data for a query node:

    NodeId -- the internal id number for a node
    ParentNodeId -- the internal id for the parent node (used for insert)
    SubQuery -- is this the start of a subquery?
    Child -- is this a Left or Right child of the parent?
    NodeType -- synonymous with operation type
    JoinType -- if a join, this is the join operation
    join_con_type -- if this is a join, this is the "on" condition
    Expressions -- the expressions from the "where" clause for this node
    Join Expressions -- the join expressions from the "join" clause(s)
    Relations[] -- the relations for this operation (at most 4)
    PreemptPipeline -- does the pipeline need to be halted for a sort?
    Fields -- the attributes for the result set of this operation
    Left -- a pointer to the left child node
    Right -- a pointer to the right child node
*/
struct query_node
{
  query_node();
  ∼query_node();
  int                 nodeid;
  int                 parent_nodeid;
  bool                sub_query;
  bool                child;
  query_node_type     node_type;
  type_join           join_type;
  join_con_type       join_cond;
  Item                *where_expr;
  Item                *join_expr;
  TABLE_LIST          *relations[4];
  bool                preempt_pipeline;
  List<Item>          *fields;
  query_node          *left;
  query_node          *right;
};

其中一些变量用于管理节点组织和形成树本身。最有趣的两个是nodeidparent_nodeid。这些用于建立树中节点的父子关系。这是必要的,因为作为优化过程的一部分,节点可以在树中上下移动。使用parent_nodeid变量避免了在树中维护反向指针的需要。 5

sub_query变量用于指示子查询的开始节点。因此,数据结构可以支持嵌套查询(子查询),而无需对结构进行额外的修改。唯一需要注意的是,优化算法被设计成使用子查询指示符作为树遍历的停止条件。也就是说,当检测到子查询节点时,优化会将子查询视为一个单独的实体。一旦检测到,就使用子查询节点作为下一次优化的开始,重新运行查询优化例程。因此,可以支持任意数量的子查询,并将其表示为树结构中的子树。这是查询树的一个重要特性,它克服了许多内部表示的局限性。

where_expr变量是一个指向 MySQL Item树的指针,它管理一个典型的通用表达式树。稍后我们将把它改为一个封装表达式的特殊类。详见第十三章

relations数组用于包含指向关系类的指针,这些关系类表示 MySQL 存储引擎中的内部记录结构的抽象。关系类通过存储引擎处理程序类提供对存储在磁盘上的数据的访问层。数组大小当前设置为 4。前两个位置(0 和 1)分别对应于左边和右边的孩子。接下来的两个位置(2 和 3)代表临时关系,比如重新排序(排序)和索引的应用。

image 注意relations数组大小设置为 4,这意味着您可以处理多达四个表的查询。如果您需要处理超过四个表的查询,您将需要更改本章后面显示的转换代码以接受超过四个表。

fields属性是指向 MySQL Item类的指针,该类包含一个表的字段列表。它在投影操作和维护关系操作所需的属性(例如,满足表达式但不是结果集一部分的属性的传播)中很有用。

最后一个感兴趣的变量是preempt_pipeline变量,DBXP Execute类使用它在处理来自子节点的数据时实现一个循环。每当操作需要遍历整个数据集(行)时,循环都是必要的。例如,在缺少允许排序的索引的情况下,联接一个公共属性上的两个关系的联接可能需要遍历一个或两个子节点,以便实现正确的映射(联接)操作。

这个类也负责查询优化(在第 13 章中描述)。由于查询树提供了操纵树的所有操作,并且由于查询优化也是一组树操作,因此优化是使用包装查询树结构的类(称为查询树类)中的方法来完成的。

优化器方法实现了一种启发式算法(在第 13 章的中描述,在第 14 章中有更详细的描述)。这些方法的执行导致将树重组为更优的树,并将一些节点分成两个或更多的其他节点,这些节点也可以被重新定位以形成更优的树。最佳树允许更有效地执行查询。

该类还支持成本优化,使用遍历树的算法,将可用索引应用于每个叶节点(直接访问关系存储的节点)的访问方法。

这种结构可以支持各种各样的操作,包括限制、投影、连接、设置和排序(排序)。查询节点结构被设计为将这些操作中的每一个表示为单个节点,并且可以存储所有相关的和必需的信息以就地执行操作。此外,EXPLAIN命令被实现为树的后序遍历,从叶子开始打印出每个节点的内容(参见本章后面的show_plan方法)。这个操作的 MySQL 等价物需要更多的计算时间,并且是用一组复杂的方法实现的。

因此,查询树是一种内部表示,它可以表示任何查询,并提供一种通过操纵树来优化查询的机制。事实上,树结构本身简化了优化,并通过提供将查询操作关联为树中的节点的方式来实现启发式优化器。因此,该查询树是在任何关系数据库系统中使用的可行机制,并且可以推广到生产系统中使用。

在 MySQL 中实现 DBXP 查询树

本节展示了在 MySQL 源代码中添加的 DBXP 查询树结构。创建关系数据库研究工具的第一步旨在向您展示查询树是如何工作的,以及如何将 MySQL 查询结构转换为基本查询树(未优化)。后面的章节将描述优化器和执行引擎。

我们不会尝试重用现有的SELECT命令,而是在解析器中创建新的条目来实现带有字符串DBXP_SELECT的 SELECT 命令的特殊版本。这将允许修改被隔离,并且不会与解析器中现有的SELECT子组件混淆。接下来的部分向您展示了如何添加查询树和添加存根来执行DBXP_SELECTEXPLAIN DBXP_SELECT命令。

image 注意Apress 网站上关于本章和后续章节的源代码示例包含一个差异文件,您可以用它来应用于 MySQL 源代码树。根据您所使用的服务器版本,如果它是基于 5.6 版的,那么修补操作只需做最小的修改就可以应用。差异文件的使用使示例代码更小,并允许您查看上下文中的变化。

添加和更改的文件

按照本章中的示例,您将创建几个文件并修改一些 MySQL 源代码文件。表 12-2 列出了将要添加和更改的文件。

表 12-2 。添加和更改的文件摘要

文件 描述
mysqld.cc 在 MySQL 版本号上添加了 DBXP 版本号标签
莱克斯·h 将 DBXP 标记添加到词法哈希中
查询 _ 树. h DBXP 查询树头文件(新文件)
query_tree.cc DBXP 查询树类文件(新文件)
sql_cmd.h 将 DBXP_SELECT 添加到 enum_sql_command 列表
sql_yacc.yy 向解析器添加了 SQL 命令解析
sql_parse.cc 添加代码以处理“大交换机”的新命令

创建测试

以下部分解释了清除DBXP_SELECT命令的过程、查询树类以及EXPLAIN DBXP_SELECTDBXP_SELECT命令。目标是允许用户输入任何有效的SELECT命令,处理查询,并返回结果。

image 注意由于 DBXP 引擎是一个实验性的引擎,它仅限于表示检索数据的基本操作的查询。将这些章节的长度保持在可管理的大小和复杂性要求 DBXP 引擎不处理带有聚集的查询——那些包含HAVINGGROUP BYORDER BY子句的查询。(没有任何东西禁止这样做,所以你可以自由地自己实现这些操作。)

下面几节详细介绍了创建 DBXP 代码的这三个方面所需的步骤。我将创建一个单独的测试文件并使用它来测试功能,而不是创建三个小的测试。对于那些未实现的操作,您可以通过在命令的开头添加井号(#)来注释掉查询语句,或者如图所示运行测试并忽略尚未实现的命令的不可避免的错误(从而遵守测试优先开发的原则)。清单 12-2 显示了Ch12.test文件。

清单 12-2 章节测试(Ch12.test)

#
# Sample test to test the DBXP_SELECT and EXPLAIN DBXP_SELECT commands
#

# Test 1: Test stubbed DBXP_SELECT command.
DBXP_SELECT * FROM no_such_table;

# Test 2: Test stubbed Query Tree implementation.
DBXP_SELECT * FROM customer;

# Test 3: Test stubbed EXPLAIN DBXP_SELECT command.
EXPLAIN DBXP_SELECT * FROM customer;

当然,您可以使用这个测试作为指南,并添加您自己的命令来探索新代码。有关如何使用 MySQL 测试套件创建和运行该测试的更多详细信息,请参考第 4 章。

清除 DBXP_SELECT 命令

在这一节中,您将学习如何向 MySQL 解析器添加一个定制的SELECT命令。您将看到如何修改解析器以适应一个新命令,该命令模拟 MySQL 中传统的SELECT命令。

识别修改

您应该首先通过在 MySQL 版本号上添加一个标签来识别一个拥有 DBXP 技术的 MySQL 服务器,以确保您总是能够分辨出您连接到的是经过修改的服务器。

image 提示你可以随时使用SELECT VERSION()命令来检索服务器的版本。如果您使用的是 MySQL 命令行客户端,您可以更改命令提示符,以指示您所连接的服务器是带有 DBXP 代码的服务器。

要添加版本标签,打开mysqld.cc文件并找到set_server_version方法。添加一条语句,将标签附加到 MySQL 版本号字符串上。在这种情况下,我们将使用“-DBXP 2.0”来代表这本书的印刷。清单 12-3 显示了修改后的set_server_version方法。

清单 12-3。 修改 mysqld.cc 文件

static void set_server_version(void)
{
  char *end= strxmov(server_version, MYSQL_SERVER_VERSION,
                     MYSQL_SERVER_SUFFIX_STR, NullS);
#ifdef EMBEDDED_LIBRARY
  end= strmov(end, "-embedded");
#endif
#ifndef DBUG_OFF
  if (!strstr(MYSQL_SERVER_SUFFIX_STR, "-debug"))
    end= strmov(end, "-debug");
#endif
  if (opt_log || opt_slow_log || opt_bin_log)
    strmov(end, "-log");                        // This may slow down system
/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* This section adds the DBXP version number to the MySQL version number. */
  strmov(end, "-DBXP 2.0");
/* END DBXP MODIFICATION */
}

修改词汇结构

现在,让我们添加标识DBXP_SELECT命令所需的标记。打开lex.h文件,将清单 12-4 中粗体显示的代码添加到上下文中的symbols数组中。

清单 12-4 对 lex.h 文件的修改

static SYMBOL symbols[] = {
...
  { "DAY_MINUTE",       SYM(DAY_MINUTE_SYM)},
  { "DAY_SECOND",       SYM(DAY_SECOND_SYM)},
/* BEGIN DBXP MODIFICATION */
/* Reason for MODIFICATION */
/* This section identifies the symbols and values for the DBXP token */
  { "DBXP_SELECT",       SYM(DBXP_SELECT_SYM)},
/* END DBXP MODIFICATION */
  { "DEALLOCATE",       SYM(DEALLOCATE_SYM)},
  { "DEC", SYM(DECIMAL_SYM)},

。。。

添加命令枚举

本节解释如何添加新的DBXP_SELECT命令枚举。修改从在sql_parse.cc文件中向解析器命令开关添加一个新的 case 语句开始。开关使用事例的枚举值。

要添加新案例,您必须添加新的枚举值。这些值在解析器代码中被识别,并存储在lex->sql_command成员变量中。要向词法分析器添加新的枚举值,打开sql_cmd.h文件,并将清单 12-5 中的代码添加到enum_sql_command枚举中。

清单 12-5 添加 DBXP_SELECT 命令枚举

enum enum_sql_command {
...
/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* This section captures the enumerations for the DBXP command tokens */
  SQLCOM_DBXP_SELECT,
  SQLCOM_DBXP_EXPLAIN_SELECT,
/* END DBXP MODIFICATION */
...

将 DBXP_SELECT 命令添加到 MySQL 解析器

一旦为 case 语句添加了新的枚举值,您还必须向解析器代码(sql_yacc.yy)添加代码,以标识新的DBXP_SELECT语句。这分几个部分完成。您将向解析器添加一个新的令牌,需要对三个地方进行更新。

新的令牌一旦被激活,将允许解析器区分普通的 MySQL SELECT语句和您想要用 DBXP 代码处理的语句。我们对解析器进行编程,以便当令牌存在时,它指示解析器应该将sql_command变量设置为SQLCOM_DBXP_SELECT值,而不是普通的 MySQL select 枚举值(SQLCOM_SELECT)。这种技术允许您向普通的 MySQL 代码和 DBXP 代码发出相同的基本SELECT语句。例如,下面的SELECT语句都完成相同的任务;它们只是被不同地优化了。第一个将指向SQLCOM_SELECT case 语句,而第二个将指向SQLCOM_DBXP_SELECT case 语句。

SELECT * FROM customer;
DBXP_SELECT * FROM customer;

添加新令牌的代码如清单 12-6 所示。在sql_yacc.yy文件中找到令牌列表并添加代码。(名单大致按字母顺序排列)。

清单 12-6 给解析器添加命令符号

%token  DAY_SECOND_SYM
%token  DAY_SYM                       /* SQL-2003-R */
/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* This section defines the tokens for the DBXP commands */
%token  DBXP_SELECT_SYM
/* END DBXP MODIFICATION */
%token  DEALLOCATE_SYM                /* SQL-2003-R */
%token  DECIMAL_NUM

我们还需要将新命令添加到类型定义中。清单 12-7 展示了这一修改。

清单 12-7 向解析器添加命令语法操作

%type <NONE>
/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* Add the dbxp_select statement to the NONE type definition. */
        query verb_clause create change select dbxp_select do drop insert replace insert2
/* END DBXP MODIFICATION */

添加令牌的最后一个区域是将以下代码添加到语句部分。清单 12-8 显示了上下文中的修改。

清单 12-8 向选择部分添加命令

statement:
...
        | select
/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* Add the dbxp_select statement to the list of statements(commands). */
        | dbxp_select
/* END DBXP MODIFICATION */
        | set
...

我们现在可以添加语句来解析 DBXP_SELECT 命令。清单 12-9 显示了识别DBXP_SELECT命令和处理 select 命令的普通部分所需的解析器代码。注意,解析器识别 select 和 DBXP 符号,然后提供 select 选项、字段列表和FROM子句的其他解析。紧跟在那一行之后的是设置sql_command的代码。请注意,该代码还在原始 select-command 解析器代码前放置了一个竖线(|)。这是解析器语法用来处理命令变体的“or”运算符。要将这一更改添加到解析器中,打开sql_yacc.yy文件并定位select:标签,然后添加代码,如清单 12-9 所示。

清单 12-9 向解析器添加命令语法操作

/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* This section captures (parses) the SELECT DBXP statement */

dbxp_select:
         DBXP_SELECT_SYM DBXP_select_options DBXP_select_item_list
                 DBXP_select_from
         {
             LEX *lex= Lex;
           lex->sql_command = SQLCOM_DBXP_SELECT;
         }
         ;

/* END DBXP MODIFICATION */

select:
         select_init
          {
            LEX *lex= Lex;
            lex->sql_command= SQLCOM_SELECT;
          }
        ;

还要注意,代码引用了其他几个标签。清单 12-10 包含了这些操作的代码。第一个是DBXP_select_options,它标识了SELECT命令的有效选项。虽然这与 MySQL select 选项非常相似,但它只提供了两个选项:DISTINCTCOUNT(*)。下一个操作是识别FROM子句中的表的DBXP_select_from代码。它还调用DBXP_where_clause操作来识别WHERE子句。下一个操作是DBXP_select_item_list,它类似于 MySQL 代码。最后,DBXP_where_clause操作标识了WHERE子句中的参数。花些时间浏览这段代码,并按照操作找到它们相关的标签,看看每个标签都做了什么。要将这段代码添加到解析器中,找到select_from:标签并在它上面添加代码。尽管将代码放在哪里并不重要,但是这个位置似乎更符合逻辑,因为它与 MySQL select 操作位于同一区域。清单 12-10 显示了DBXP_SELECT解析器代码的完整源代码。

清单 12-10??【附加操作】为 DBXP_SELECT 命令

/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* This section captures (parses) the sub parts of the SELECT DBXP statement */

DBXP_select_options:
   /* empty */
   | DISTINCT
   {
     Select->options|= SELECT_DISTINCT;
   }
  ;

DBXP_select_from:
  FROM join_table_list DBXP_where_clause {};

DBXP_select_item_list:
  /* empty */
  | DBXP_select_item_list ',' select_item
  | select_item
  | '*'
    {
      THD *thd= YYTHD;
      Item *item= new (thd->mem_root)
                    Item_field(&thd->lex->current_select->context,
                               NULL, NULL, "*");
      if (item == NULL)
        MYSQL_YYABORT;
      if (add_item_to_list(thd, item))
        MYSQL_YYABORT;
      (thd->lex->current_select->with_wild)++;
    };

DBXP_where_clause:
  /* empty */  { Select->where= 0; }
   | WHERE expr
    {
      SELECT_LEX *select= Select;
      select->where= $2;
      if ($2)
        $2->top_level_item();
    }
    ;

/* END DBXP MODIFICATION */

...

image 注意一个精明的 yacc 开发者可能会发现代码中的一些地方,可以从原始 SELECT 语句的规则中减少或重用。对于那些对优化这段代码感兴趣的人来说,我把它作为一个练习。

既然您已经对词法分析器进行了更改,那么您必须生成等价的 C 源代码。幸运的是,正常的 cmake/make 步骤会解决这个问题。只需从源代码树的根执行这些命令。

cmake .
make

如果您想在不等待 make 文件处理所有源文件的情况下检查您的代码,您可以使用 Bison 来生成这些文件。打开一个命令窗口,导航到源代码树根下的/sql目录。运行命令:

bison -y -d sql_yacc.yy

这会生成两个新文件:y.tab.cy.tab.h。这些文件分别替换了sql_yacc.ccsql_yacc.h文件。在复制它们之前,请备份原始文件。完成后,将y.tab.c复制到sql_yacc.cc,将y.taqb.h复制到sql_yacc.h

莱克斯和 YACC 是什么,拜辛是谁?

Lex 代表“词法分析器生成器”,用作解析器来识别标记和文字,以及语言的语法。YACC 代表“又一个编译器”,用于识别和处理语言的语义定义。这些工具与 Bison(一个 YACC 兼容的解析器生成器,它从 Lex/YACC 代码生成 C 源代码)一起使用,为创建能够解析和处理语言命令的子系统提供了丰富的机制。事实上,这正是 MySQL 使用这些技术的方式。

如果您现在编译服务器,您可以发出DBXP_SELECT命令,但是什么也不会发生。这是因为您需要将 case 语句添加到sql_parse.cc中的解析器开关中。因为我们还没有一个完整的 DBXP 引擎,所以让我们通过删除 case 语句来使这个练习更有趣一些。清单 12-11 显示了一套完整的脚手架代码,你可以用它来实现DBXP_SELECT命令。在这段代码中,我使用 MySQL 实用程序类来建立一个记录集。代码的第一部分为虚拟表设置字段列表。接下来是将数据值写入网络流的代码行,最后,向客户端发送一个文件结束标记。将数据写入输出流需要调用protocol->prepare_for_resend(),使用protocol->store()存储要发送的数据,然后使用protocol->write()将缓冲区写入流。

清单 12-11 修改解析器命令开关

/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* This section adds the code to call the new DBXP_SELECT command. */
  case SQLCOM_DBXP_SELECT:
  {
    List<Item> field_list;
    /* The protocol class is used to write data to the client. */
    Protocol *protocol= thd->protocol;
    /* Build the field list and send the fields to the client */
    field_list.push_back(new Item_int("Id",(longlong) 1,21));
    field_list.push_back(new Item_empty_string("LastName",40));
    field_list.push_back(new Item_empty_string("FirstName",20));
    field_list.push_back(new Item_empty_string("Gender",2));
    if (protocol->send_ result_set_metadata (&field_list,
                              Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF))
      DBUG_RETURN(TRUE);
    protocol->prepare_for_resend();
    /* Write some sample data to the buffer and send it with write() */
    protocol->store((longlong)3);
    protocol->store("Flintstone", system_charset_info);
    protocol->store("Fred", system_charset_info);
    protocol->store("M", system_charset_info);
    if (protocol->write())
      DBUG_RETURN(TRUE);
    protocol->prepare_for_resend();
    protocol->store((longlong)5);
    protocol->store("Rubble", system_charset_info);
    protocol->store("Barnie", system_charset_info);
    protocol->store("M", system_charset_info);
    if (protocol->write())
      DBUG_RETURN(TRUE);

    protocol->prepare_for_resend();
    protocol->store((longlong)7);
    protocol->store("Flintstone", system_charset_info);
    protocol->store("Wilma", system_charset_info);
    protocol->store("F", system_charset_info);
    if (protocol->write())
      DBUG_RETURN(TRUE);

    /*
      send_eof() tells the communication mechanism that we're finished
      sending data (end of file).
    */
    my_eof(thd);
    break;
  }
/* END DBXP MODIFICATION */
  case SQLCOM_PREPARE:
...

每当检测到一个DBXP_SELECT命令时,这个存根代码就向客户机返回一个模拟记录集。继续输入这段代码,然后编译并运行测试。

测试 DBXP_SELECT 命令

我们想要运行的测试是发出一个DBXP_SELECT命令,并验证该语句是否被新的存根 case 语句解析和处理。您可以运行之前创建的测试,或者只需在 MySQL 命令行客户端中输入如下 SQL 语句(确保键入 DBXP 部分):

DBXP_SELECT * from no_such_table;

只要是有效的 SQL SELECT语句,在 DBXP 之后键入什么并不重要。清单 12-12 展示了一个你应该期待的输出的例子。

清单 12-12 存根测试结果

mysql> DBXP_SELECT * from no_such_table;
+----+------------+-----------+--------+
| Id | LastName   | FirstName | Gender |
+----+------------+-----------+--------+
| 3  | Flintstone | Fred      | M      |
| 5  | Rubble     | Barnie    | M      |
| 7  | Flintstone | Wilma     | F      |
+----+------------+-----------+--------+
3 rows in set (0.23 sec)

mysql>

添加查询树类

现在您已经有了一个存根DBXP_SELECT命令,您可以开始实现特定于 DBXP 的代码来执行一个SELECT命令。在这一节中,我将向您展示如何添加基本的查询树类并将 MySQL 内部结构转换为查询树。在下一章之前,我不会深入到查询树代码的内部。

添加查询树头文件

添加查询树类需要创建查询树头文件,并在 MySQL 代码中引用它。查询树头文件如清单 12-13 所示。请注意,我将该类命名为Query_tree。这遵循了 MySQL 编码准则,用首字母大写来命名类。花点时间浏览一下标题代码。您会看到这里没有太多代码——只有查询树节点结构和枚举的基础。注意,节点类型、连接条件类型、连接和聚集类型都有枚举。这些枚举允许查询树节点在查询的执行中承担独特的角色。我将在下一章详细解释如何使用它们。

您可以选择任何方式创建文件(或下载)。将其命名为query_tree.h,并将其放在 MySQL 源代码树的/sql目录中。不要担心如何将其添加到项目中;我将在后面的部分向您展示如何做到这一点。

清单 12-13 查询树头文件

/*
  query_tree.h

  DESCRIPTION
    This file contains the Query_tree class declaration. It is responsible for containing the
    internal representation of the query to be executed. It provides methods for
    optimizing and forming and inspecting the query tree. This class is the very
    heart of the DBXP query capability! It also provides the ability to store
    a binary "compiled" form of the query.

  NOTES
    The data structure is a binary tree that can have 0, 1, or 2 children. Only
    Join operations can have 2 children. All other operations have 0 or 1
    children. Each node in the tree is an operation and the links to children
    are the pipeline.

  SEE ALSO
    query_tree.cc
*/
#include "sql_priv.h"
#include "sql_class.h"
#include "table.h"
#include "records.h"

class Query_tree
{
public:
  enum query_node_type          //this enumeration lists the available
  {                              //query node (operations)
    qntUndefined = 0,
    qntRestrict = 1,
    qntProject = 2,
    qntJoin = 3,
    qntSort = 4,
    qntDistinct = 5
  };

  enum join_con_type            //this enumeration lists the available
  {                              //join operations supported
    jcUN = 0,
    jcNA = 1,
    jcON = 2,
    jcUS = 3
  };

  enum type_join                //this enumeration lists the available
  {                              //join types supported.
    jnUNKNOWN      = 0,          //undefined
    jnINNER        = 1,
    jnLEFTOUTER    = 2,
    jnRIGHTOUTER   = 3,
    jnFULLOUTER    = 4,
    jnCROSSPRODUCT = 5,
    jnUNION        = 6,
    jnINTERSECT    = 7
  };

    enum AggregateType          //used to add aggregate functions
    {
        atNONE      = 0,
        atCOUNT     = 1
    };

  /*
    STRUCTURE query_node

    DESCRIPTION
      This this structure contains all of the data for a query node:

      NodeId -- the internal id number for a node
      ParentNodeId -- the internal id for the parent node (used for insert)
      SubQuery -- is this the start of a subquery?
      Child -- is this a Left or Right child of the parent?
      NodeType -- synonymous with operation type
      JoinType -- if a join, this is the join operation
      join_con_type -- if this is a join, this is the "on" condition
      Expressions -- the expressions from the "where" clause for this node
      Join Expressions -- the join expressions from the "join" clause(s)
      Relations[] -- the relations for this operation (at most 2)
      PreemptPipeline -- does the pipeline need to be halted for a sort?
      Fields -- the attributes for the result set of this operation
      Left -- a pointer to the left child node
      Right -- a pointer to the right child node
*/
  struct query_node
  {
    query_node();
    ∼query_node();
    int                 nodeid;
    int                 parent_nodeid;
    bool                sub_query;
    bool                child;
    query_node_type     node_type;
    type_join           join_type;
    join_con_type       join_cond;
    Item                *where_expr;
    Item                *join_expr;
    TABLE_LIST          *relations[4];
    bool                preempt_pipeline;
    List<Item>          *fields;
    query_node          *left;
    query_node          *right;
  };

  query_node *root;              //The ROOT node of the tree

  ∼Query_tree(void);
  void ShowPlan(query_node *QN, bool PrintOnRight);

};

有了查询树头文件,您还需要查询树源文件。源文件必须提供查询树类的构造函数和析构函数方法的代码。清单 12-14 展示了完整的构造函数和析构函数方法。创建query_tree.cc文件,输入这个代码(或者下载)。将这个文件放在 MySQL 源代码树的/sql目录中。在后面的小节中,我将向您展示如何将它添加到项目中。

清单 12-14 查询树类

/*
  query_tree.cc

  DESCRIPTION
    This file contains the Query_tree class. It is responsible for containing the
    internal representation of the query to be executed. It provides methods for
    optimizing and forming and inspecting the query tree. This class is the very
    heart of the DBXP query capability! It also provides the ability to store
    a binary "compiled" form of the query.

  NOTES
    The data structure is a binary tree that can have 0, 1, or 2 children. Only
    Join operations can have 2 children. All other operations have 0 or 1
    children. Each node in the tree is an operation and the links to children
    are the pipeline.

  SEE ALSO
    query_tree.h
*/
#include "query_tree.h"

Query_tree::query_node::query_node()
{
  where_expr = NULL;
  join_expr = NULL;
  child = false;
  join_cond = Query_tree::jcUN;
  join_type = Query_tree::jnUNKNOWN;
  left = NULL;
  right = NULL;
  nodeid = -1;
  node_type = Query_tree::qntUndefined;
  sub_query = false;
  parent_nodeid = -1;
}

Query_tree::query_node::∼query_node()
{
  if(left)
    delete left;
  if(right)
    delete right;
}

Query_tree::∼Query_tree(void)
{
  if(root)
    delete root;
}

从 MySQL 结构构建查询树

接下来我们需要的是执行从 MySQL 内部结构到查询树的转换的代码。让我们使用一个助手源文件,而不是将代码添加到sql_parse.cc文件中。事实上,case 语句(在sql_parse.cc文件中)表示的许多命令都是这样完成的。创建一个名为sql_dbxp_parse.cc的新文件。在名为build_query_tree的文件中创建一个新函数,如清单 12-15 所示。代码是一个基本的转换方法。输入代码时,花点时间浏览一下代码(或者下载并复制粘贴到文件中)。

清单 12-15DBXP 解析器辅助文件

/*
  sql_dbxp_parse.cc

  DESCRIPTION
    This file contains methods to execute the DBXP_SELECT query statements.

  SEE ALSO
    query_tree.cc
*/
#include "query_tree.h"

/*
  Build Query Tree

  SYNOPSIS
    build_query_tree()
    THD *thd            IN the current thread
    LEX *lex            IN the pointer to the current parsed structure
    TABLE_LIST *tables  IN the list of tables identified in the query

  DESCRIPTION
    This method returns a converted MySQL internal representation (IR) of a
    query as a query_tree.

  RETURN VALUE
    Success = Query_tree * -- the root of the new query tree.
    Failed = NULL
*/
Query_tree *build_query_tree(THD *thd, LEX *lex, TABLE_LIST *tables)
{
  DBUG_ENTER("build_query_tree");
  Query_tree *qt = new Query_tree();
  Query_tree::query_node *qn = new Query_tree::query_node();
  TABLE_LIST *table;
  int i = 0;
  int num_tables = 0;

  /* Create a new restrict node. */
  qn->parent_nodeid = -1;
  qn->child = false;
  qn->join_type = (Query_tree::type_join) 0;
  qn->nodeid = 0;
  qn->node_type = (Query_tree::query_node_type) 2;
  qn->left = 0;
  qn->right = 0;

  /* Get the tables (relations) */
  i = 0;
  for(table = tables; table; table = table->next_local)
  {
    num_tables++;
    qn->relations[i] = table;
    i++;
  }

  /* Populate attributes */
  qn->fields = &lex->select_lex.item_list;
  /* Process joins */
  if (num_tables > 0)  //indicates more than 1 table processed
    for(table = tables; table; table = table->next_local)
      if (((Item *)table->join_cond() != 0) && (qn->join_expr == 0))
        qn->join_expr = (Item *)table->join_cond();
  qn->where_expr = lex->select_lex.where;
  qt->root = qn;
  DBUG_RETURN(qt);
}

注意,build_query_tree代码从创建一个新的查询节点开始,标识查询中使用的表,填充字段列表,并捕获joinwhere表达式。这些都是执行最基本的查询所需的基本项目。

阻止查询树执行

现在让我们考虑用代码创建一个查询树需要什么。创建一个名为DBXP_select_command的新函数,并复制清单 12-16 中的代码。将这个函数放在sql_dbxp_parse.cc文件中。该函数将从sql_parse.cc中的 case 语句中调用。

清单 12-16?? 处理 DBXP_SELECT 命令

/*
  Perform Select Command

  SYNOPSIS
    DBXP_select_command()
    THD *thd            IN the current thread

  DESCRIPTION
    This method executes the SELECT command using the query tree.

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int DBXP_select_command(THD *thd)
{
  DBUG_ENTER("DBXP_select_command");
  Query_tree *qt = build_query_tree(thd, thd->lex,
                                    (TABLE_LIST*) thd->lex->select_lex.table_list.first);
  List<Item> field_list;
  Protocol *protocol= thd->protocol;
  field_list.push_back(new Item_empty_string("Database Experiment Project (DBXP)",40));
  if (protocol->send_result_set_metadata(&field_list,
                            Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF))
    DBUG_RETURN(TRUE);
  protocol->prepare_for_resend();
  protocol->store("Query tree was built.", system_charset_info);
  if (protocol->write())
    DBUG_RETURN(TRUE);
  my_eof(thd);
  DBUG_RETURN(0);
}

这段代码首先调用转换函数(build_query_tree),然后创建一个存根结果集。这一次,我创建了一个只有一列一行的记录集,用于向客户机传递查询树转换完成的消息。虽然这段代码不是很有趣,但它是一个占位符,让您可以在查询树上进行更多的实验(参见本章末尾的练习)。将sql_dbxp_parse.cc文件放在 MySQL 源代码树的/sql目录中。

重新访问 DBXP_SELECT 命令

打开sql_parse.cc文件,为DBXP_select_command函数添加一个函数声明,将声明放在短语mysql_execute_command附近。清单 12-17 显示了DBXP_select_command函数的完整函数头。在注释块上方输入此代码,如下所示。

清单 12-17 修改解析器命令代码

/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* This section adds the code to call the new SELECT DBXP command. */
int DBXP_select_command(THD *thd);
int DBXP_explain_select_command(THD *thd);
/* END DBXP MODIFICATION */

您现在可以更改 case 语句中的代码(也称为解析器命令开关)来调用新的DBXP_select_command函数。清单 12-18 显示了调用这个函数的完整代码。注意,我们需要传入的唯一参数是当前线程(thd)。MySQL 内部查询结构和查询的所有其他元数据都是通过线程指针引用的。如您所见,这种技术大大简化了 case 语句。这也有助于模块化 DBXP 代码,使其更容易维护和修改您的实验。

清单 12-18 修改解析命令开关(sql_parse.cc)

/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* This section adds the code to call the new DBXP_SELECT command. */
  case SQLCOM_DBXP_SELECT:
  {
    res = DBXP_select_command(thd);
    if (res)
      goto error;
    break;
  }
/* END DBXP MODIFICATION */
  case SQLCOM_PREPARE:
  {
...

在编译服务器之前,您需要用将新的源代码文件(query_tree.hquery_tree.ccsql_dbxp_parse.cc)添加到项目(make)文件中。

将文件添加到 CMakeLists.txt 文件中

添加项目文件需要从源代码树的根目录修改/sql目录中的CMakeLists.txt文件。打开文件并找到 SQL_SHARED_SOURCES _标签。将源代码文件添加到服务器编译的源代码列表中(mysqld)。清单 12-19 显示了定义的开始和添加的项目文件。

清单 12-19?? 对 CMakeLists.txt 文件的修改

SET(SQL_SHARED_SOURCES
  abstract_query_plan.cc
  datadict.cc
...
  sql_dbxp_parse.cc
  query_tree.cc
...

image 注意修改 cmake 文件时,格式化列表时一定要使用空格。

测试查询树

一旦服务器编译无误,就可以使用 SQL 语句对其进行测试。与上一个测试不同,您应该输入一个引用现有对象的有效 SQL 命令。您可以如前一节所述运行测试(参见清单 12-20 ),或者在 MySQL 命令行客户端中输入以下命令:

DBXP_SELECT * from customer;

清单 12-20DBXP _ SELECT 测试的结果

mysql> DBXP_SELECT * FROM customer;

+--------------------------------------------------+
| Database Experiment Project (DBXP)               |
+--------------------------------------------------+
| Query tree was built.                            |
+--------------------------------------------------+
1 row in set (0.00 sec)

mysql>

您已经取消了DBXP_SELECT操作并构建了一个查询树,但是这并不有趣。如果我们能看到查询的样子会怎么样呢?我们将创建一个类似于EXPLAIN命令的函数,只是我们将创建一个树形查询的图形表示 6 ,而不是查询的信息列表。

显示查询树的详细信息

添加新命令需要在sql_parse.cc的解析器开关中为新的 case 语句添加新的枚举,并添加解析器代码来标识新命令。您还必须向sql_DBXP_parse.cc文件添加执行新命令的代码。虽然创建并向解析器添加解释查询树的EXPLAIN命令听起来很复杂,但是EXPLAIN SELECT命令在 MySQL 中是可用的,因此我们可以复制大量代码并重用其中的大部分。

向 MySQL 解析器添加 EXPLAIN DBXP_SELECT 命令

要向解析器添加新的枚举,打开sql_lex.h文件,在SQLCOM_DBXP_SELECT枚举的代码后面添加一个名为SQLCOM_DBXP_EXPLAIN_SELECT的枚举。清单 12-21 显示了完整的代码变更。一旦添加了代码,就可以像前面描述的那样重新生成词法哈希。

清单 12-21添加解释枚举

/* A Oracle compatible synonym for show */
describe:
/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* This section captures (parses) the EXPLAIN (DESCRIBE) DBXP statements */

  describe_command DBXP_SELECT_SYM DBXP_select_options DBXP_select_item_list
                 DBXP_select_from
  {
          LEX *lex= Lex;
          lex->sql_command = SQLCOM_DBXP_EXPLAIN_SELECT;
          lex->select_lex.db= 0;
          lex->verbose= 0;
  }

/* END DBXP MODIFICATION */
...

注意,在这段代码中,解析器识别了一个EXPLAIN DBXP_SELECT命令。事实上,它调用了许多与DBXP_SELECT解析器代码相同的操作。唯一的区别是这段代码将sql_command设置为新的枚举(SQLCOM_DBXP_EXPLAIN_SELECT)。

sql_parse.cc中解析器开关语句的修改需要为sql_DBXP_parse.cc中执行EXPLAIN命令的代码添加函数声明。打开sql_parse.cc文件,为EXPLAIN函数添加函数声明。说出函数的名字DBXP_explain_select_command(你开始看到一个模式了吗?).将它添加到与DBXP_select_command函数声明相同的位置。清单 12-22 显示了两个 DBXP 命令的完整代码。

清单 12-22修改解析器命令代码

/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* This section adds the code to call the new DBXP_SELECT command. */
int DBXP_select_command(THD *thd);
int DBXP_explain_select_command(THD *thd);
/* END DBXP MODIFICATION */

还需要为 DBXP explain 命令添加新的 case 语句。这些语句类似于DBXP_SELECT命令的 case 语句。清单 12-23 显示了添加的新 case 语句。

清单 12-23对解析器切换语句的修改

/* BEGIN DBXP MODIFICATION */
/* Reason for Modification: */
/* This section adds the code to call the new DBXP_SELECT command. */
  case SQLCOM_DBXP_SELECT:
  {
    res = DBXP_select_command(thd);
    if (res)
      goto error;
    break;
  }
  case SQLCOM_DBXP_EXPLAIN_SELECT:
  {
    res = DBXP_explain_select_command(thd);
    if (res)
      goto error;
    break;
  }
/* END DBXP MODIFICATION */

创建 show_plan 函数

EXPLAIN DBXP_SELECT命令将查询路径显示为在字符文本范围内打印出来的树。在sql_DBXP_parse.cc文件中名为show_plan的函数中执行EXPLAIN代码。一个名为write_printf的辅助函数用于使show_plan代码更容易阅读。清单 12-2412-25 显示了这两种方法的完整代码。

清单 12-24 增加一个函数来捕获协议存储和写语句

/*
  Write to vio with printf.

  SYNOPSIS
    write_printf()
    Protocol *p     IN the Protocol class
    char *first     IN the first string to write
    char *last      IN the last string to write

  DESCRIPTION
    This method writes to the vio routines printing the strings passed.

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int write_printf(Protocol *p, char *first, const char *last)
{
  char *str = new char[1024];

  DBUG_ENTER("write_printf");
  strcpy(str, first);
  strcat(str, last);
  p->prepare_for_resend();
  p->store(str, system_charset_info);
  p->write();
  delete str;
  DBUG_RETURN(0);
}

请注意,write_printf代码调用了protocol->storeprotocol->write函数向客户端写入一行绘图。我将让您探索清单 12-25 中的源代码,看看它是如何工作的。在下一节中,我将向您展示一个代码执行的示例。该代码使用后序遍历从查询树的根开始生成查询计划。将这些方法添加到sql_DBXP_parse.cc文件中。

清单 12-25?? 展示 _ 计划源代码

/*
  Show Query Plan

  SYNOPSIS
    show_plan()
    Protocol *p         IN the MySQL protocol class
    query_node *Root    IN the root node of the query tree
    query_node *qn      IN the starting node to be operated on.
    bool print_on_right IN indicates the printing should tab to the right
                           of the display.

  DESCRIPTION
    This method prints the execute plan to the client via the protocol class

  WARNING
    This is a RECURSIVE method!
    Uses postorder traversal to draw the quey plan

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int show_plan(Protocol *p, Query_tree::query_node *root,
              Query_tree::query_node *qn, bool print_on_right)
{
  DBUG_ENTER("show_plan");

  /* spacer is used to fill white space in the output */
  char *spacer = (char *)my_malloc(80, MYF(MY_ZEROFILL | MY_WME));
  char *tblname = (char *)my_malloc(256, MYF(MY_ZEROFILL | MY_WME));
  int i = 0;

  if(qn != 0)
  {
    show_plan(p, root, qn->left, print_on_right);
    show_plan(p, root, qn->right, true);
    /* draw incoming arrows */
    if(print_on_right)
      strcpy(spacer, "          |               ");
    else
      strcpy(spacer, "     ");

    /* Write out the name of the database and table */
    if((qn->left == NULL) && (qn->right == NULL))
    {
      /*
         If this is a join, it has 2 children, so we need to write
         the children nodes feeding the join node. Spaces are used
         to place the tables side-by-side.
      */
      if(qn->node_type == Query_tree::qntJoin)
      {
        strcpy(tblname, spacer);
        strcat(tblname, qn->relations[0]->db);
        strcat(tblname, ".");
        strcat(tblname, qn->relations[0]->table_name);
        if(strlen(tblname) < 15)
          strcat(tblname, "               ");
        else
          strcat(tblname, "          ");
        strcat(tblname, qn->relations[1]->db);
        strcat(tblname, ".");
        strcat(tblname, qn->relations[1]->table_name);
        write_printf(p, tblname, "");
        write_printf(p, spacer, "     |                              |");
        write_printf(p, spacer, "     |   ----------------------------");
        write_printf(p, spacer, "     |   |");
        write_printf(p, spacer, "     V   V");
      }
      else
              strcpy(tblname, spacer);
        strcat(tblname, qn->relations[0]->db);
        strcat(tblname, ".");
        strcat(tblname, qn->relations[0]->table_name);
        write_printf(p, tblname, "");
        write_printf(p, spacer, "     |");
        write_printf(p, spacer, "     |");
        write_printf(p, spacer, "     |");
        write_printf(p, spacer, "     V");
      }
    }
    else if((qn->left != 0) && (qn->right != 0))
    {
      write_printf(p, spacer, "     |                              |");
      write_printf(p, spacer, "     |   ----------------------------");
      write_printf(p, spacer, "     |   |");
      write_printf(p, spacer, "     V   V");
    }
    else if((qn->left != 0) && (qn->right == 0))
    {
      write_printf(p, spacer, "     |");
      write_printf(p, spacer, "     |");
      write_printf(p, spacer, "     |");
      write_printf(p, spacer, "     V");
    }
    else if(qn->right != 0)
    {
    }
    write_printf(p, spacer, "-------------------");

    /* Write out the node type */
    switch(qn->node_type)
    {
    case Query_tree::qntProject:
      {
        write_printf(p, spacer, "|     PROJECT     |");
        write_printf(p, spacer, "-------------------");
        break;
      }
    case Query_tree::qntRestrict:
      {
        write_printf(p, spacer, "|    RESTRICT     |");
        write_printf(p, spacer, "-------------------");
        break;
      }
    case Query_tree::qntJoin:
      {
        write_printf(p, spacer, "|      JOIN       |");
        write_printf(p, spacer, "-------------------");
        break;
      }
    case Query_tree::qntDistinct:
      {
        write_printf(p, spacer, "|     DISTINCT    |");
        write_printf(p, spacer, "-------------------");
        break;
      }
    default:
      {        write_printf(p, spacer, "|      UNDEF      |");
        write_printf(p, spacer, "-------------------");
        break;
      }
    }
    write_printf(p, spacer, "| Access Method:  |");
    write_printf(p, spacer, "|    iterator     |");
    write_printf(p, spacer, "-------------------");
    if(qn == root)
    {
      write_printf(p, spacer, "        |");
      write_printf(p, spacer, "        |");
      write_printf(p, spacer, "        V");
      write_printf(p, spacer, "    Result Set");
    }
  }
  my_free(spacer);
  my_free(tblname);
  DBUG_RETURN(0);
}

您需要做的最后一件事是添加代码来执行 DBXP EXPLAIN命令,调用show_plan()方法,并将结果返回给客户端。清单 12-26 显示了这个函数的完整代码。注意,在这个函数中,我构建了查询树,然后使用名为“执行路径”的单字符字符串列创建了一个字段列表,然后调用show_plan将计划写入客户端。

清单 12-26DBXP 解释命令源代码

/*
  Perform EXPLAIN command.

  SYNOPSIS
    DBXP_explain_select_command()
    THD *thd            IN the current thread

  DESCRIPTION
    This method executes the EXPLAIN SELECT command.

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int DBXP_explain_select_command(THD *thd)
{
  DBUG_ENTER("DBXP_explain_select_command");
  Query_tree *qt = build_query_tree(thd, thd->lex,
                                    (TABLE_LIST*) thd->lex->select_lex.table_list.first);
  List<Item> field_list;
  Protocol *protocol= thd->protocol;
  field_list.push_back(new Item_empty_string("Execution Path",NAME_LEN));
  if (protocol->send_result_set_metadata(&field_list,
                            Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF))
    DBUG_RETURN(TRUE);
  protocol->prepare_for_resend();
  show_plan(protocol, qt->root, qt->root, false);
  my_eof(thd);
  DBUG_RETURN(0);
}

现在,让我们编译服务器并用测试文件试一试。

测试 DBXP 解释命令

与前面的测试一样,您可以使用前面章节中描述的测试,或者在 MySQL 命令行客户端中输入一个有效的 SQL 命令。清单 12-27 展示了一个查询执行路径的例子。在这一点上应该说明的是,查询没有经过优化,将显示为单个节点。一旦你添加了优化器(见第 11 章),查询执行路径将反映输入的查询语句的适当执行。

清单 12-27DBXP 解释测试的结果

mysql> EXPLAIN DBXP_SELECT * FROM customer;

+--------------------------+
| Execution Path           |
+--------------------------+
|      test.customer       |
|           |              |
|           |              |
|           |              |
|           V              |
|      ------------------- |
|      |     PROJECT     | |
|      ------------------- |
|      | Access Method:  | |
|      |    iterator     | |
|      ------------------- |
|              |           |
|              |           |
|              V           |
|          Result Set      |
+--------------------------+
15 rows in set (0.00 sec)

mysql>

这比枯燥的罗列事实有趣得多。在 DBXP 项目的这个阶段添加EXPLAIN命令允许您观察和诊断优化器是如何形成查询树的。当你开始自己的实验时,你会发现这很有帮助。

如果到目前为止您还没有这样做,那么您应该运行完整的测试来测试本章中介绍的代码的所有三个部分。

摘要

我在本章中介绍了一些更复杂的数据库内部技术。您了解了查询是如何在 MySQL 服务器内部表示的,因为它们是通过“大开关”进行解析和处理的。更重要的是,您发现了如何使用 MySQL 通过查询树类进行自己的数据库内部实验。了解这些技术会让您更好地理解 MySQL 内部组件是如何构建的。

在下一章中,我将通过一个查询树优化策略的示例实现向您展示更多关于内部查询表示的内容。如果您想知道为关系数据库系统构建一个优化器需要什么,下一章将向您展示一个使用查询树类的启发式查询优化器的例子。

练习

下面的列表代表了您可能希望作为实验(或作为课堂作业)来探索关系数据库技术的活动类型。

  1. 图 12-1 中的查询暴露了一个表格中的设计缺陷。这是什么?该缺陷是否违反了任何标准形式?如果有,是哪一个?
  2. 浏览TABLE结构并更改DBXP_SELECT存根以返回关于表及其字段的信息。
  3. 更改EXPLAIN DBXP_SELECT命令,产生类似于 MySQL EXPLAIN SELECT命令的输出。
  4. 修改build_query_tree函数来识别和处理LIMIT子句。
  5. 如何改变查询树query_node的结构以容纳HAVINGGROUP BYORDER子句?

奇怪的是,很少有文献对符号的选择做出解释。传统上,θ代表θ连接,image代表自然连接,但大多数文本互换了这些概念,导致所有连接都使用一个或另一个符号(有时两个都用)来表示。

2 你能发现经常被滥用的特征吗?提示:学期属性的定义域是什么?对列中的数据进行编码违反了哪条规则?

3 有些人会说这不应该,因为 MySQL 的内部结构是用来为优化器组织数据的。另一方面,查询树被设计为就地优化和执行。

4 虽然许多关于查询处理主题的文章对如何区分每个过程有不同的看法,但他们都同意必须出现某些不同的过程步骤。

5 一种被克努特和其他算法大师极力劝阻的做法。

6 无论如何,像命令行界面那样图形化是允许的。

十三、查询优化

第 12 章中显示的查询树类是为 DBXP 构建实验性查询优化和执行引擎的起点。在本章中,我将向您展示如何将优化器添加到查询树类中。我首先解释优化器中使用的试探法(或规则)的基本原理,然后开始编写代码。因为一些函数的代码很长,所以本章中的例子是摘录的。如果您正在编写示例代码,请下载本章的源代码,而不是从头开始输入代码。

查询优化器的类型

第一批查询优化器是为早期的数据库系统设计的,比如 System R 1 和 INGRES 。 2 这些优化器是为关系模型的特定实现而开发的,它们经受住了时间的考验,是如何实现优化器的例证。许多商业上可用的数据库系统都是基于这些工作。从那时起,优化器已经被创建用于关系模型的扩展,以包括面向对象和分布式数据库系统。

一个例子是火山优化器 ,它使用动态编程算法 3 在面向对象的数据库系统中生成基于成本优化的查询计划。另一个例子是关于如何在异构数据库系统中执行优化(类似于分布式系统,但是没有公共共享的组织概念)。在这些环境中,可以使用统计方法来推导优化策略。

查询优化需求产生独特需求的另一个领域是内存数据库系统。这些系统被设计成包含整个系统和计算机辅助存储器(即磁盘)中的所有数据。虽然这些应用中的大多数是作为嵌入式系统实现的,但是一些由系统集合组成的大型分布式系统使用内存数据库来加速内存数据库系统中的信息检索优化,这需要高效的算法,因为与处理查询本身的需要相比,优化检索的需要是微不足道的。4

所有对传统和非传统优化的研究都是基于系统优化器的领域。System R optimizer 是一个基于成本的优化器,它使用收集到的关于数据库和数据的信息,或者统计数据,来形成对查询如何执行的成本估计。此外,将查询的内部表示安排成不同但等价的内部表示(它们生成相同的答案)的概念提供了一种存储替代形式的机制。这些可选形式中的每一种都被称为一个查询计划 。成本最低的计划被选为执行查询的最有效方式。

系统 R 工作中确定的一个关键特性是选择性的概念——基于包含属性及其值的引用的表达式的评估来预测结果。选择性是决定合取选择中的简单表达式测试顺序的核心。最具选择性的表达式(即具有最小选择性的表达式)将检索最少数量的元组(行)。因此,该表达式应该是查询中第一个操作的基础。合取选择可以被认为是“交集”条件。相反,析取选择是“联合”条件。在析取条件中,顺序没有影响。

某些查询优化器,比如 System R,并不处理所有可能的连接顺序。相反,它们将搜索限制在某些类型的连接顺序上,已知这些连接顺序会产生更有效的执行。例如,可以对多路连接进行排序,以便首先执行生成最少可能结果的条件。类似地,System R 优化器只考虑那些连接顺序,其中每个连接的右操作数是初始关系之一。这种连接顺序称为左深连接顺序。左深连接顺序对于流水线执行特别方便,因为右操作数通常是一个关系(相对于中间关系),因此每个连接只有一个输入被流水线化。管道的使用是数据库实验项目的优化器和执行引擎的一个关键元素。

基于成本的优化器

基于成本的优化器通过使用等价规则从给定的查询生成一系列查询评估计划,并根据收集的关于执行查询所需的关系和操作的度量(或统计)选择成本最低的计划。对于一个复杂的查询,许多等价的计划是可能的。

基于成本的优化的目标是利用从过去的查询中收集的索引和统计信息来安排查询执行和表访问。微软 SQL Server 和甲骨文等系统使用基于成本的优化器。

数据库系统中负责获取和处理统计数据(以及许多其他实用功能)的部分被称为数据库目录 。该目录维护关于引用关系和每个引用关系上可用的访问路径的统计信息。这些将在后面的访问路径选择中使用,以选择最有效的计划(成本最低)。例如,System R 为以下每个表维护统计信息:

  • 每个关系的基数

  • 段中包含每个关系的元组的页数

  • 段中包含关系元组(阻塞因子或填充)的数据页的比例

  • 对于每个索引:

  • 每个索引中不同键的数量

  • 每个索引中的页数

这些统计数据来自系统内的几个来源。加载关系和创建索引时会创建统计信息。然后由用户命令 5 定期更新,该命令可由任何用户运行。System R 不会实时更新这些统计数据,因为这会在系统编目中产生额外的数据库操作和锁定瓶颈。统计数据的动态更新倾向于对修改关系内容的访问进行串行化,从而限制了系统在多用户环境中处理同时查询的能力。

在基于成本的优化中使用统计数据并不复杂。大多数受访的数据库专业人员似乎认为统计数据的收集和应用是查询优化的一个复杂而重要的元素。尽管基于成本的查询优化甚至混合优化方案使用成本和/或排名的统计,但优化方案既不复杂也不关键。例如,在属性中均匀分布值的概念。这个概念本身就证明了统计学应用的不精确性。统计计算在本质上很大程度上是分类的,它们不是为了生成精确的值而设计的。它们只是帮助确定一个查询执行计划是否通常比另一个更昂贵。

属性值的频率分布是预测查询结果大小的常用方法。通过形成属性的可能(或实际 6 )值的分布,数据库系统可以使用该分布,通过预测计划必须处理的元组(或行)的数量来计算给定查询计划的成本。然而,现代数据库系统只处理单个属性的频率分布,因为考虑所有可能的属性组合是非常昂贵的。这实质上对应于所谓的属性值独立性假设,尽管这很少是真的,但几乎所有的关系数据库系统都采用这种假设。

收集分布数据需要不断更新统计数据或对数据进行预测分析。另一个策略是使用均匀分布,其中属性值的分布被假设为对于所有不同的值都是相等的。例如,给定 5000 个元组和给定属性的 50 个可能值,均匀分布假设每个值表示 100 次。这种情况很少发生,而且通常是不正确的。尽管没有任何统计数据,但在许多情况下,这仍然是现实的合理近似值。

在最坏的情况下,动态编程的内存需求和运行时间随着查询大小(即连接的数量)呈索引增长,因为在每个步骤中生成的所有可行的部分计划都必须存储起来,以便在下一个步骤中使用。事实上,许多现代系统对可以提交的查询的大小有限制(通常在 15 个连接左右),因为对于较大的查询,优化器会由于非常高的内存需求而崩溃。然而,实践中看到的大多数查询包含的连接少于 10 个,并且该算法已被证明在这种情况下是有效的。它被认为是查询优化搜索策略的标准。收集的关于表(或关系)中的行(或元组)的统计数据包括:

  • 表中元组的数量
  • 包含行的块数(块数)
  • 以字节为单位的行的大小
  • 每个属性(或列)的不同值的数量
  • 每个属性的选择基数(有时表示为均匀分布)
  • 索引内部节点的扇出(产生子树的子节点数)
  • 索引的 B 树的高度
  • 索引叶级别的块数

将操作的最终结果写回磁盘的开销被忽略。不管使用什么样的查询评估计划,这个成本都不会改变;因此,不将其包括在计算中并不影响计划的选择。

今天大多数数据库系统使用一种形式的动态编程来生成所有可能的查询计划。虽然动态编程为成本优化提供了良好的性能,但它是一种复杂的算法,对于更复杂的查询可能需要更多的资源。虽然大多数数据库系统不会遇到这些类型的查询,但分布式数据库系统和高性能计算领域的研究人员已经探索了动态编程技术的替代方案和变体。Kossmann 和 Stocker 最近的研究表明,我们开始看到传统查询优化方法的局限性。 7 我们需要的是更高效的优化技术,这些技术能够生成遵循良好实践的执行计划,而不是穷尽式的探索。换句话说,我们需要在各种通用环境中表现良好的优化器,也需要在独特的数据库环境中表现良好的优化器。

启发式优化器

启发式优化的目标是应用确保查询执行良好实践的规则。使用启发式优化器的系统包括 INGRES 和各种学术变体。大多数系统通常使用启发式优化作为避免真正糟糕的计划的手段,而不是作为优化的主要手段。

启发式优化器在选择替代实现之前,使用关于如何将查询塑造成最佳形式的规则。试探法或规则的应用可以消除可能低效的查询。使用试探法作为形成查询计划的基础确保了查询计划在评估之前最有可能(但不总是)得到优化。这种试探法包括:

  • 尽可能早地执行选择操作。通常最好在投影之前执行选择,因为这样可以减少沿树向上发送的元组数量。
  • 尽早进行预测。
  • 确定哪些选择操作和连接操作产生最小的结果集,并首先使用这些操作(最左边的深度)。
  • 用连接操作替换笛卡尔积。
  • 在树中尽可能向下移动投影属性。
  • 识别其操作可以被流水线化的子树。

启发式优化器不是新技术。研究人员已经为各种专门目的创建了基于规则的优化器。一个例子是基于 Prairie 规则的查询优化器。这个基于规则的优化器允许根据给定的语言符号创建规则。使用控制优化器执行方式的规则来处理查询。在这种情况下,Prairie 优化器主要是一个基于成本的优化器,它使用规则来调整优化器。

除了 Prairie 和 INGRES 等早期原语之外,没有任何商业数据库系统实现纯粹的启发式优化器。对于那些具有启发式或基于规则的优化步骤的优化,它通常作为经典的基于成本的优化器的附加或预处理器来实现,或者作为优化中的预处理步骤来实现。

语义优化器

语义优化的目标是形成查询执行计划,该计划使用数据库的语义或拓扑以及其中的关系和索引来形成查询,以确保在给定数据库中执行查询的最佳实践。语义查询优化使用模式的知识(例如,完整性约束)来将查询转换成可以比原始版本更有效地回答的形式。

虽然还没有在商业数据库系统中作为主要的优化技术来实现,但是语义优化目前是大量研究的焦点。语义优化的前提是优化器对实际的数据库模式有基本的了解。当提交一个查询时,优化器使用它对系统约束的了解来简化或忽略一个特定的查询,如果它保证返回一个空的结果集的话。这项技术很有希望在未来的关系数据库系统中进一步提高查询处理效率。

参数优化器

Ioannidis 在他关于参数化查询优化的工作中描述了一种查询优化方法,该方法将启发式方法的应用与基于成本的优化相结合。生成的查询优化器提供了一种方法来生成一个较小的有效查询计划集,根据该计划可以估计成本,从而可以执行该计划集中成本最低的计划。 8 查询计划生成是使用一种随机算法创建的,称为 sipR。这允许利用参数查询优化的系统选择可以包括参数变化的不确定性(例如缓冲区大小)的查询计划,以选择在运行中或从存储中形成的最佳计划。

有趣的是,在他的工作中,Ioannidis 认为可能不需要使用动态编程算法,因此可以避免使用这些技术的开销。此外,他发现,在应用动态编程算法进行查询优化之前,使用试探法来修剪或调整查询的数据库系统通常是 System R 的原始算法的增强版本。Ioannidis 表明,对于小型查询(大约多达 10 个连接),动态编程优于随机化算法,而对于大型查询,情况正好相反。

启发式优化再探

启发式优化过程 使用一组已定义的规则来保证良好的执行计划。因此,启发式优化器产生良好计划的有效性仅仅基于其规则的有效性和完整性。

以下段落描述了用于创建 DBXP 查询优化器的规则。尽管这些规则非常基本,但是当它们应用于典型的查询时,最终的执行是接近最优的,具有快速的性能和准确的结果。

最初使用一些基本策略来构建查询树。具体来说,所有的执行都发生在查询树节点中。限制和投影是在分支上处理的,不会生成中间关系。连接总是作为两条路径的交集来处理。多路连接将使用一系列双向连接来形成。以下规则代表了形成一组启发式规则以生成良好的执行计划的最佳实践。DBXP 优化器被设计来应用这些规则,以便将查询树转换成确保高效执行的形式。 9

  1. 拆分包含项目的任何节点并连接或限制并连接。这一步是必要的,因为一些查询在WHERE子句 10 中指定了连接条件,从而可以“欺骗”优化器形成连接节点,这些节点的表达式部分不属于连接条件。
  2. 把所有的限制都推到树叶上。表达式根据它们各自的关系被分组到单独的查询树节点中。虽然有些复杂的表达式不能简化,但大多数可以很容易地简化为一个单一的关系式。通过在叶子上设置限制,减少了必须沿树向上传递的结果元组的数量。
  3. 将所有投影放置在树的最低点。投影应该放在高于限制的节点中,并且它们将通过从结果元组中消除不需要的属性来进一步减少通过树传递的数据量。应当注意,可以修改投影以包括操作所需的属性,例如驻留在投影查询树节点的父代中的连接。
  4. 将所有连接放在 join 子句中包含的关系的投影或限制的交叉点上。 11 这确保了对最昂贵的操作——连接——评估最少量的元组。对来自子节点的结果元组进行排序的中间查询树节点可能是必要的。这些中间节点称为实用操作,可以根据连接的类型对元组进行排序或分组,它们可以大大提高连接的性能。

image 注意可以使用其他启发式方法。前面的列表包含那些产生最大性能增益的。

Lee、Shih 和 Chen 对将选择和限制推下树的做法给出了一个有趣的反驳。 12 他们提出,在某些条件下,有些选择和投射可能比联结的代价更大。他们的论点提出了一种基于图论的查询优化器,可以更准确地预测复杂选择和投影情况下的查询优化。然而,一般情况下,可以使用我列出的规则为大多数查询构建“高效”的执行计划。

DBXP 查询优化器

尽管这些规则为形成最佳查询树提供了一整套操作,但它们并没有解决平衡多路连接或应用索引的问题。这些步骤被认为是基于成本的优化。由于这个原因,大多数启发式优化器被实现为两阶段优化,第一阶段生成优化的查询路径,第二阶段应用成本优化策略。

image 注意 DBXP 优化器是作为两遍操作实现的。第一个操作使用启发式算法重新排列树以供执行。第二遍遍历树,改变与被操作的属性上可用的索引有关系的节点的访问方法。我将成本优化过程的实现留给读者作为练习。

为启发式优化器创建全面的测试需要编写涵盖优化器中所有可能路径的 SQL 语句。本质上,您需要创建一个测试来测试所有可能的查询,包括有效的和无效的(无效的查询通常在 SQL 解析器代码中被捕获)。然而,实现启发式优化器只是 DBXP 引擎的第二部分。在前一章中,我们创建了基本的查询树内部表示,并删除了执行方法。在本章中,我们将创建优化器,但不能执行查询。您可以继续使用存根执行来测试优化器,但是您可以重用前一章中的代码来显示查询计划而不是查询结果,而不是显示查询结果。

记住这一点,让我们设计几个基本的查询来测试优化器,以显示它正在处理这些查询。我们将在下一章关注查询的执行。清单 13-1 展示了一个测试查询优化器的示例。

清单 13-1。 示例 DBXP 查询优化器测试(Ch13.test)

#
# Sample test to test the DBXP_SELECT optimizer
#

# Test 1:
DBXP_SELECT * FROM staff;

# Test 2:
DBXP_SELECT id FROM staff WHERE staff.id = '123456789';

# Test 3:
DBXP_SELECT id, dir_name FROM staff, directorate
WHERE staff.dno = directorate.dnumber;

# Test 4:
DBXP_SELECT * FROM staff JOIN tasking ON staff.id = tasking.id
WHERE staff.id = '123456789';

image 提示这些例子中使用的数据库包含在附录中。

您可以使用这个测试作为指南,并添加您自己的命令来探索新代码。有关如何使用 MySQL 测试套件创建和运行该测试的更多详细信息,请参考第 4 章

清除 DBXP_SELECT 命令

由于没有查询执行功能,查询命令可以优化,但不能执行。show plan 机制(EXPLAIN命令)可以作为演示优化器的一种手段。要添加这个功能,您可以打开sql_dbxp_parse.cc文件并改变DBXP_select_command()方法,如清单 13-2 所示。

清单 13-2。 对查询优化器进行测试

int DBXP_explain_select_command(THD *thd);

/*
  Perform DBXP_SELECT Command

  SYNOPSIS
    DBXP_select_command()
    THD *thd            IN the current thread

  DESCRIPTION
    This method executes the SELECT command using the query tree and optimizer.

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int DBXP_select_command(THD *thd)
{
  DBUG_ENTER("DBXP_select_command");
  DBXP_explain_select_command(thd);
  DBUG_RETURN(0);
}

这些变化改变了调用EXPLAIN命令代码的代码,而不是执行查询。这允许测试返回有效的结果集(查询计划),这样我们就可以在没有查询执行部分的情况下测试优化器。

image 注意我在DBXP_select_command()方法上面用了一个函数声明。这允许代码在不使用头文件的情况下向前调用DBXP_explain_select_command()方法。

还有一个对DBXP_explain_select_command()方法的必要改变。您需要添加对新优化方法的调用。这包括heuristic_optimization()cost_optimization()方法。我将在接下来的小节中更详细地讨论启发式优化。清单 13-3 显示了对EXPLAIN代码的修改。

清单 13-3。 修改解释命令代码

/*
  Perform EXPLAIN command.

  SYNOPSIS
    DBXP_explain_select_command()
    THD *thd            IN the current thread

  DESCRIPTION
    This method executes the EXPLAIN SELECT command.

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int DBXP_explain_select_command(THD *thd)
{
  bool res= 0;

  DBUG_ENTER("DBXP_explain_select_command");

  /* Prepare the tables (check access, locks) */
  res = check_table_access(thd, SELECT_ACL, thd->lex->query_tables, 0, 1, 1);
  if (res)
    DBUG_RETURN(1);
  res = open_and_lock_tables(thd, thd->lex->query_tables, 0,
                             MYSQL_LOCK_IGNORE_TIMEOUT);
  if (res)
    DBUG_RETURN(1);

  /* Create the query tree and optimize it */
  Query_tree *qt = build_query_tree(thd, thd->lex,
           (TABLE_LIST*) thd->lex->select_lex.table_list.first);
  qt->heuristic_optimization();
  qt->cost_optimization();

  /* create a field list for returning the query plan */
  List<Item> field_list;

  /* use the protocol class to communicate to client */
  Protocol *protocol= thd->protocol;

  /* write the field to the client */
  field_list.push_back(new Item_empty_string("Execution Path",NAME_LEN));
  if (protocol->send_result_set_metadata(&field_list,
                            Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF))
    DBUG_RETURN(TRUE);
  protocol->prepare_for_resend();

  /* generate the query plan and send it to client */
  show_plan(protocol, qt->root, qt->root, false);
  my_eof(thd); /* end of file tells client no more data is coming */

  /* unlock tables and cleanup memory */
  mysql_unlock_read_tables(thd, thd->lock);
  delete qt;
  DBUG_RETURN(0);
}

重要的 MySQL 结构和类

MySQL 源代码中有许多关键的结构和类。到目前为止,您已经在示例中看到了许多。一些更重要的问题记录在 MySQL 内部手册中。不幸的是,没有任何文件将它们全部列出。下面几节描述了在使用 DBXP 查询优化器(以及后面的查询执行代码)时会遇到的一些结构和类。这些包括TABLE结构、Field类和一些常见的Item迭代器(在第 3 章中讨论了Item类)。

表格结构

在编写优化器时,最重要的 MySQL 结构是TABLE结构。

这个结构很重要,因为它包含了一个表的所有相关数据。它包含所有内容,从指向适当存储处理程序类的指针,到执行查询时用于存储行的字段、键和临时缓冲区的列表。

虽然这个结构非常庞大(就像 MySQL 中最重要的结构一样),但是你会反复看到一些关键的属性。表 13-1 列出了TABLE结构的一些更重要的属性。关于TABLE结构的详细检查,参见handler.h文件。

表 13-1 。表结构概述

属性 描述
文件 对存储引擎对象的引用。
领域 表的字段数组。
田地(复数);场;域;字段 字段数组中的字段数。
然后 指向表列表中下一个表的指针。
上一个 指向表列表中上一个表的指针。

字段类

Field类包含创建、赋值和操作数据库中表的字段(或属性)的所有属性和方法。Field类在/sql/field.h文件中定义,在/sql/field.cc文件中实现。Field类实际上是一个基类,几种类型的字段都是从这个基类派生出来的。这些名为Field_XXX,的派生类可以在 MySQL 源代码的几个地方找到。

由于它只是一个基类, 13 类中的许多方法都打算被派生类覆盖(它们被定义为virtual)。然而,许多派生类具有相同的基本属性和方法。表 13-2 列出了你在使用 DBXP 源代码时会遇到的属性和方法。对于Field类的详细检查,参见field.h文件。

表 13-2 。字段类

属性/方法 描述
光电带读数机(photoelectric tape reader) 指向记录缓冲区中字段的指针。
空值 ptr 指向记录缓冲区中一个(或多个)字节的指针,指示哪些属性可以包含 NULL。
表名 与该字段关联的表名。
字段名称 该字段的属性名。
字段长度 字段的长度。指示可以存储的字节数。
is_null() 检查字段是否为空。
move_field() 将内存中字段的指针更改为指向不同的位置。
商店() 用于将值存储到字段中的一系列重载方法。
val_str() 以字符串形式获取字段的值。
val_int() 以整数形式获取字段的值。
结果类型() 获取字段的数据类型。
化学机械抛光() 返回字段与传递的值的比较结果。

迭代器

MySQL 源代码中有三种类型的迭代器。在前面的章节中,你已经看到了这些迭代器。迭代器是一种特殊的结构,它使得创建和浏览对象列表变得很容易,它们通常以链表或数组的形式出现。MySQL 中的迭代器是作为模板类实现的,它将列表操作的数据类型作为参数。MySQL 迭代器是链表,但有些更像队列和堆栈。以下部分描述了 MySQL 中一些可用的迭代器类。这些迭代器在sql/sql_list.h头文件中定义。

模板<>班级列表

List模板类被实现为一个队列或堆栈,使用push_back()方法将项目推到列表的后面,或者使用push_front()方法将项目推到列表的前面。可以使用pop()方法检索项目,或者使用remove()方法删除项目。您可以通过使用数据项的next属性来循环遍历列表,但是列表通常用于形成项目的链表(例如List<Item> item_list),然后使用List_iterator类中的一个来快速循环遍历列表。这个类从base_list类派生而来(也在/sql/sql_list.h中定义)。

模板<>类列表 _ 迭代器

List_iterator类被实现为一个链表,该链表具有使用重载的++操作符在列表中移动的方法。可以使用ref()方法检索项目,或者使用remove()方法删除项目。可以通过发出rewind()方法从前面重新开始列表。该类从base_list类派生而来(也在/sql/sql_list.h中定义)。

模板<>类列表 _ 迭代器 _ 快速

List_iterator_fast类本质上与List_iterator类相同,但它是为快速向前遍历而优化的。它被实现为一个链表,并带有使用重载的++操作符在列表中移动的方法。可以使用ref()方法检索项目,或者使用remove()方法删除项目。该类从base_list类派生而来(也在/sql/sql_list.h中定义)。

例子

使用迭代器很容易。如果你想使用一个列表来操作条目,一个简单的列表,比如List<Item_field>, 将是最好的选择。如果想快速遍历字段列表,可以创建一个列表迭代器作为List_iterator<Item_field>List_iterator_fast<Item_field>。循环结构的例子如清单 13-4 所示。

清单 13-4。 示例迭代器

  /* create a list and populate with some items */
  List<Item> item_list;
  item_list.push_back(new Item_int((int32)
             join->select_lex->select_number));
  item_list.push_back(new Item_string(join->select_lex->type,
    strlen(join->select_lex->type), cs));
  item_list.push_back(new Item_string(message,strlen(message),cs));

../* start a basic list iterator to iterate through the item_list */
  List_iterator<Item_field> item_list_it(*item_list);

  /* control the iteration using an offset */
  while ((curr_item= item_list_it++))
  {
    /* do something */
  }

../* start a fast list iterator to iterate through the item_list */
  List_iterator_fast<Item_field> li(item_equal->fields);

  /* control the iteration using an offset */
  while ((item= li++))
  {
    /* do something */
  }

DBXP 助手类

我在第 11 章中提到了 DBXP 引擎中使用的两个额外的类(属性和表达式)。这些类旨在使优化器更容易编码和理解。它们是现有 MySQL 类(和结构)的封装,并且重用了 MySQL 代码中的许多可用方法。

第一个助手类是封装查询中使用的属性的类。这些属性在 MySQL 代码中表示为Item类。名为Attribute的助手类通过提供访问项目的公共接口,使得访问这些类变得更加容易。清单 13-5 显示了Attribute类的头文件。

清单 13-5。 属性类表头

#include "sql_priv.h"
#include "sql_class.h"
#include "table.h"

class Attribute
{
public:
  Attribute(void);
  int remove_attribute(int num);
  Item *get_attribute(int num);
  int add_attribute(bool append, Item *new_item);
  int num_attributes();
  int index_of(char *table, char *value);
  int hide_attribute(Item *item, bool hide);
  char *to_string();
private:
  List<Item> attr_list;
  bool hidden[256];
};

第二个助手类封装了查询中使用的表达式。像属性一样,表达式在 MySQL 代码中表示为Item类实例。名为Expression的助手类为Item类提供了一个公共(且简化的)接口。清单 13-6 显示了Expression类的头文件。

清单 13-6。 表达式类表头

#include "sql_priv.h"
#include "sql_class.h"
#include "table.h"
#include <sql_string.h>

struct expr_node
{
  Item      *left_op;
  Item      *operation;
  Item      *right_op;
  Item      *junction;
  expr_node *next;
};

class Expression
{
public:
  Expression(void);
  int remove_expression(int num, bool free);
  expr_node *get_expression(int num);
  int add_expression(bool append, expr_node *new_item);
  int num_expressions();
  int index_of(char *table, char *value);
  int reduce_expressions(TABLE *table);
  bool has_table(char *table);
  int convert(THD *thd, Item *mysql_expr);
  char *to_string();
  bool evaluate(TABLE *table 1);
  int compare_join(expr_node *expr, TABLE *t1, TABLE *t2);
  int get_join_expr(Expression *where_expr);
private:
  expr_node *root;
  Field *find_field(TABLE *tbl, char *name);
  bool compare(expr_node *expr, TABLE *t1);
  int num_expr;
};

我使用一个结构来包含左操作数、运算符和右操作数形式的表达式。这是一种比 MySQL 类表示的表达式树更简化的方法,使得阅读优化器代码更容易。更简单的方法也使得在交互式调试器中评估条件更容易。

image 注意我在正文中省略了这些助手类的一些细节,因为它们是调用用于TABLE结构的 MySQL 方法以及ItemField类的非常简单的抽象。然而,这些文件包含在在线章节源代码中。这本书的源代码可以在 http://www.apress.com的源代码部分下载。

这些助手类和头文件应该放在/sql目录中,并添加到 CMakeLists.txt 文件中。我将在“编译和测试代码”一节中向您展示如何做到这一点。

对现有代码的修改

实现优化器还需要一个小的修改:我们需要添加代码来使用新的AttributeExpression类。打开query_tree.h头文件,进行清单 13-7 所示的修改。正如你所看到的,我已经改变了where_exprjoin_expr属性来使用新的Expression类。同样,我更改了attributes属性以使用新的Attribute类。

清单 13-7。 对查询树类的修改

#include "attribute.h"
#include "expression.h"
#include "sql_priv.h"
#include "sql_class.h"
#include "table.h"
#include "records.h"

const int MAXNODETABLES = 4;
const int LEFTCHILD = 0;
const int RIGHTCHILD = 1;

class Query_tree
{
public:
  enum query_node_type          //this enumeration lists the available
  {                              //query node (operations)
    qntUndefined = 0,
    qntRestrict = 1,
    qntProject = 2,
    qntJoin = 3,
    qntSort = 4,
    qntDistinct = 5
  };

  enum join_con_type            //this enumeration lists the available
  {                              //join operations supported
    jcUN = 0,
    jcNA = 1,
    jcON = 2,
    jcUS = 3
  };

  enum type_join                //this enumeration lists the available
  {                              //join types supported.
    jnUNKNOWN      = 0,          //undefined
    jnINNER        = 1,
    jnLEFTOUTER    = 2,
    jnRIGHTOUTER   = 3,
    jnFULLOUTER    = 4,
    jnCROSSPRODUCT = 5,
    jnUNION        = 6,
    jnINTERSECT    = 7
  };

    enum AggregateType          //used to add aggregate functions
    {
        atNONE      = 0,
        atCOUNT     = 1
    };

  /*
    STRUCTURE query_node

    DESCRIPTION
      This this structure contains all of the data for a query node:

      NodeId -- the internal id number for a node
      ParentNodeId -- the internal id for the parent node (used for insert)
      SubQuery -- is this the start of a subquery?
      Child -- is this a Left or Right child of the parent?
      NodeType -- synonymous with operation type
      JoinType -- if a join, this is the join operation
      join_con_type -- if this is a join, this is the "on" condition
      Expressions -- the expressions from the "where" clause for this node
      Join Expressions -- the join expressions from the "join" clause(s)
      Relations[] -- the relations for this operation (at most 2)
      PreemptPipeline -- does the pipeline need to be halted for a sort?
      Fields -- the attributes for the result set of this operation
      Left -- a pointer to the left child node
      Right -- a pointer to the right child node
*/
  struct query_node
  {
    query_node();
    ∼query_node();
    int                 nodeid;
    int                 parent_nodeid;
    bool                sub_query;
    int                 child;
    query_node_type     node_type;
    type_join           join_type;
    join_con_type       join_cond;
    Expression          *where_expr;
    Expression          *join_expr;
    TABLE_LIST          *relations[MAXNODETABLES];
    int                 eof[MAXNODETABLES];
    int                 ndx[MAXNODETABLES];
    bool                preempt_pipeline;
    Attribute           *attributes;
    query_node          *left;
    query_node          *right;
  };

  struct record_buff
  {
    uchar *field_ptr;
    long field_length;
    record_buff *next;
    record_buff *prev;
    READ_RECORD *record;
  };

许多方法也需要添加到查询树类中。我没有描述每个方法及其实现的细节,而是在清单 13-8 中包含了查询树定义的其余部分。这段代码也被添加到query_tree.h文件中。

清单 13-8。 查询-树类的新方法

  query_node *root;              //The ROOT node of the tree

  Query_tree(void);
  ∼Query_tree(void);
  int init_node(query_node *qn);
  int heuristic_optimization();
  int cost_optimization();
  int insert_attribute(query_node *qn, Item *c);
  bool distinct;
  int prepare(query_node *qn);
  int cleanup(query_node *qn);
  bool Eof(query_node *qn);
  READ_RECORD *get_next(query_node *qn);
  List <Item> result_fields;

private:
  bool h_opt;              //has query been optimized (rules)?
  bool c_opt;              //has query been optimized (cost)?
  READ_RECORD *lbuff;
  READ_RECORD *rbuff;
  record_buff *left_record_buff;
  record_buff *right_record_buff;
  record_buff *left_record_buffer_ptr;
  record_buff *right_record_buffer_ptr;

  int push_projections(query_node *qn, query_node *pNode);
  query_node *find_projection(query_node *qn);
  bool is_leaf(query_node *qn);
  bool has_relation(query_node *qn, char *Table);
  bool has_attribute(query_node *qn, Item *a);
  int del_attribute(query_node *qn, Item *a);
  int push_restrictions(query_node *qn, query_node *pNode);
  query_node *find_restriction(query_node *qn);
  query_node *find_join(query_node *qn);
  int push_joins(query_node *qn, query_node *pNode);
  int prune_tree(query_node *prev, query_node *cur_node);
  int balance_joins(query_node *qn);
  int split_restrict_with_project(query_node *qn);
  int split_restrict_with_join(query_node *qn);
  int split_project_with_join(query_node *qn);
  bool find_table_in_tree(query_node *qn, char *tbl);
  bool find_table_in_expr(Expression *expr, char *tbl);
  bool find_attr_in_expr(Expression *expr, char *tbl, char *value);
  int apply_indexes(query_node *qn);
  bool do_restrict(query_node *qn, READ_RECORD *t);
  READ_RECORD *do_project(query_node *qn, READ_RECORD *t);
  READ_RECORD *do_join(query_node *qn);
  int find_index_in_expr(Expression *e, char *tbl);
  TABLE *get_table(query_node *qn);
  int insertion_sort(bool left, Field *field, READ_RECORD *rcd);
  int check_rollback(record_buff *cur_left, record_buff *curr_left_prev,
    record_buff *cur_right, record_buff *cur_right_prev);
};

注意有几个公共方法,包括heuristic_optimization()cost_optimization()。我还添加了一个公共属性distinct,,你可以用它来帮助实现不同的操作(参见本章末尾的练习)。其余的方法是优化代码的助手方法。我解释了一些更有趣的,并把平凡的留给你去探索。

现在我们有了一些助手类来使优化器更容易实现,我们需要将它们合并到将 MySQL 内部查询表示转换为 DBXP 查询树的转换代码中。打开sql_dbxp_parse.cc文件,定位build_query_tree()方法 。清单 13-9 显示了添加新的AttributeExpression类所必需的改变。

清单 13-9。 对构建-查询-树方法的修改


/*
  Build Query Tree

  SYNOPSIS
    build_query_tree()
    THD *thd            IN the current thread
    LEX *lex            IN the pointer to the current parsed structure
    TABLE_LIST *tables  IN the list of tables identified in the query

  DESCRIPTION
    This method returns a converted MySQL internal representation (IR) of a
    query as a query_tree.

  RETURN VALUE
    Success = Query_tree * -- the root of the new query tree.
    Failed = NULL
*/
Query_tree *build_query_tree(THD *thd, LEX *lex, TABLE_LIST *tables)
{
  DBUG_ENTER("build_query_tree");
  Query_tree *qt = new Query_tree();
  Query_tree::query_node *qn =
    (Query_tree::query_node *)my_malloc(sizeof(Query_tree::query_node),
    MYF(MY_ZEROFILL | MY_WME));
  TABLE_LIST *table;
  int i = 0;
  Item *w;
  int num_tables = 0;

  /* create a new restrict node */
  qn->parent_nodeid = −1;
  qn->child = false;
  qn->join_type = (Query_tree::type_join) 0;
  qn->nodeid = 0;
  qn->node_type = (Query_tree::query_node_type) 2;
  qn->left = NULL;
  qn->right = NULL;
  qn->attributes = new Attribute();
  qn->where_expr = new Expression();
  qn->join_expr = new Expression();

  /* Get the tables (relations) */
  i = 0;
  for(table = tables; table; table = table->next_local)
  {
    num_tables++;
    qn->relations[i] = table;
    i++;
  }

  /* prepare the fields (find associated tables) for query */
  List <Item> all_fields;
  Name_resolution_context context;
  List_iterator <Item> it(thd->lex->select_lex.item_list);
  it++;
  if (lex->select_lex.with_wild)
  {
    bool found = FALSE;
    Field_iterator_table_ref field_iterator;
    for(table = tables; table; table = table->next_local)
    {
      field_iterator.set(table);
      for (; !field_iterator.end_of_fields(); field_iterator.next())
      {
        Item *item= field_iterator.create_item(thd);
        if (!found)
        {
          found= TRUE;
          it.replace(item); /* Replace '*' with the first found item. */
        }
        else
        {
          it.after(item);   /* Add 'item' to the SELECT list. */
        }
      }
    }
  }
if (setup_fields(thd, lex->select_lex.ref_pointer_array,
                   lex->select_lex.item_list, thd->mark_used_columns,
                   &all_fields, 1))
    DBUG_RETURN(NULL);
  qt->result_fields = lex->select_lex.item_list;

  /* get the attributes from the raw query */
  w = lex->select_lex.item_list.pop();
  while (w != 0)
  {
    uint unused_field_idx= NO_CACHED_FIELD_INDEX;
    TABLE_LIST *dummy;
    Field *f = NULL;
    for(table = tables; table; table = table->next_local)
    {
      f = find_field_in_table_ref(thd, table, ((Field *)w)->field_name,
                                  strlen(((Field *)w)->field_name),
                                  ((Field *)w)->field_name, NULL, NULL, NULL,
                                  FALSE, FALSE, &unused_field_idx, FALSE,
                                  &dummy);
      if (f)
      {
        qn->attributes->add_attribute(true, (Item *)f);
        break;
      }
    }
    w = lex->select_lex.item_list.pop();
  }

  /* get the joins from the raw query */
  if (num_tables > 0)  //indicates more than 1 table processed
    for(table = tables; table; table = table->next_local)
    {
      if (table->join_cond() != 0)
        qn->join_expr->convert(thd, (Item *)table->join_cond());
    }

  /* get the expressions for the where clause */
  qn->where_expr->convert(thd, lex->select_lex.where);

  /* get the join conditions for the joins */
  qn->join_expr->get_join_expr(qn->where_expr);

  /* if there is a where clause, set node to restrict */
  if (qn->where_expr->num_expressions() > 0)
    qn->node_type = (Query_tree::query_node_type) 1;

  qt->root = qn;
  DBUG_RETURN(qt);
}

此时,需要调整包含文件,以确保我们包含了编译代码所需的所有内容。例如,以下语句出现在query_tree.cc文件的顶部:

#include "query_tree.h"

query_tree.h 头文件包括属性和表达式头文件,以及所需的 MySQL includes 文件,如下所示。

#include "query_tree.h"
#include "sql_base.h"
#include "sql_acl.h"
#include "sql_parse.h"
#include "lock.h"

image 注意如果在编译时遇到奇怪的错误,请检查您的 CMakeLists.txt 文件中没有包含attributeexpressionquery_tree头文件。编译器将按照 include 指令自动包含这些文件。

启发式优化器的详细信息

启发式优化器是使用前面描述的规则模型实现的。启发式优化器中使用的每个方法都实现一些或所有规则。这些方法在表 13-3 中列出。

表 13-3 。启发式优化器中的启发式方法

方法 描述
split_restrict_with_join() 在树中搜索具有限制(有表达式)和连接表达式的节点。它将节点分为两个节点:一个用于限制,另一个用于连接。
split_project_with_join() 在树中搜索具有投影(具有属性)和连接表达式的节点。它将节点分为两个节点:一个用于投影,一个用于连接。
split _ restrict _ with _ project() 在树中搜索具有限制(有表达式)和投影(有属性)的节点。它将节点分为两个节点:一个用于限制,另一个用于投影。
find_restriction() 在树中搜索不在叶节点上的限制节点。
push_restrictions() 将限制沿树向下推至尽可能低的节点。它寻找限制可以驻留在叶子上的情况。这个方法在一个循环中与 find_restrictions()一起使用(当没有找到更多已经不在叶子上的限制时,循环结束)。
find_projection() 在树中搜索不在叶子上的投影节点。
push_projections() 将投影沿树向下推至可能的最低节点。它寻找投影可以驻留在叶子上或作为限制的父对象的情况。此方法在循环中与 find_projections()一起使用(当没有发现更多已经不在叶或作为限制的叶的父级的投影时,循环结束)。
find_join() 在树中搜索联接节点。
push_joins() 将联接沿树向下推至节点,作为合格限制和/或投影(在联接中的表上操作的那些)的父级。
prune_tree() 识别树中已经优化掉并且不再有效的节点(没有属性或表达式,也没有连接或排序),并删除它们。

启发式优化器的实现读起来非常容易。清单 13-10 展示了heuristic_optimization()方法的源代码实现。

清单 13-10。DBXP 启发式优化方法 14

/*
  Perform heuristic optimization

  SYNOPSIS
    heuristic_optimization()

  DESCRIPTION
    This method performs heuristic optimization on the query tree. The
    operation is destructive in that it rearranges the original tree.

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int Query_tree::heuristic_optimization()
{
  DBUG_ENTER("heuristic_optimization");
  query_node       *pNode;
  query_node       *nNode;

  h_opt = true;
  /*
    First, we have to correct the situation where restrict and
    project are grouped together in the same node.
  */
  split_restrict_with_join(root);
  split_project_with_join(root);
  split_restrict_with_project(root);

  /*
    Find a node with restrictions and push down the tree using
    a recursive call. continue until you get the same node twice.
    This means that the node cannot be pushed down any further.
  */
  pNode = find_restriction(root);
  while(pNode != 0)
  {
    push_restrictions(root, pNode);
    nNode = find_restriction(root);
    /*
      If a node is found, save a reference to it unless it is
      either the same node as the last node found or
      it is a leaf node. This is done so that we can ensure we
      continue searching down the tree visiting each node once.
    */
    if(nNode != 0)
    {
      if(nNode->nodeid == pNode->nodeid)
        pNode = 0;
      else if(is_leaf(nNode))
        pNode = 0;
      else
        pNode = nNode;
    }
  }

  /*
    Find a node with projections and push down the tree using
    a recursive call. Continue until you get the same node twice.
    This means that the node cannot be pushed down any further.
  */
  pNode = find_projection(root);
  while(pNode != 0)
  {
    push_projections(root, pNode);
    nNode = find_projection(root);
    /*
      If a node is found, save a reference to it unless it is
      either the same node as the last node found or
      it is a leaf node. This is done so that we can ensure we
      continue searching down the tree visiting each node once.
    */
    if(nNode != 0)
    {
      if(nNode->nodeid == pNode->nodeid)
        pNode = 0;
      else if(is_leaf(nNode))
        pNode = 0;
      else
        pNode = nNode;
    }
  }

  /*
    Find a join node and push it down the tree using
    a recursive call. Continue until you get the same node twice.
    This means that the node cannot be pushed down any further.
  */
  pNode = find_join(root);
  while(pNode != 0)
  {
    push_joins(root, pNode);
    nNode = find_join(root);
    /*
      If a node is found, save a reference to it unless it is
      either the same node as the last node found or
      it is a leaf node. This is done so that we can ensure we
      continue searching down the tree visiting each node once.
    */
    if(nNode != 0)
    {
      if(nNode->nodeid == pNode->nodeid)
        pNode = 0;
      else if(is_leaf(nNode))
        pNode = 0;
      else
        pNode = nNode;
    }
    else
      pNode = nNode;
  }

  /*
    Prune the tree of "blank" nodes
    Blank Nodes are:
     1) projections without attributes that have at least 1 child
     2) restrictions without expressions
     BUT...Can't delete a node that has TWO children!
  */
  prune_tree(0, root);

  /*
    Lastly, check to see if this has the DISTINCT option.
    If so, create a new node that is a DISTINCT operation.
  */
  if(distinct && (root->node_type != qntDistinct))
  {
    int i;
    pNode = (query_node*)my_malloc(sizeof(query_node),
            MYF(MY_ZEROFILL | MY_WME));
    init_node(pNode);
    pNode->sub_query = 0;
    pNode->attributes = 0;
    pNode->join_cond = jcUN;  /* (join_con_type) 0; */
    pNode->join_type = jnUNKNOWN;  /* (type_join) 0; */
    pNode->left = root;
    pNode->right = 0;
    for(i = 0; i < MAXNODETABLES; i++)
      pNode->relations[i] = NULL;
    pNode->nodeid = 90125;  // sentinel value to indicate node is not set
    pNode->child = LEFTCHILD;
    root->parent_nodeid = 90125;  // sentinel value to indicate node is not set
    root->child = LEFTCHILD;
    pNode->parent_nodeid = −1;
    pNode->node_type = qntDistinct;
    pNode->attributes = new Attribute();
    pNode->where_expr = new Expression();
    pNode->join_expr = new Expression();
    root = pNode;
  }
  DBUG_RETURN(0);
}

请注意用于定位限制、投影和连接的循环。该代码被设计为使用前序遍历遍历树,应用规则,直到不再有违反规则的条件(即,没有“坏的”节点放置)。

下面的清单显示了前面描述的heuristic_optimization()方法中主要方法的一些源代码。为了节省空间,我没有列出次要的助手方法,因为它们是 MySQL 结构和类方法的简单抽象。您应该下载本章的源代码,并研究其他帮助器方法,看看它们是如何工作的。

split_restrict_with_join()方法在树中搜索具有where表达式的连接(因此既是连接又是限制),并将它们分成两个节点:一个连接和一个限制节点。清单 13-11 显示了这个方法的源代码。

清单 13-11。 分割制约合并

/*
  Split restrictions that have joins.

  SYNOPSIS
    split_restrict_with_join()
    query_node *QN IN the node to operate on

  DESCRIPTION
     This method looks for joins that have where expressions (thus are both
     joins and restrictions) and breaks them into two nodes.

  NOTES
    This is a RECURSIVE method!

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int Query_tree::split_restrict_with_join(query_node *QN)
{
  int j = 0;
  int i = 0;

  DBUG_ENTER("split_restrict_with_join");
  if(QN != 0)
  {
    if(((QN->join_expr->num_expressions() > 0) &&
      (QN->where_expr->num_expressions() > 0)) &&
       ((QN->node_type == qntJoin) || (QN->node_type == qntRestrict)))
    {
      bool isleft = true;
      /*
        Create a new node and:
          1) Move the where expressions to the new node.
          2) Set the new node's children = current node children
          3) Set the new node's relations = current node relations.
          4) Set current node's left or right child = new node;
          5) Set new node's id = current id + 200;
           6) set parent id, etc.
          7) determine which table needs to be used for the
             restrict node.
      */
      query_node *new_node = (query_node*)my_malloc(sizeof(query_node),
                             MYF(MY_ZEROFILL | MY_WME));
      init_node(new_node);
      new_node->node_type = qntRestrict;
      new_node->parent_nodeid = QN->nodeid;
      new_node->nodeid = QN->nodeid + 200;
      new_node->where_expr = QN->where_expr;
      new_node->join_expr = new Expression();
      QN->where_expr = new Expression();

      /*
        Loop through tables and move table that matches
        to the new node
      */
      for(i = 0; i < MAXNODETABLES; i++)
      {
        if (QN->relations[i] != NULL)
        {
          if (find_table_in_expr(new_node->where_expr,
                QN->relations[i]->table_name))
          {
            new_node->relations[j] = QN->relations[i];
            j++;
            if (i != 0)
              isleft = false;
            QN->relations[i] = NULL;
          }
        }
      }

      /* set children to point to balance of tree */
      new_node->right = 0;
      if (isleft)
      {
        new_node->child = LEFTCHILD;
        new_node->left = QN->left;
        QN->left = new_node;
      }
      else
      {
        new_node->child = RIGHTCHILD;
        new_node->left = QN->right;
        QN->right = new_node;
      }
      if (new_node->left)
        new_node->left->parent_nodeid = new_node->nodeid;
      j = QN->attributes->num_attributes();
      if ((QN->node_type == qntJoin) && (j > 0))
      {
        Attribute *attribs = 0;
        Item * attr;
        int ii = 0;
        int jj = 0;
        if ((QN->attributes->num_attributes() == 1) &&
            (strcasecmp("*",
            ((Field *)QN->attributes->get_attribute(0))->field_name) == 0))
        {
          new_node->attributes = new Attribute();
          new_node->attributes->add_attribute(j,
            QN->attributes->get_attribute(0));
        }
        else
        {
          attribs = new Attribute();
          for (i = 0; i < (int)new_node->relations[0]->table->s->fields; i++)
          {
            Item *f = (Item *)new_node->relations[0]->table->field[i];
            attribs->add_attribute(true, (Item *)f);
          }
          j = attribs->num_attributes();
          new_node->attributes = new Attribute();
          for (i = 0; i < j; i++)
          {
            attr = attribs->get_attribute(i);
            jj = QN->attributes->index_of(
              (char *)((Field *)attr)->table->s->table_name.str,
              (char *)((Field *)attr)->field_name);
            if (jj > −1)
            {
              new_node->attributes->add_attribute(ii, attr);
              ii++;
              QN->attributes->remove_attribute(jj);
            }
            else if (find_attr_in_expr(QN->join_expr,
             (char *)((Field *)attr)->table->s->table_name.str,
             (char *)((Field *)attr)->field_name))
            {
              new_node->attributes->add_attribute(ii, attr);
              new_node->attributes->hide_attribute(attr, true);
              ii++;
            }
          }
        }
      }
      else
      {
        QN->node_type = qntJoin;
        QN->join_type = jnINNER;
        new_node->attributes = new Attribute();
      }
    }
    split_restrict_with_join(QN->left);
    split_restrict_with_join(QN->right);
  }
  DBUG_RETURN(0);
}

split_project_with_join()方法在树中搜索具有属性的连接(因此既是连接又是投影),并将它们分成两个节点:一个连接和一个项目节点。清单 13-12 显示了这个方法的源代码。

清单 13-12。 拆分项目合并

/*
  Split projections that have joins.

  SYNOPSIS
    split_project_with_join()
    query_node *QN IN the node to operate on

  DESCRIPTION
     This method looks for joins that have attributes (thus are both
     joins and projections) and breaks them into two nodes.

  NOTES
    This is a RECURSIVE method!

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int Query_tree::split_project_with_join(query_node *QN)
{
  int j = 0;
  int i;

  DBUG_ENTER("split_project_with_join");
  if(QN != 0)
  {
    if((QN->join_expr->num_expressions() > 0) &&
       ((QN->node_type == qntJoin) || (QN->node_type == qntProject)))
    {
      /*
        Create a new node and:
          1) Move the where expressions to the new node.
          2) Set the new node's children = current node children
          3) Set the new node's relations = current node relations.
          4) Set current node's left or right child = new node;
          5) Set new node's id = current id + 300;
          6) set parent id, etc.
      */
      QN->node_type = qntJoin;
      QN->join_type = jnINNER;
      if (QN->left == 0)
      {
        query_node *new_node = (query_node*)my_malloc(sizeof(query_node),
                               MYF(MY_ZEROFILL | MY_WME));
        init_node(new_node);
        new_node->node_type = qntProject;
        new_node->parent_nodeid = QN->nodeid;
        new_node->nodeid = QN->nodeid + 300;
        for(i = 0; i < MAXNODETABLES; i++)
          new_node->relations[i] = 0;
        new_node->relations[0] = QN->relations[0];
        QN->relations[0] = 0;
        new_node->left = QN->left;
        QN->left = new_node;
        new_node->right = 0;
        new_node->child = LEFTCHILD;
        if (new_node->left != 0)
           new_node->left->parent_nodeid = new_node->nodeid;
        j = QN->attributes->num_attributes();
        new_node->attributes = new Attribute();
        new_node->where_expr = new Expression();
        new_node->join_expr = new Expression();
        if ((j == 1) &&
            (strcasecmp("*", ((Field *)QN->attributes->get_attribute(0))->field_name)==0))
        {
          new_node->attributes = new Attribute();
          new_node->attributes->add_attribute(j, QN->attributes->get_attribute(0));
          if (QN->right != 0)
            QN->attributes->remove_attribute(0);
        }
        else if (j > 0)
        {
          Attribute *attribs = 0;
          Item * attr;
          int ii = 0;
          int jj = 0;
          attribs = new Attribute();
          for (i = 0; i < (int)new_node->relations[0]->table->s->fields; i++)
          {
            Field *f = new_node->relations[0]->table->field[i];
            attribs->add_attribute(true, (Item *)f);
          }
          j = attribs->num_attributes();
          for (i = 0; i < j; i++)
          {
            attr = attribs->get_attribute(i);
            jj = QN->attributes->index_of(
              (char *)((Field *)attr)->table->s->table_name.str,
              (char *)((Field *)attr)->field_name);
            if (jj > −1)
            {
              new_node->attributes->add_attribute(ii, attr);
              ii++;
              QN->attributes->remove_attribute(jj);
            }
            else if (find_attr_in_expr(QN->join_expr,
              (char *)((Field *)attr)->table->s->table_name.str,
              (char *)((Field *)attr)->field_name))
            {
              new_node->attributes->add_attribute(ii, attr);
              new_node->attributes->hide_attribute(attr, true);
              ii++;
            }
          }
        }
      }
      if (QN->right == 0)
      {
        query_node *new_node = (query_node*)my_malloc(sizeof(query_node),
                               MYF(MY_ZEROFILL | MY_WME));
        init_node(new_node);
        new_node->node_type = qntProject;
        new_node->parent_nodeid = QN->nodeid;
        new_node->nodeid = QN->nodeid + 400;
        for(i = 0; i < MAXNODETABLES; i++)
          new_node->relations[0] = 0;
        new_node->relations[0] = QN->relations[1];
        QN->relations[1] = 0;
        new_node->left = QN->right;
        QN->right = new_node;
        new_node->right = 0;
        new_node->child = RIGHTCHILD;
        if (new_node->left != 0)
          new_node->left->parent_nodeid = new_node->nodeid;
        j = QN->attributes->num_attributes();
        new_node->attributes = new Attribute();
        new_node->where_expr = new Expression();
        new_node->join_expr = new Expression();
        if ((j == 1) &&
            (strcasecmp("*", ((Field *)QN->attributes->get_attribute(0))->field_name)==0))
        {
          new_node->attributes = new Attribute();
          new_node->attributes->add_attribute(j, QN->attributes->get_attribute(0));
          QN->attributes->remove_attribute(0);
        }
        else
        {
          Attribute *attribs = 0;
          Item * attr;
          int ii = 0;
          int jj = 0;
          attribs = new Attribute();
          for (i = 0; i < (int)new_node->relations[0]->table->s->fields; i++)
          {
            Field *f = new_node->relations[0]->table->field[i];
            attribs->add_attribute(true, (Item *)f);
            if (j == 0)
            {
              new_node->attributes->hide_attribute((Item *)f, true);
            }
          }
          j = attribs->num_attributes();
          new_node->attributes = new Attribute();
          for (i = 0; i < j; i++)
          {
            attr = attribs->get_attribute(i);
            jj = QN->attributes->index_of(
              (char *)((Field *)attr)->table->s->table_name.str,
              (char *)((Field *)attr)->field_name);
            if (jj > −1)
            {
              new_node->attributes->add_attribute(ii, attr);
              ii++;
              QN->attributes->remove_attribute(jj);
            }
            else if (find_attr_in_expr(QN->join_expr,
              (char *)((Field *)attr)->table->s->table_name.str,
              (char *)((Field *)attr)->field_name))
            {
              new_node->attributes->add_attribute(ii, attr);
              new_node->attributes->hide_attribute(attr, true);
              ii++;
            }
          }
        }
      }
    }
    split_project_with_join(QN->left);
    split_project_with_join(QN->right);
  }
  DBUG_RETURN(0);
}

split_restrict_with_project()方法在树中搜索具有属性的限制(因此既是投影又是限制),并将它们分成两个节点:一个限制节点和一个项目节点。清单 13-13 显示了这个方法的源代码。

清单 13-13。 用项目分割制约

/*
  Split restrictions that have attributes (projections).

  SYNOPSIS
    split_restrict_with_project()
    query_node *QN IN the node to operate on

  DESCRIPTION
    This method looks for restrictions that have attributes (thus are both
    projections and restrictions) and breaks them into two nodes.

  NOTES
    This is a RECURSIVE method!

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int Query_tree::split_restrict_with_project(query_node *QN)
{
  DBUG_ENTER("split_restrict_with_project");
  if(QN != 0)
  {
    if(((QN->attributes->num_attributes() > 0) &&
      (QN->where_expr->num_expressions() > 0)) &&
      ((QN->node_type == qntProject) || (QN->node_type == qntRestrict)))
    {
      /*
        Create a new node and:
          1) Move the expressions to the new node.
           2) Set the new node's children = current node children
          3) Set the new node's relations = current node relations.
          4) Set current node's left child = new node;
          5) Set new node's id = current id + 1000;
          6) set parent id, etc.
      */
      query_node *new_node = (query_node*)my_malloc(sizeof(query_node),
                             MYF(MY_ZEROFILL | MY_WME));
      init_node(new_node);
      new_node->child = LEFTCHILD;
      new_node->node_type = qntRestrict;
      if(new_node->node_type == qntJoin)
      {
        new_node->join_cond = QN->join_cond;
        new_node->join_type = QN->join_type;
      }
      QN->node_type = qntProject;
      new_node->attributes = new Attribute();
      new_node->where_expr = QN->where_expr;
      new_node->join_expr = new Expression();
      QN->where_expr = new Expression();
      new_node->left = QN->left;
      new_node->right = QN->right;
      new_node->parent_nodeid = QN->nodeid;
      new_node->nodeid = QN->nodeid + 1000;
      if(new_node->left)
        new_node->left->parent_nodeid = new_node->nodeid;
      if(new_node->right)
        new_node->right->parent_nodeid = new_node->nodeid;
      for(int i = 0; i < MAXNODETABLES; i++)
      {
        new_node->relations[i] = QN->relations[i];
        QN->relations[i] = NULL;
      }
      QN->left = new_node;
      QN->right = 0;
    }
    split_restrict_with_project(QN->left);
    split_restrict_with_project(QN->right);
  }
  DBUG_RETURN(0);
}

find_restriction()方法从起始节点(QN)开始在树中搜索下一个限制。如果找到限制,则返回指向该节点的指针;否则,该方法返回NULL清单 13-14 显示了这个方法的源代码。

清单 13-14。 寻找制约

/*
  Find a restriction in the subtree.
  SYNOPSIS
    find_restriction()
    query_node *QN IN the node to operate on

  DESCRIPTION
    This method looks for a node containing a restriction and returns the node
    pointer.

  NOTES
    This is a RECURSIVE method!
    This finds the first restriction and is biased to the left tree.

  RETURN VALUE
    Success = query_node * the node located
    Failed = NULL
*/
Query_tree::query_node *Query_tree::find_restriction(query_node *QN)
{
  DBUG_ENTER("find_restriction");
  query_node   *N;

  N = 0;
  if(QN != 0)
  {
    /*
      A restriction is a node marked as restrict and
      has at least one expression
    */
    if (QN->where_expr->num_expressions() > 0)
      N = QN;
    else
    {
      N = find_restriction(QN->left);
      if(N == 0)
        N = find_restriction(QN->right);
    }
  }
  DBUG_RETURN(N);
}

push_restriction()方法 从起始节点(QN)开始搜索树,并将约束节点(pNode)向下推到包含约束中指定的关系的节点。清单 13-15 显示了这个方法的源代码。

清单 13-15。 推送限制

/*
  Push restrictions down the tree.

  SYNOPSIS
    push_restrictions()
    query_node *QN IN the node to operate on
    query_node *pNode IN the node containing the restriction attributes

  DESCRIPTION
    This method looks for restrictions and pushes them down the tree to nodes
    that contain the relations specified.

  NOTES
    This is a RECURSIVE method!
    This finds the first restriction and is biased to the left tree.

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int Query_tree::push_restrictions(query_node *QN, query_node *pNode)
{
  query_node       *NewQN=0;

  DBUG_ENTER("push_restrictions");
  if((QN != 0) && (pNode != 0) && (pNode->left != 0))
  {
    /*
      Conditions:
        1) QN is a join node
        2) QN is a project node
        3) QN is a restrict node
        4) All other nodes types are ignored.

      Methods:
        1) if join or project and the children are not already restrictions
           add a new node and put where clause in new node else
           see if you can combine the child node and this one
        2) if the node has the table and it is a join,
           create a new node below it and push the restriction
           to that node.
        3) if the node is a restriction and has the table,
           just add the expression to the node's expression list
    */

    /* if projection, move node down tree */
    if((QN->nodeid != pNode->nodeid) && (QN->node_type == qntProject))
    {
      if (QN->left != 0)
      {
        QN->left = (query_node*)my_malloc(sizeof(query_node),
                   MYF(MY_ZEROFILL | MY_WME));
        init_node(QN->left);
        NewQN = QN->left;
        NewQN->left = 0;
      }
      else
      {
        NewQN = QN->left;
        QN->left = (query_node*)my_malloc(sizeof(query_node),
                   MYF(MY_ZEROFILL | MY_WME));
        QN->left->left = NewQN;
        NewQN = QN->left;
      }
      NewQN->sub_query = 0;
      NewQN->join_cond = jcUN;  /* (join_con_type) 0; */
      NewQN->join_type = jnUNKNOWN;  /* (type_join) 0; */
      NewQN->right = 0;
      for(long i = 0; i < MAXNODETABLES; i++)
        NewQN->relations[i] = 0;
      NewQN->nodeid = QN->nodeid + 1;
      NewQN->parent_nodeid = QN->nodeid;
      NewQN->node_type = qntRestrict;
      NewQN->attributes = new Attribute();
      NewQN->where_expr = new Expression();
      NewQN->join_expr = new Expression();
      if (pNode->relations[0])
        NewQN->where_expr->reduce_expressions(pNode->relations[0]->table);
      if ((QN->relations[0] != NULL) && (QN->relations[0] == pNode->relations[0]))
      {
        if (QN->relations[0])
        {
          if (find_table_in_expr(pNode->where_expr, QN->relations[0]->table_name))
          {
            NewQN->relations[0] = QN->relations[0];
            QN->relations[0] = 0;
          }
        }
      }
      else
      {
        if (pNode->relations[0])
          if (find_table_in_tree(QN->left, pNode->relations[0]->table_name))
            NewQN->relations[0] = 0;
        pNode->where_expr = NULL;
        pNode->relations[0] = 0;
      }
    }
    /* if join, move restrict node down tree */
    else if((QN->nodeid != pNode->nodeid) &&
      ((QN->left == 0) || (QN->right == 0)) &&
      (QN->node_type == qntJoin))
    {
      if(QN->relations[0] != 0)
      {
        QN->left = (query_node*)my_malloc(sizeof(query_node),
                   MYF(MY_ZEROFILL | MY_WME));
        NewQN = QN->left;
        NewQN->sub_query = 0;
        NewQN->join_cond = jcUN;  /* (join_con_type) 0; */
        NewQN->join_type = jnUNKNOWN;  /* (type_join) 0; */
        NewQN->left = 0;
        NewQN->right = 0;
        for(long i = 0; i < MAXNODETABLES; i++)
          NewQN->relations[i] = 0;
        NewQN->nodeid = QN->nodeid + 1;
        NewQN->parent_nodeid = QN->nodeid;
        NewQN->node_type = qntRestrict;
        NewQN->attributes = new Attribute();
        NewQN->where_expr = new Expression();
        NewQN->join_expr = new Expression();
        NewQN->relations[0] = QN->relations[0];
        QN->relations[0] = 0;
        if (pNode->relations[0])
          NewQN->where_expr->reduce_expressions(pNode->relations[0]->table);
      }
      else if(QN->relations[1] != 0)
      {
        QN->right = (query_node*)my_malloc(sizeof(query_node),
                    MYF(MY_ZEROFILL | MY_WME));
        NewQN = QN->left;
        NewQN->sub_query = 0;
        NewQN->join_cond = jcUN;  /* (join_con_type) 0; */
        NewQN->join_type = jnUNKNOWN;  /* (type_join) 0; */
        NewQN->left = 0;
        NewQN->right = 0;
        for(long i = 0; i < MAXNODETABLES; i++)
          NewQN->relations[i] = 0;
      }
      NewQN->nodeid = QN->nodeid + 1;
      NewQN->parent_nodeid = QN->nodeid;
      NewQN->node_type = qntRestrict;
      NewQN->attributes = new Attribute();
      NewQN->where_expr = new Expression();
      NewQN->join_expr = new Expression();
      NewQN->relations[0] = QN->relations[1];
      QN->relations[1] = 0;
      NewQN->where_expr->reduce_expressions(pNode->relations[0]->table);
    }
    push_restrictions(QN->left, pNode);
    push_restrictions(QN->right, pNode);
  }
   DBUG_RETURN(0);
}

find_projection()方法从起始节点(QN)开始搜索树,寻找树中的下一个投影。如果找到投影,则返回指向该节点的指针;否则,该方法返回NULL。清单 13-16 显示了这个方法的源代码。

清单 13-16。 寻找投影

/*
  Find a projection in the tree

  SYNOPSIS
    find_projection()
    query_node *QN IN the node to operate on

  DESCRIPTION
    This method looks for a node containing a projection and returns the node
    pointer.

  NOTES
    This finds the first projection and is biased to the left tree.
    This is a RECURSIVE method!

  RETURN VALUE
    Success = query_node * the node located or NULL for not found
    Failed = NULL
*/
Query_tree::query_node *Query_tree::find_projection(query_node *QN)
{
  DBUG_ENTER("find_projection");
  query_node   *N;

  N = 0;
  if(QN != 0)
  {
    /*
      A projection is a node marked as project and
      has at least one attribute
    */
    if((QN->node_type == qntProject) &&
       (QN->attributes != 0))
      N = QN;
    else
    {
      N = find_projection(QN->left);
      if(N == 0)
        N = find_projection(QN->right);
    }
  }
  DBUG_RETURN(N);
}

push_projection()方法 从起始节点(QN)开始搜索树,并将投影节点(pNode)向下推到包含投影中指定的关系的节点。清单 13-17 显示了这个方法的源代码。

清单 13-17。 推投影

/*
  Push projections down the tree.

  SYNOPSIS
    push_projections()
    query_node *QN IN the node to operate on
    query_node *pNode IN the node containing the projection attributes

  DESCRIPTION
    This method looks for projections and pushes them down the tree to nodes
    that contain the relations specified.

  NOTES
    This is a RECURSIVE method!

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int Query_tree::push_projections(query_node *QN, query_node *pNode)
{
  DBUG_ENTER("push_projections");
  Item *   a;
  int         i;
  int         j;

  if((QN != 0) && (pNode != 0))
  {
    if((QN->nodeid != pNode->nodeid) &&
       (QN->node_type == qntProject))
    {
      i = 0;
      j = QN->attributes->num_attributes();
      /* move attributes to new node */
      while(i < j)
      {
        a = QN->attributes->get_attribute(i);
        if(has_relation(QN,
          (char *)((Field *)a)->table->s->table_name.str))
        {
          if(!has_attribute(QN, a))
            insert_attribute(QN, a);
          del_attribute(pNode, a);
        }
        i++;
      }
    }
    if(pNode->attributes->num_attributes() != 0)
    {
      push_projections(QN->left, pNode);
      push_projections(QN->right, pNode);
    }
  }
  DBUG_RETURN(0);
}

find_join ()方法 从起始节点(QN)开始在树中搜索下一个连接。如果找到一个连接,则返回一个指向该节点的指针;否则,该方法返回NULL。清单 13-18 显示了这个方法的源代码。

清单 13-18。 寻找加入

/*
  Find a join in the subtree.
  SYNOPSIS
    find_restriction()
    query_node *QN IN the node to operate on

  DESCRIPTION
    This method looks for a node containing a join and returns the
    node pointer.

  NOTES
    This is a RECURSIVE method!
    This finds the first restriction and is biased to the left tree.

  RETURN VALUE
    Success = query_node * the node located
    Failed = NULL
*/
Query_tree::query_node *Query_tree::find_join(query_node *QN)
{
  DBUG_ENTER("find_join");
  query_node               *N;
  N = 0;

  if(QN != 0)
  {
    /*
      if this is a restrict node or a restrict node with
      at least one expression it could be an unprocessed join
      because the default node type is restrict
    */
    if(((QN->node_type == qntRestrict) ||
      (QN->node_type == qntRestrict)) && (QN->join_expr->num_expressions() > 0))
      N = QN;
    else
    {
      N = find_join(QN->left);
      if(N == 0)
        N = find_join(QN->right);
    }
  }
  DBUG_RETURN(N);
}

push_joins()方法 从起始节点(QN)开始搜索树,并将连接节点(pNode)沿树向下推到一个位置,在该位置,连接是两个节点的父节点,这两个节点包含连接的子节点中指定的关系。清单 13-19 显示了这个方法的源代码。

清单 13-19。 推归附

/*
  Push joins down the tree.

  SYNOPSIS
    push_restrictions()
    query_node *QN IN the node to operate on
    query_node *pNode IN the node containing the join

  DESCRIPTION
    This method looks for theta joins and pushes them down the tree to the
    parent of two nodes that contain the relations specified.

  NOTES
    This is a RECURSIVE method!

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int Query_tree::push_joins(query_node *QN, query_node *pNode)
{
  DBUG_ENTER("push_joins");
  Item *lField;
  Item *rField;
  expr_node *node;

  if(!pNode->join_expr)
    DBUG_RETURN(0);
  node = pNode->join_expr->get_expression(0);
  if (!node)
    DBUG_RETURN(0);
  lField = node->left_op;
  rField = node->right_op;

  /* Node must have expressions and not be null */
  if((QN != NULL) && (pNode != NULL) &&
     (pNode->join_expr->num_expressions() > 0))
  {
    /* check to see if tables in join condition exist */
    if((QN->nodeid != pNode->nodeid) &&
       (QN->node_type == qntJoin) &&
        QN->join_expr->num_expressions() == 0 &&
       ((has_relation(QN->left,
         (char *)((Field *)lField)->table->s->table_name.str) &&
       has_relation(QN->right,
         (char *)((Field *)rField)->table->s->table_name.str)) ||
      (has_relation(QN->left,
        (char *)((Field *)rField)->table->s->table_name.str) &&
       has_relation(QN->right,
         (char *)((Field *)lField)->table->s->table_name.str))))
    {
      /* move the expression */
      QN->join_expr = pNode->join_expr;
      pNode->join_expr = new Expression();
      QN->join_type = jnINNER;
      QN->join_cond = jcON;
    }
    push_joins(QN->left, pNode);
    push_joins(QN->right, pNode);
  }
  DBUG_RETURN(0);
}

prune_tree()方法 在树上搜索作为对树执行启发式优化的结果的空白节点(不再具有任何操作或功能的节点)并删除它们。清单 13-20 显示了这个方法的源代码。

清单 13-20。 修枝树

/*
  Prune the tree of dead limbs.

  SYNOPSIS
    prune_tree()
    query_node *prev IN the previous node (parent)
    query_node *cur_node IN the current node pointer (used to delete).

  DESCRIPTION
    This method looks for nodes blank nodes that are a result of performing
    heuristic optimization on the tree and deletes them.

  NOTES
    This is a RECURSIVE method!

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int Query_tree::prune_tree(query_node *prev, query_node *cur_node)
{
  DBUG_ENTER("prune_tree");
  if(cur_node != 0)
  {
    /*
      Blank Nodes are 1) projections without attributes
      that have at least 1 child, or 2) restrictions
      without expressions
    */
    if((((cur_node->node_type == qntProject) &&
       (cur_node->attributes->num_attributes() == 0)) ||
      ((cur_node->node_type == qntRestrict) &&
       (cur_node->where_expr->num_expressions() == 0))) &&
       ((cur_node->left == 0) || (cur_node->right == 0)))
    {
      /*
        Redirect the pointers for the nodes above and
        below this node in the tree.
      */
      if(prev == 0)
      {
         if(cur_node->left == 0)
        {
          cur_node->right->parent_nodeid = −1;
          root = cur_node->right;
        }
        else
        {
          cur_node->left->parent_nodeid = −1;
          root = cur_node->left;
        }
        my_free(cur_node);
        cur_node = root;
      }
      else
      {
        if(prev->left == cur_node)
        {
          if(cur_node->left == 0)
          {
            prev->left = cur_node->right;
            if (cur_node->right != NULL)
              cur_node->right->parent_nodeid = prev->nodeid;
          }
          else
          {
            prev->left = cur_node->left;
            if (cur_node->left != NULL)
              cur_node->left->parent_nodeid = prev->nodeid;
          }
          my_free(cur_node);
          cur_node = prev->left;
        }
        else
        {
          if(cur_node->left == 0)
          {
            prev->right = cur_node->right;
            if (cur_node->right != NULL)
              cur_node->right->parent_nodeid = prev->nodeid;
          }
          else
          {
            prev->right = cur_node->left;
            if (cur_node->left != NULL)
              cur_node->left->parent_nodeid = prev->nodeid;
          }
          my_free(cur_node);
          cur_node = prev->right;
        }
      }
      prune_tree(prev, cur_node);
    }
    else
    {
      prune_tree(cur_node, cur_node->left);
      prune_tree(cur_node, cur_node->right);
    }
  }
  DBUG_RETURN(0);
}

编译和测试代码

如果还没有,下载本章的源代码,并将文件放在源代码根目录下的/sql目录中。在示例代码中,您还会发现一个差异文件(ch13.diff ),您可以使用它将更改应用到服务器源代码文件(例如 mysqld.cc sql_cmd.h 等)。).或者,您可以使用第 12 章中的修改,因为对服务器代码的修改是相同的。

花一些时间浏览源代码,以便熟悉这些方法。如果您需要调试代码以使用您的配置,或者如果您想要添加其他增强功能或进行练习,现在花时间浏览代码将会有所帮助。

一旦下载了所有的源代码文件并检查了代码,就将这些文件添加到 CMakeLists.txt 文件中。详见第 12 章中的“将文件添加到 CMakeLists.txt 文件中”。您将添加attributeexpression辅助源文件(attribute.cc 和 expression.cc)。将文件添加到项目后,导航到源代码树的根目录,运行 cmake,并发出如下所示的命令。确保代码编译时没有编译错误。

cmake .
make

一旦安装并编译了新代码,运行服务器并执行测试。您可以运行之前创建的测试,也可以在 MySQL 客户端实用程序中输入命令。清单 13-21 显示了运行测试中列出的命令的预期输出。

清单 13-21。 示例测试运行

DBXP_SELECT * FROM staff' at line 1
+−−------------------------+
| Execution Path           |
+−−------------------------+
|      expert_mysql.staff  |
|           |              |
|           |              |
|           |              |
|           V              |
|      ------------------- |
|      |    RESTRICT     | |
|      ------------------- |
|      | Access Method:  | |
|      |    iterator     | |
|      ------------------- |
|           |              |
|           |              |
|           |              |
|           V              |
|      ------------------- |
|      |     PROJECT     | |
|      ------------------- |
|      | Access Method:  | |
|      |    iterator     | |
|      ------------------- |
|              |           |
|              |           |
|              V           |
|          Result Set      |
+−−------------------------+
25 rows in set (0.00 sec)

+−−--------------------------------------------------+
| Execution Path                                     |
+−−--------------------------------------------------+
|      expert_mysql.staff                            |
|           |                                        |
|           |                                        |
|           |                                        |
|           V                                        |
|      -------------------                           |
|      |     PROJECT     |                           |
|      -------------------                           |
|      | Access Method:  |                           |
|      |    iterator     |                           |
|      -------------------                           |
|           |               expert_mysql.directorate |
|           |                    |                   |
|           |                    |                   |
|           |                    |                   |
|           |                    V                   |
|           |               -------------------      |
|           |               |     PROJECT     |      |
|           |               -------------------      |
|           |               | Access Method:  |      |
|           |               |    iterator     |      |
|           |               -------------------      |
|           |                              |         |
|           |   ----------------------------         |
|           |   |                                    |
|           V   V                                    |
|      -------------------                           |
|      |      JOIN       |                           |
|      -------------------                           |
|      | Access Method:  |                           |
|      |    iterator     |                           |
|      -------------------                           |
|              |                                     |
|              |                                     |
|              V                                     |
|          Result Set                                |
+−−--------------------------------------------------+
36 rows in set (0.00 sec)

+−−----------------------------------------------+
| Execution Path                                 |
+−−----------------------------------------------+
|      expert_mysql.staff                        |
|           |                                    |
|           |                                    |
|           |                                    |
|           V                                    |
|      -------------------                       |
|      |    RESTRICT     |                       |
|      -------------------                       |
|      | Access Method:  |                       |
|      |    iterator     |                       |
|      -------------------                       |
|           |               expert_mysql.tasking |
|           |                    |               |
|           |                    |               |
|           |                    |               |
|           |                    V               |
|           |               -------------------  |
|           |               |     PROJECT     |  |
|           |               -------------------  |
|           |               | Access Method:  |  |
|           |               |    iterator     |  |
|           |               -------------------  |
|           |                              |     |
|           |   ----------------------------     |
|           |   |                                |
|           V   V                                |
|      -------------------                       |
|      |      JOIN       |                       |
|      -------------------                       |
|      | Access Method:  |                       |
|      |    iterator     |                       |
|      -------------------                       |
|              |                                 |
|              |                                 |
|              V                                 |
|          Result Set                            |
+−−----------------------------------------------+
36 rows in set (0.00 sec)

Query OK, 4 rows affected (0.00 sec)
mysql >

请注意输入的每个语句的查询计划有何不同。花些时间研究其他查询语句,看看优化器如何优化其他形式的查询。

image 注意DBXP_SELECT命令的输出一开始可能看起来有点奇怪(它们是查询计划),但是回想一下之前我们在sql_dbxp_parser.cc文件中删除了DBXP_select_command()方法以重定向到DBXP_explain_select_command()方法。我们将在下一章添加查询的执行。

摘要

在本章中,我介绍了最复杂的数据库内部技术——优化器。您了解了如何扩展查询树的概念,以包含一个在优化过程中使用树结构的查询优化器。更重要的是,您发现了如何构建启发式查询优化器。启发式优化器的知识应该让您更好地理解 DBXP 引擎,以及如何使用它来更深入地研究数据库技术。没有比优化器更深入的了!

在下一章中,我将通过一个查询树优化策略的示例实现向您展示更多关于查询执行的内容。下一章将通过使用查询树类将启发式查询优化器链接到一个也使用查询树结构的执行过程来完成 DBXP 引擎。

练习

下面列出了几个需要进一步探索的领域。它们代表了您可能希望作为实验(或作为课堂作业)来探索关系数据库技术的活动类型。

  1. 完成balance_joins()方法的代码。提示:您需要创建一个可以移动合取连接的算法,以便首先执行限制性最强的连接(位于树的最底层)。
  2. 完成cost_optimization()方法的代码。提示:您需要遍历树并指出可以使用索引的节点。
  3. 检查启发式优化器的代码。它涵盖了所有可能的查询吗?如果不是,有没有其他规则可以用来完成保险?
  4. 检查查询树和启发式优化器的代码。如何实现查询树类中列出的 distinct 节点类型?提示:参见heuristic_optimization()方法中prune_tree()方法后面的代码。
  5. 如何更改代码来识别无效查询?确定查询无效的条件是什么?如何测试这些条件?
  6. (高级)MySQL 目前不支持 intersect 操作(按日期定义)。更改 MySQL 解析器以识别新的关键字并处理查询,例如SELECT * FROM A INTERSECT B。这个操作有什么限制吗,它们在优化器中有反映吗?
  7. (高级)你将如何实现GROUP BYORDER BYHAVING子句?对优化器进行更改以启用这些子句。

1 P. G .塞林格、M. M .阿斯特拉姆、D. D .钱伯林、R. A .洛里斯、T. G .普莱斯。1979."关系数据库管理系统中的访问路径选择."ACM SIGMOD 数据管理国际会议记录,苏格兰阿伯丁:23–34。被一些人认为是“查询优化的圣经”

2 M .斯通布雷克,e .黄,p .克雷普斯。1976." INGRES 的设计和实现."数据库系统上的 ACM 事务1(3):189–222。

3

4 传统系统中的查询执行不仅包括处理查询,还包括从物理介质中访问数据。然而,内存系统没有与从物理介质中检索相关联的长访问时间。

这种做法至今仍被大多数商业数据库系统所采用。

6 实时统计的累加称为捎带统计生成。

7 D .科斯曼和 k .斯托克。2000."迭代动态规划:一类新的查询优化算法." ACM 数据库系统汇刊25(1):43–82。

8 Y. E .约安尼迪斯、R. T. Ng、K. Shim 和 T. Sellis。1997.“参数查询优化。” VLDB 日报6:132–151。

9 在这种情况下,高效的执行未必是最优方案。

10 数据库新手常用的技巧。

11 可能不允许使用索引进行连接操作。

12 李政道、施振荣、陈奕奕。2001."优化包含方法的查询的图论模型."LDB 日报 9:327–343。

13 它不是一个真正的抽象类,因为它包含了一些在源代码中定义的方法。一个真正的抽象类将所有方法定义为virtual,因此它们被用作接口而不是基类。

14 这段代码中的哨兵值来源于一张经典摇滚专辑。你知道乐队的名字吗?

十四、查询执行

第 12 章中显示的查询树类和第 13 章中显示的启发式优化器构成了 DBXP 查询执行引擎的三个组件中的前两个。在本章中,我将向您展示如何扩展查询树类来处理投影、限制和连接操作。这将让您一瞥数据库查询执行的世界。我首先简要解释查询执行算法的基本原理,然后开始编写代码。因为某些方法的代码相当长,所以本章中显示的代码示例并不都包含完整的源代码。如果您正在编写示例代码,请考虑加载本章的源代码并使用它,而不是从头开始键入代码。

重新审视查询执行

查询执行过程是各种关系理论操作的实现。这些操作包括投影、限制、连接、叉积、并集和交集。很少有数据库系统实现并集和交集。

image 注意 Union 和 intersect 与 SQL 中的UNION运算符不同。union 和 intersect 关系操作是集合操作,而 SQL 中的UNION操作符只是组合两个或多个具有兼容结果列的SELECT语句的结果。

编写实现这些操作的算法非常简单,在关系理论和数据库系统的教科书中经常被忽略。不幸的是,算法被省略了,因为连接操作并不简单。以下部分描述了关系运算的基本算法。

项目

在项目(或投影)操作中,结果集包含原始关系(表)中属性(列)的子集。 1 因此,结果集可以包含比原始关系更少的属性。用户在 SQL SELECT命令中指定投影,方法是在列列表中紧随SELECT关键字列出所需的列。例如,这个命令从staff表中投影出first_namelast_name列:

SELECT first_name, last_name FROM staff

实现该操作的项目算法 如清单 14-1 所示。

清单 14-1。 项目算法

begin
  do
    get next tuple from relation
    for each attribute in tuple
      if attribute.name found in column_list
        write attribute data to client
      fi
  while not end_of_relation
end

从清单中可以看出,实现该算法的代码将发送给客户机的数据限制为列列表中指定的数据。

限制

在限制(或约束)操作中,结果集包含原始关系(表)中元组(行)的子集。因此,结果集可以包含比原始关系更少的元组。用户通过在紧随FROM子句之后的WHERE子句中列出期望的条件,在 SQL SELECT命令中指定限制。例如,下面的命令将来自staff表的结果集限制为年收入超过$65,000.00 的雇员。

SELECT first_name, last_name FROM staff WHERE salary > 65000.00

实现该操作的限制算法 如清单 14-2 所示。

清单 14-2。 限制算法

begin
  do
    get next tuple from relation
    if tuple attribute values match conditions
      write attribute data to client
    fi
  while not end_of_relation
end

从清单中可以看出,实现该算法的代码是元组中的数据值与WHERE子句中的条件相匹配的地方。该算法通常通过额外的优化步骤来实现,以将表达式减少到最小集合(例如,忽略总是为真的条件)。

加入

连接操作是这样一种操作,其中结果集包含与两个关系(表)的指定组合相匹配的元组(行)。使用n-1连接来连接三个或更多的表,其中n是表的数量。在连接三个表(A、B、C)的情况下,连接是两个表的组合,并且该连接的结果与剩余的表连接。连接方式的组合——作为左深或右深的树或者甚至作为浓密的树——是中间连接的执行顺序之一。图 14-1 中显示了这些类型的采油树布置的示例。

9781430246596_Fig14-01.jpg

图 14-1。树形排列示例

联接最常用于主/从关系中,在这种关系中,一个表(基表或主表)引用一个或多个子表(从表),其中基表中的一条记录与从表中的一条或多条记录相匹配。例如,您可以创建一个包含客户信息的customer表和一个包含客户订单数据的orders表。customer表是基表,orders表是子表。

SELECT customer.name, orders.number
FROM customer JOIN orders on customer.id = orders.customerid

用户通过在FROM子句中列出所需的表和连接条件,在 SQL SELECT命令中指定连接。例如,下面的命令将customer表中的记录与orders表中的记录连接起来。注意,在这种情况下,连接条件是一个简单的等式关系,表共享一个公共列。

连接操作的算法不像前面描述的那样简单。这是因为连接操作可以用几种形式表示。您可以选择使用一个简单的column from table A = column from table B表达式进行连接,就像前面的例子一样,或者您可以选择通过只包含左、右或两个表中的匹配行(内部)、匹配和不匹配行(外部)来控制输出。因此,连接操作包括内连接(有时称为自然连接或等价连接 2 )、左外连接、右外连接、全外连接、叉积、并集和交集。以下各节详细描述了这些操作。

image 注意一些数据库文本将叉积、并集和交集视为离散操作。我认为它们是连接操作的特殊形式。

内部连接

inner-join 操作是这样一种操作,其中结果集包含原始关系(表)中元组(行)的一个子集,其中有一个连接条件匹配。之所以称为内部联接,是因为只有第一个关系中联接条件值与第二个关系中某行的联接条件值相匹配的那些行才会包含在结果集中。

用户通过在FROM子句中列出所需的条件,在 SQL SELECT命令中指定内部连接。例如,下面的命令将结果集从staff表连接到directorate表,返回一个结果集,该结果集包含所有分配了董事会的雇员(一个雇员没有分配董事会)。

SELECT staff.last_name, staff.dept_name
FROM staff JOIN directorate on staff.dept_id = directorate.id

image 注意对于大多数数据库系统来说,关键字INNER通常是可选的,因为默认的连接操作是内部连接。

实现该操作的内部连接算法如清单 14-3 中的所示。该算法只是几种形式的连接算法之一。所示的算法是合并-连接的一种变体。因此,可以使用另一个同等能力的连接算法来实现该算法,例如嵌套循环连接算法。

清单 14-3。 加入算法

begin
  sort relation a as rel_a on join column(s)
  sort relation b as rel_b on join column(s)
  do
    get next tuple from rel_a
    get next tuple from rel_b
    if join column values match join conditions
      write attribute data to client
    fi
    check rewind conditions
  while not end_of_rel_a and not end_of_rel_b
end

用户还可以通过在WHERE子句中包含连接条件来指定内部连接,如下所示。一些数据库专业人员不鼓励这种变体,因为它可能会被误认为是普通的SELECT命令。大多数人认为这种变体在功能上是等价的,大多数数据库优化器都是为了适应它而编写的。


SELECT staff.last_name, directorate.dept_name
FROM staff, directorate WHERE staff.dept_id = directorate.id

从清单中可以看出,实现该算法的代码需要使用排序。需要进行排序来对表中连接列上的行进行排序,这样,如果行中有任何重复的条件值,算法就可以正确地识别所有匹配项。为了说明这一点,考虑清单 14-4 中的表格。

清单 14-4。 示例连接表(无序)

staff table
+-----------+------------+----------+------------+------+--------+-----------+
| id        | first_name | mid_name | last_name  | sex  | salary | mgr_id    |
+-----------+------------+----------+------------+------+--------+-----------+
| 333445555 | John       | Q        | Smith      | M    |  30000 | 333444444 |
| 123763153 | William    | E        | Walters    | M    |  25000 | 123654321 |
| 333444444 | Alicia     | F        | St.Cruz    | F    |  25000 | None      |
| 921312388 | Goy        | X        | Hong       | F    |  40000 | 123654321 |
| 800122337 | Rajesh     | G        | Kardakarna | M    |  38000 | 333445555 |
| 820123637 | Monty      | C        | Smythe     | M    |  38000 | 333445555 |
| 830132335 | Richard    | E        | Jones      | M    |  38000 | 333445555 |
| 333445665 | Edward     | E        | Engles     | M    |  25000 | 333445555 |
| 123654321 | Beware     | D        | Borg       | F    |  55000 | 333444444 |
| 123456789 | Wilma      | N        | Maxima     | F    |  43000 | 333445555 |
+-----------+------------+----------+------------+------+--------+-----------+

directorate table
+----------+-----------------+-------------+
| dir_code | dir_name        | dir_head_id |
+----------+-----------------+-------------+
| N41      | Development     | 333445555   |
| N01      | Human Resources | 123654321   |
| M00      | Management      | 333444444   |
+----------+-----------------+-------------+

image 注意一些数据库系统(如 MySQL)以原始的无序顺序返回行。为了突出重点,按照内部排序的顺序列出了所示的示例。

请注意,这些表没有排序。如果要在不对行排序的情况下运行使用该算法显示的示例连接,则必须从一个表中读取每行的所有行,从另一个表中读取。例如,如果按所示顺序读取staff表,您将从directorate表中读取一行用于第一次连接,从directorate表中读取两行用于从staff开始的下一行,然后是两行、一行、一行、一行、四行、两行,总共从directorate表中读取 14 次来完成操作。然而,如果表按照清单 14-5 中的排序,你可以避免从directorate表中重新读取行。

清单 14-5。 示例连接表(按连接列排序)

staff table
+-----------+------------+----------+------------+------+--------+-----------+
| id        | first_name | mid_name | last_name  | sex  | salary | mgr_id    |
+-----------+------------+----------+------------+------+--------+-----------+
| 123763153 | William    | E        | Walters    | M    |  25000 | 123654321 |
| 921312388 | Goy        | X        | Hong       | F    |  40000 | 123654321 |
| 333445555 | John       | Q        | Smith      | M    |  30000 | 333444444 |
| 123654321 | Beware     | D        | Borg       | F    |  55000 | 333444444 |
| 333445665 | Edward     | E        | Engles     | M    |  25000 | 333445555 |
| 830132335 | Richard    | E        | Jones      | M    |  38000 | 333445555 |
| 820123637 | Monty      | C        | Smythe     | M    |  38000 | 333445555 |
| 800122337 | Rajesh     | G        | Kardakarna | M    |  38000 | 333445555 |
| 123456789 | Wilma      | N        | Maxima     | F    |  43000 | 333445555 |
| 333444444 | Alicia     | F        | St.Cruz    | F    |  25000 | None      |
+-----------+------------+----------+------------+------+--------+-----------+

directorate table
+----------+-----------------+-------------+
| dir_code | dir_name        | dir_head_id |
+----------+-----------------+-------------+
| N01      | Human Resources | 123654321   |
| M00      | Management      | 333444444   |
| N41      | Development     | 333445555   |
+----------+-----------------+-------------+

然而,这产生了另一个问题。您如何知道不从任何一个表中读取另一行呢?请注意内部连接算法的最后一步。这就是实现变得有点棘手的地方。这里您需要做的是能够重用已经被读取的行,以便您可以将一个表中的一行与另一个表中的多行进行比较。当您考虑到可能必须从任一表前进(前进一行)或后退(后退一行)时,这就变得很棘手了。

如果您使用 staff . mgr _ id = directorate . dir _ head _ id 作为连接条件(首先从staff表中读取为rel_a)手动执行排序示例表的算法,您将会看到该算法需要两次“重用”dir_head_ id为 333444444 的 directive 行和 dir_head_ id为 333445555 的行。行的缓存有时被称为“倒回”表读指针。这个例子的结果集如清单 14-6 所示。

清单 14-6。 示例内部连接结果集

+------------+-----------------+
| last_name  | dir_name        |
+------------+-----------------+
| Smith      | Management      |
| Walters    | Human Resources |
| Hong       | Human Resources |
| Kardakarna | Development     |
| Smythe     | Development     |
| Jones      | Development     |
| Engles     | Development     |
| Borg       | Management      |
| Maxima     | Development     |
+------------+-----------------+

清单 14-6 只产生 9 行,而不是 10 行。这是因为在董事表中有一行没有经理标识,因为她是老板。因此,在董事表中没有与 mgr_id = dir_head_id 匹配的相应行。

外部连接

外部连接类似于内部连接,但是在这种情况下,我们感兴趣的是从左表、右表或两个表中获取所有的行。也就是说,无论另一个表中是否有匹配的行,我们都包括指定表中的行(左、右或左、右——也称为全表)。这些操作中的每一个都可以用一般外连接算法的微小变化来表示。

用户通过在FROM子句中列出所需的条件并调用其中一个选项(leftrightfull),在 SQL SELECT命令中指定外部连接。如果省略选项,一些数据库系统默认使用left。例如,以下命令将结果集从staff表连接到directorate表,返回所有雇员和董事会的结果集:

SELECT staff.last_name, directorate.dir_name
FROM staff LEFT OUTER JOIN directorate on staff.mgr_id = directorate.dir_head_id;

请注意,这不同于内部连接,因为左侧表中的行没有被省略。清单 14-7 显示了基本的外连接算法。以下部分描述了该算法如何实现三种类型的外部联接。

清单 14-7。 外连接算法

begin
  sort relation a as rel_a on join column(s)
  sort relation b as rel_b on join column(s)
  do
    get next tuple from rel_a
    get next tuple from rel_b
    if type is FULL
      if join column values match join conditions
        write attribute data from both tuples to client
      else
        if rel_a has data
          write NULLS for rel_b
        else if rel_b has data
          write NULLS for rel_a
        fi
    else if type is LEFT
      if join column values match join conditions
        write attribute data from rel_a to client
      else
        if rel_a has data
          write NULLS for rel_b
      fi
    else if type is RIGHT
      if join column values match join conditions
        write attribute data from rel_b to client
      else
        if rel_b has data
          write NULLS for rel_a
      fi
    fi
    check rewind conditions
  while not end_of_rel_a and not end_of_rel_b
end

接下来,我们讨论每种类型的外部连接的例子。

左外部连接

左外连接 包括左表中的所有行和右表中的行。对于那些不符合连接条件的行,将为右表中的列返回 null 值。

SELECT staff.last_name, directorate.dir_name
FROM staff LEFT OUTER JOIN directorate on staff.mgr_id = directorate.dir_head_id;

清单 14-8 显示了样本表的左外部连接的结果集。

清单 14-8。 示例左外连接结果集

+------------+-----------------+
| last_name  | dir_name        |
+------------+-----------------+
| Smith      | Management      |
| Walters    | Human Resources |
| St.Cruz    | NULL            |
| Hong       | Human Resources |
| Kardakarna | Development     |
| Smythe     | Development     |
| Jones      | Development     |
| Engles     | Development     |
| Borg       | Management      |
| Maxima     | Development     |
+------------+-----------------+

请注意,现在我们看到包含 staff 表中的行的行在 directorate 表中没有匹配的行。这是因为我们告诉优化器使用 staff 表中的所有行(连接规范左边的行)并匹配右边的表。因此,我们看到了外部连接的许多用途之一——它们可以用来识别任何不匹配。 3

右外部联接

右外连接 包括右表中的所有行和左表中的行。对于那些不符合连接条件的行,将为左表中的列返回 null 值。

SELECT staff.last_name, directorate.dir_name
FROM staff RIGHT OUTER JOIN directorate on staff.mgr_id = directorate.dir_head_id;

清单 14-9 显示了样本表的左外部连接的结果集。

清单 14-9。 例子右外连接结果集

+------------+-----------------+
| last_name  | dir_name        |
+------------+-----------------+
| Kardakarna | Development     |
| Smythe     | Development     |
| Jones      | Development     |
| Engles     | Development     |
| Maxima     | Development     |
| Walters    | Human Resources |
| Hong       | Human Resources |
| Smith      | Management      |
| Borg       | Management      |
+------------+-----------------+

现在我们回到九排。这是因为我们指示优化器使用右表中与左表匹配的所有行,并且由于董事表(连接规范右侧的行)中没有与 staff 表中的行不匹配的行,所以左表中不匹配的行被省略。

完全外部连接

完全外部连接包括两个表中连接在一起的所有行。对于那些不匹配连接条件的行,将不匹配表中的列返回 null 值。

SELECT staff.last_name, directorate.dir_name
FROM staff FULL OUTER JOIN directorate on staff.mgr_id = directorate.dir_head_id;

清单 14-10 显示了样本表的完全外部连接的结果集。

清单 14-10。 示例完整的外连接结果集

+------------+-----------------+
| last_name  | dir_name        |
+------------+-----------------+
| Smith      | Management      |
| Walters    | Human Resources |
| St.Cruz    | NULL            |
| Hong       | Human Resources |
| Kardakarna | Development     |
| Smythe     | Development     |
| Jones      | Development     |
| Engles     | Development     |
| Borg       | Management      |
| Maxima     | Development     |
+------------+-----------------+

虽然 MySQL 不支持完整的外部连接,但上面的输出是有代表性的。现在考虑一下,如果董事表包含一个没有 dir_head_id 的行,输出会是什么。我把这作为一个练习留给你去思考。

交叉积

叉积运算是结果集包含左表的每一行和右表的每一行的运算。因此,结果集包含n x m行,其中n表示左表中的行数,m表示右表中的行数。虽然概念简单,但并不是所有的数据库系统都支持跨产品操作。

image 注意在大多数数据库系统中,可以使用像SELECT * FROM table1, table2这样的查询来表示叉积查询。在这种情况下,没有连接条件,所以来自table1的所有行都与来自table2的所有行匹配。在 MySQL 上亲自尝试一下。您将看到 MySQL 使用这种方法支持跨产品操作。

用户通过在FROM子句中包含关键字CROSS代替JOIN来指定叉积运算。您可能认为这个操作的适用性有限,但是您会对它的有用性感到惊讶。假设你正在为人工智能算法的可能结果建模。您可能有存储可能的下一步行动(结果)的表和存储刺激的其他表。如果你想找到所有可能的组合,给定从一个表中选择的刺激列表和从另一个表中选择的对移动的可能影响,你可以产生一个显示所有组合的结果集。清单 14-11 展示了这样一个场景的例子。

清单 14-11。 示例跨产品场景

CREATE TABLE next_stim
SELECT source, stimuli_id FROM stimuli WHERE likelihood >= 0.75
+------------+------------+
| source     | stimuli_id |
+------------+------------+
| obstacle   | 13         |
| other_bot  | 14         |
| projectile | 15         |
| chasm      | 23         |
+------------+------------+

CREATE TABLE next_moves
SELECT move_name, next_move_id, likelihood FROM moves WHERE likelihood >= 0.90
+------------+--------------+------------+
| move_name  | next_move_id | likelihood |
+------------+--------------+------------+
| turn left  | 21           | 0.25       |
| reverse    | 18           | 0.40       |
| turn right | 22           | 0.45       |
+------------+--------------+------------+

SELECT * FROM next_stim CROSS next_moves
+------------+------------+------------+--------------+------------+
| source     | stimuli_id | move_name  | next_move_id | likelihood |
+------------+------------+------------+--------------+------------+
| obstacle   | 13         | turn left  | 21           | 0.25       |
| obstacle   | 13         | reverse    | 18           | 0.40       |
| obstacle   | 13         | turn right | 22           | 0.45       |
| other_bot  | 14         | turn left  | 21           | 0.25       |
| other_bot  | 14         | reverse    | 18           | 0.40       |
| other_bot  | 14         | turn right | 22           | 0.45       |
| projectile | 15         | turn left  | 21           | 0.25       |
| projectile | 15         | reverse    | 18           | 0.40       |
| projectile | 15         | turn right | 22           | 0.45       |
| chasm      | 23         | turn left  | 21           | 0.25       |
| chasm      | 23         | reverse    | 18           | 0.40       |
| chasm      | 23         | turn right | 22           | 0.45       |
+------------+------------+------------+--------------+------------+

清单 14-12 显示了叉积算法。请注意,这个示例是使用两个步骤编写的:一个步骤是合并行,另一个步骤是删除重复的行。

清单 14-12。 叉积算法

begin
  do
    get next tuple from rel_a
    do
      get next tuple from rel_b
      write tuple from rel_a concat tuple from rel_b to client
    while not end_of_rel_b
  while not end_of_rel_a
  remove duplicates from temp_table
  return data from temp_table to client
end

从清单中可以看出,实现该算法的代码实际上是两个循环中的一个,其中左表中的行与右表中的行连接在一起(即嵌套循环算法)。

联盟

union 运算与同名的 set 运算相同。在这种情况下,联接是两个表中所有行的联合,其中删除了重复的行。当然,为了使该操作能够进行,这些表必须具有相同的设计。这不同于 SQL union,因为 SQL union 包括来自所有SELECT命令的行(具有兼容的列列表),而不考虑重复。与其他连接不同,union 操作的实现有时分两步实现:一步是合并表,另一步是删除重复的表。

用户通过在FROM子句中包含关键字UNION代替JOIN来指定 union 命令。假设您想合并两个雇员表(一个包括在美国工作的所有雇员,另一个包括在加拿大工作的雇员),并确保您得到的结果集只列出所有雇员一次。您可以使用如下命令将两者结合起来:

SELECT * from us_employees UNION ca_employees

让我们再仔细看看这个。清单 14-13 展示了提到的表格的例子。快速浏览一下就会发现有两个雇员同时在美国和加拿大工作。如果您使用 SQL UNION命令,您将获得两个表的内容,这两个雇员被计数两次。清单 14-14 展示了使用样本表进行联合运算的结果。

清单 14-13。 样本员工表

US employees table
+------------+-----------+-----------+---------+
| first_name | last_name | id        | dept_id |
+------------+-----------+-----------+---------+
| Chad       | Borg      | 990441234 | 1       |
| Alicia     | Wallace   | 330506781 | 4       |
| Howard     | Bell      | 333445555 | 5       |
| Tamra      | English   | 453453453 | 5       |
| Bill       | Smith     | 123456789 | 5       |
+------------+-----------+-----------+---------+

Canada employees table
+------------+-----------+-----------+---------+
| first_name | last_name | id        | dept_id |
+------------+-----------+-----------+---------+
| William    | Wallace   | 220059009 | <null>  |
| Aaron      | Hill      | 987987987 | 4       |
| Lillian    | Wallace   | 987654321 | 4       |
| Howard     | Bell      | 333445555 | 5       |
| Bill       | Smith     | 123456789 | 5       |
+------------+-----------+-----------+---------+

清单 14-14。 示例联合结果集

+------------+-----------+-----------+---------+
| first_name | last_name | id        | dept_id |
+------------+-----------+-----------+---------+
| Chad       | Borg      | 990441234 | 1       |
| Alicia     | Wallace   | 330506781 | 4       |
| Howard     | Bell      | 333445555 | 5       |
| Tamra      | English   | 453453453 | 5       |
| Bill       | Smith     | 123456789 | 5       |
| William    | Wallace   | 220059009 | <null>  |
| Aaron      | Hill      | 987987987 | 4       |
| Lillian    | Wallace   | 987654321 | 4       |
+------------+-----------+-----------+---------+

清单 14-15 显示了联合算法。请注意,该示例使用两个步骤来合并和删除重复项。

Note In MySQL, you can use the ALL option for the UNION clause to skip removal of duplicates.

清单 14-15。 联邦算法

begin
  do
    get next tuple from rel_a
    write tuple from rel_a to temp_table
    get next tuple from rel_b
    write tuple from rel_b to temp_table
  while not (end_of_rel_a or end_of_rel_b)
  remove duplicates from temp_table
  return data from temp_table to client
end

横断

交集运算与同名集合运算相同。在这种情况下,联接是两个表中的行的交集,删除了重复的行。当然,为了使该操作能够进行,这些表必须具有相同的设计。

用户通过在FROM子句中包含关键字INTERSECT代替JOIN来指定交集操作。假设您想要合并两个雇员表(一个包含所有在美国工作的雇员,另一个包含在加拿大工作的雇员),并确保您得到的结果集包含所有在美国和加拿大工作的雇员。您可以使用如下命令将两者相交:

SELECT * from us_employees INTERSECT ca_employees

让我们更仔细地看看这个。使用清单 14-13 中的示例表,您会看到有两名雇员同时在美国和加拿大工作。清单 14-16 显示了使用样本表进行交集运算的结果。清单 14-17 显示了交集算法。

清单 14-16。 示例相交结果集

+------------+-----------+-----------+---------+
| first_name | last_name | id        | dept_id |
+------------+-----------+-----------+---------+
| Howard     | Bell      | 333445555 | 5       |
| Bill       | Smith     | 123456789 | 5       |
+------------+-----------+-----------+---------+

清单 14-17。 相交算法

begin
  do
    get next tuple from rel_a
    get next tuple from rel_b
    if join column values match intersection conditions
      write tuple from rel_a to client
  while not (end_of_rel_a or end_of_rel_b)
end

DBXP 查询执行

DBXP 中的查询执行是使用优化的查询树 完成的。树结构本身被用作处理查询的管道。当执行查询时,在根节点的每个子节点上发出一个get_next()方法 。另一个get_next()方法调用是对它们的每个子节点进行的。当树被遍历到包含对单个表的引用的树的最低层时,该过程继续。考虑以下查询:

SELECT col1, col2 FROM table_a JOIN
(SELECT col2, col8 FROM table_b WHERE col6 = 7)
ON col8 WHERE table_a.col7 > 14

该查询正在从table_a中检索与table_b中的数据子集相匹配的数据。注意,我将子集写成了一个子查询。查询树执行很容易适应子查询机制,其中子查询将被表示为一个子树。图 14-2 显示了该概念的高级视图。

9781430246596_Fig14-02.jpg

图 14-2。查询树执行

对关系中的一行执行每个节点的操作。完成后,该操作的结果将传递给树中的下一个操作。如果没有产生任何结果,控制将保留在当前节点中,直到产生一个结果或者没有更多结果要处理。当树被爬回到根节点时,结果被依次传递给每个父节点,直到到达根节点。一旦根节点中的操作完成,产生的元组被传递给客户机。以这种方式,查询的执行看起来产生结果更快,因为数据(结果)被显示给客户机比在任何结果被给予客户机之前对整个操作集执行查询要早得多。

Query_tree类 被设计成包括执行查询所必需的操作。包括投影、限制和连接操作。在查询执行开始时调用一个prepare()方法。prepare()方法遍历查询树,初始化所有要执行的节点。执行是通过使用一个while循环完成的,该循环遍历结果集,从根节点开始向树发出一个脉冲。脉冲是对get_next()方法的调用,它沿着树向下传播。每个被脉冲化的节点向它的每个子节点发出一个脉冲,从左边的子节点开始。为以下每个操作提供了单独的参数化方法:do_restrict()do_project(), 和do_join()``.4这些方法使用一个或两个元组作为输入进行操作,并返回 null 或元组。空返回指示一个或多个元组不满足当前操作。例如,接受元组的do_restrict()操作使用 expression 类来计算元组中的值。如果表达式的计算结果为 false,则返回 null 结果。如果表达式的计算结果为 true,则返回相同的元组。 5

这个过程在整个树中重复,将单个元组沿树向上传递到根。来自根的结果元组然后由外部的while循环处理,并通过现有的 MySQL 客户端通信协议呈现给客户端。由于遍历节点的方式,这种执行形式被称为管道,通过树传递单个节点,从而通过查询中的所有操作。

设计测试

为查询执行创建全面的测试需要编写涵盖优化器中所有可能路径的 SQL 语句。本质上,您需要创建一个测试来测试所有可能的查询,包括有效的和无效的。然而,DBXP 的执行是不完整的。尽管 project 和 restrict 操作已经完全实现,但是在do_join()方法中只实现了内部连接。这允许您在稳定的 DBXP 环境中为剩余的连接操作创建自己的实现。

记住这一点,让我们设计几个测试执行引擎的基本查询,看看 DBXP 引擎是如何处理查询的。清单 14-18 显示了一个测试查询优化器的例子。您可以随意添加自己的查询来测试 DBXP 引擎的局限性。

清单 14-18。 示例 DBXP 查询-执行测试(Ch14.test)

#
# Sample test to test the DBXP_SELECT execution
#

# Test 1:
DBXP_SELECT first_name, last_name, sex, id FROM staff;

# Test 2:
DBXP_SELECT id FROM staff;

# Test 3:
DBXP_SELECT dir_name FROM directorate;

# Test 4a:
DBXP_SELECT id, dir_name FROM staff
JOIN directorate ON staff.mgr_id = directorate.dir_head_id;

# Test 4b:
DBXP_SELECT id, dir_name FROM staff, directorate
WHERE staff.mgr_id = directorate.dir_head_id;

# Test 5:
DBXP_SELECT * FROM staff WHERE staff.id = '123456789';

# Test 6:
DBXP_SELECT first_name, last_name FROM staff join directorate ON staff.mgr_id = directorate.dir_head_id
WHERE directorate.dir_code = 'N41';

# Test 7:
DBXP_SELECT * FROM directorate JOIN building ON directorate.dir_code = building.dir_code;

# Test 8:
DBXP_SELECT directorate.dir_code, dir_name, building, dir_head_id
FROM directorate JOIN building ON directorate.dir_code = building.dir_code;

注意,测试用例 4 有两个查询。这两个不同的查询会显示什么?我为什么要包括他们?我包含它们是因为它们实际上是相同的查询(它们生成相同的结果集)。由于 SQL 命令结构 6 的灵活性,可以使用几种语法形式编写一个查询。在这种情况下,它们应该产生相同的连接,从而产生相同的输出。这也是对优化器和执行引擎的一个很好的测试。

image 提示这些例子中使用的数据库包含在附录中。

关于如何使用 MySQL 测试套件在清单 14-18 中创建和运行测试的更多细节,请参考第 4 章

更新 DBXP 选择命令

既然我们现在有了执行查询的方法,我们可以用运行SELECT命令的代码替换DBXP_select_command()方法中的代码。这个方法将检查表访问、打开和锁定表、执行查询、将结果发送到客户机,以及解锁表。清单 14-19 显示了完成的DBXP_select_command()

清单 14-19。 完成 DBXP 选择命令

/*
  Perform Select Command

  SYNOPSIS
    DBXP_select_command()
    THD *thd            IN the current thread

  DESCRIPTION
    This method executes the SELECT command using the query tree and optimizer.

  RETURN VALUE
    Success = 0
    Failed = 1
*/
int DBXP_select_command(THD *thd)
{
  bool res;
  READ_RECORD *record;
  select_result *result = thd->lex->result;

  DBUG_ENTER("DBXP_select_command");

  /* Prepare the tables (check access, locks) */
  res = check_table_access(thd, SELECT_ACL, thd->lex->query_tables, 0, 1, 1);
  if (res)
    DBUG_RETURN(1);
  res = open_and_lock_tables(thd, thd->lex->query_tables, 0,
                             MYSQL_LOCK_IGNORE_TIMEOUT);
  if (res)
    DBUG_RETURN(1);

  /* Create the query tree and optimize it */
  Query_tree *qt = build_query_tree(thd, thd->lex,
                     (TABLE_LIST*) thd->lex->select_lex.table_list.first);
  qt->heuristic_optimization();
  qt->cost_optimization();
  qt->prepare(qt->root);
  if (!(result= new select_send()))
    DBUG_RETURN(1);

  /* use the protocol class to communicate to client */
  Protocol *protocol= thd->protocol;

  /* write the field list for returning the query results */
  if (protocol->send_result_set_metadata(&qt->result_fields,
                            Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF))
    DBUG_RETURN(1);

  /* pulse the execution engine to get a row from the result set */
  while (!qt->Eof(qt->root))
  {
    record = qt->get_next(qt->root);
    if (record != NULL)
    {
      /* send the data to the client */
      send_data(protocol, qt->result_fields, thd);
    }
  }
  my_eof(thd);

  /* unlock tables and cleanup memory */
  qt->cleanup(qt->root);
  mysql_unlock_read_tables(thd, thd->lock);
  delete qt;
  DBUG_RETURN(0);
}

这个实现现在拥有了执行查询所需的所有元素。它从检查表访问和打开表开始。假设这些步骤成功完成,接下来是 DBXP 查询引擎调用,首先构建查询树,然后优化,最后循环执行查询。

注意,这个循环是一个简单的while not end of file循环,它调用根节点上的get_next()方法。如果返回一个元组(记录),代码将该行写入客户端;否则,它调用get_next()方法,直到检测到文件的结尾。处理完所有元组后,代码释放所有使用的内存并解锁表。

因为我将向客户机发送数据的代码放在了查询树方法之外的一个地方,所以所有关系操作的实现都简化了一些。正如您将在下一节中看到的,查询树方法类似于那些理论算法。

DBXP 算法

现在操作 DBXP 查询引擎的代码已经完成,让我们将注意力转向 DBXP Query_tree类如何实现关系操作。

项目

DBXP 项目操作 在Query_tree类的一个名为do_project()的方法中实现。这种方法很容易实现,因为 MySQL 基类提供了一种快速的方法来进行投影。我们可以使用 MySQL 基类将数据发送给客户机,而不是遍历行中的属性。

do_project()方法可以简化为只将当前行存储在缓冲区中,并将该行返回到树中的下一个节点。当控制返回到DBXP_select_command()方法时,一个名为send_data()的助手方法被用来将数据发送到客户端。清单 14-20 显示了do_project()方法的代码。

清单 14-20。 DBXP 项目方法

/*
  Perform project operation.

  SYNOPSIS
    do_project()
    query_node *qn IN the operational node in the query tree.
    READ_RECORD *t -- the tuple to apply the operation to.

  DESCRIPTION
    This method performs the relational model operation entitled "project".
    This operation is a narrowing of the result set vertically by
    restricting the set of attributes in the output tuple.

  NOTES
    Returns 0 (null) if no tuple satisfies child operation (does NOT indicate
    the end of the file or end of query operation. Use Eof() to verify.

  RETURN VALUE
    Success = new tuple with correct attributes
    Failed = NULL
*/
READ_RECORD *Query_tree::do_project(query_node *qn, READ_RECORD *t)
{
  DBUG_ENTER("do_project");
  if (t != NULL)
  {
    if (qn == root)

      /*
         If the left table isn't NULL, copy the record buffer from
         the table into the record buffer of the relations class.
         This completes the read from the storage engine and now
         provides the data for the projection (which is accomplished
         in send_data().
      */
      if (qn->relations[0] != NULL)
        memcpy((uchar *)qn->relations[0]->table->record[0],
          (uchar *)t->rec_buf,
          qn->relations[0]->table->s->rec_buff_length);
  }
  DBUG_RETURN(t);
}

注意,在这段代码中,所要做的就是将从存储引擎读取的数据复制到表对象的记录缓冲区中。我通过将从存储引擎读取的READ_RECORD中的内存复制到表的第一个READ_RECORD 缓冲区来实现这一点,复制的字节数在表的rec_buff_length属性中指定。

限制

DBXP 限制操作 在Query_tree类的一个名为do_restrict()的方法中实现。代码使用了包含Expression助手类实例化的Query_tree类的where_expr成员变量。因此,restrict 操作的实现被简化为调用Expression类的evaluate()方法。清单 14-21 显示了do_restrict()方法的代码。

清单 14-21。 DBXP 限制方法

/*
  Perform restrict operation.

  SYNOPSIS
    do_restrict()
    query_node *qn IN the operational node in the query tree.
    READ_RECORD *t -- the tuple to apply the operation to.

  DESCRIPTION
    This method performs the relational model operation entitled "restrict".
    This operation is a narrowing of the result set horizontally by
    satisfying the expressions listed in the where clause of the SQL
    statement being executed.

  RETURN VALUE
    Success = true
    Failed = false
*/
bool Query_tree::do_restrict(query_node *qn, READ_RECORD *t)
{
  bool found = false;

  DBUG_ENTER("do_restrict");
  if (qn != NULL)
  {
    /*
       If the left table isn't NULL, copy the record buffer from
       the table into the record buffer of the relations class.
       This completes the read from the storage engine and now
       provides the data for the projection (which is accomplished
       in send_data().

       Lastly, evaluate the where clause. If the where clause
       evaluates to true, we keep the record else we discard it.
    */
    if (qn->relations[0] != NULL)
      memcpy((uchar *)qn->relations[0]->table->record[0], (uchar *)t->rec_buf,
        qn->relations[0]->table->s->rec_buff_length);
    if (qn->where_expr != NULL)
      found = qn->where_expr->evaluate(qn->relations[0]->table);
  }
  DBUG_RETURN(found);
}

当发现匹配时,数据被复制到表的记录缓冲区。这将当前记录缓冲区中的数据与表相关联。它还允许使用许多 MySQL 方法来操作字段和向客户端发送数据。

加入

DBXP join 操作 在Query_tree类的一个名为do_join()的方法中实现。代码使用了包含Expression助手类实例化的Query_tree类的join_expr成员变量。连接条件评估的实现因此被简化为调用Expression类的evaluate()方法。

这个方法是所有 DBXP 代码中最复杂的。之所以如此复杂,部分原因在于必须对连接进行评估的许多条件。前面描述的理论上的连接算法和显示的例子说明了复杂性。我将在这里详细阐述一下,为您检查do_join()源代码做准备。清单 14-22 给出了do_join()方法的简化伪代码。

清单 14-22。DBXP 加入算法

begin
  if preempt_pipeline
    do
      if no left child
        get next tuple from left relation
      else
        get next tuple from left child
      fi
      insert tuple in left buffer in order by join column for left relation
    until eof
    do
      if no right child
        get next tuple from right relation
      else
        get next tuple from right child
      fi
      insert tuple in right buffer in order by join column for right relation
    until eof
  fi
  if left record pointer is NULL
    get next tuple from left buffer
  fi
  if right record pointer is NULL
    get next tuple from right buffer
  fi
  if there are tuples to process
    write attribute data of both tuples to table record buffers
    if join column values match join conditions
      check rewind conditions
      clear record pointers
      check for end of file
      set return record to left record pointer (indicates a match)
 else if left join value < right tuple join value
      set return record to NULL (no match)
      set left record pointer to NULL
    else if left join value > right tuple join value
      set return record to NULL (no match)
      set right record pointer to NULL
    fi
  else
    set return record to NULL (no match)
  fi
end

由于 join 方法是从get_next()方法中反复调用的,因此算法被修改为使用来自query_nodepreempt_pipeline成员变量。在执行查询树之前的prepare()方法期间,该变量被设置为TRUE。这允许 join 方法检测何时进行第一次调用,以便可以创建临时缓冲区。这样,树的遍历被抢先,直到第一个匹配的连接操作完成(或者如果没有匹配,则文件结束)。

请注意,该算法使用两个缓冲区来存储来自传入表的有序行。这些缓冲区用于读取连接操作的记录,并且使用每个缓冲区的记录指针来表示它们。如果发现匹配,两个记录指针都被设置为NULL,这迫使代码读取下一个记录。如果连接条件的评估表明来自左表的连接值小于右表的值,则左记录指针被设置为NULL,以便在下一次调用do_join()方法时,从左记录缓冲区读取下一条记录。类似地,如果左连接值大于右连接值,则右记录指针被设置为NULL,并且在下一次调用时,从右记录缓冲区读取新记录。

既然已经解释了do_join()方法的基础知识,那么看一下源代码。清单 14-23 显示了do_join()方法的代码。

image 注意我选择不使用助手函数来为连接操作的第一步创建临时缓冲区,这样我可以将代码放在一起以便于调试。因此,这个决定纯粹是为了方便。如果您愿意,可以通过将这部分代码设置为帮助函数来节省一点代码。

清单 14-23。 DBXP 加入方法

/*
  Perform join operation.

  SYNOPSIS
    do_join()
    query_node *qn IN the operational node in the query tree.
    READ_RECORD *t -- the tuple to apply the operation to.

  DESCRIPTION
    This method performs the relational model operation entitled
    "join". This operation is the combination of two relations to
    form a composite view. This algorithm implements ALL variants
    of the join operation.

  NOTES
    Returns 0 (null) if no tuple satisfies child operation (does
    NOT indicate the end of the file or end of query operation.
    Use Eof() to verify.

  RETURN VALUE
    Success = new tuple with correct attributes
    Failed = NULL
*/
READ_RECORD *Query_tree::do_join(query_node *qn)
{
  READ_RECORD *next_tup=0;
  int i;
  TABLE *ltable = NULL;
  TABLE *rtable = NULL;
  Field *fright = NULL;
  Field *fleft = NULL;
  record_buff *lprev=0;
  record_buff *rprev=0;
  expr_node *expr;

  DBUG_ENTER("do_join");
  if (qn == NULL)
    DBUG_RETURN(NULL);

  /* check join type because some joins require other processing */
  switch (qn->join_type)
  {
    case (jnUNKNOWN) :
      break;
    case (jnINNER) :
    case (jnLEFTOUTER) :
    case (jnRIGHTOUTER) :
    case (jnFULLOUTER) :
    {

      /*
        preempt_pipeline == true means we need to stop the pipeline
        and sort the incoming rows. We do that by making an in-memory
        copy of the record buffers stored in left_record_buff and
        right_record_buff
      */
      if (qn->preempt_pipeline)
      {
        left_record_buff = NULL;
        right_record_buff = NULL;
        next_tup = NULL;

        /* Build buffer for tuples from left child. */
        do
        {
          /* if left child exists, get row from it */
          if (qn->left != NULL)
            lbuff = get_next(qn->left);

          /* else, read the row from the table (the storage handler) */
          else
          {
            /*
               Create space for the record buffer and
               store pointer in lbuff
            */
            lbuff = (READ_RECORD *) my_malloc(sizeof(READ_RECORD),
                                      MYF(MY_ZEROFILL | MY_WME));
            lbuff->rec_buf =
              (uchar *) my_malloc(qn->relations[0]->table->s->rec_buff_length,
                                      MYF(MY_ZEROFILL | MY_WME));

            /* check for end of file. Store result in eof array */
            qn->eof[0] =
              qn->relations[0]->table->file->ha_rnd_next(lbuff->rec_buf);
            if (qn->eof[0] != HA_ERR_END_OF_FILE)
              qn->eof[0] = false;
            else
            {
              lbuff = NULL;
              qn->eof[0] = true;
            }
          }
          /* if the left buffer is not null, get a new row from table */
          if (lbuff != NULL)
          {
            /* we need the table information for processing fields */
            if (qn->left == NULL)
              ltable = qn->relations[0]->table;
            else
              ltable = get_table(qn->left);
            if (ltable != NULL)
              memcpy((uchar *)ltable->record[0], (uchar *)lbuff->rec_buf,
                ltable->s->rec_buff_length);

            /* get the join expression */
            expr = qn->join_expr->get_expression(0);
            for (Field **field = ltable->field; *field; field++)
              if (strcasecmp((*field)->field_name, ((Field *)expr->left_op)->field_name)==0)
                fleft = (*field);

            /*
               If field was found, add the row to the in-memory buffer
               ordered by the join column.
            */
            if ((fleft != NULL) && (!fleft->is_null()))
              insertion_sort(true, fleft, lbuff);
          }
        } while (lbuff != NULL);
        /* Build buffer for tuples from right child. */
        do
        {
          /* if right child exists, get row from it */
          if (qn->right != NULL)
            rbuff = get_next(qn->right);

          /* else, read the row from the table (the storage handler) */
          else
          {
            /*
               Create space for the record buffer and
               store pointer in rbuff
            */
            rbuff = (READ_RECORD *) my_malloc(sizeof(READ_RECORD),
                                      MYF(MY_ZEROFILL | MY_WME));
            rbuff->rec_buf =
              (uchar *) my_malloc(qn->relations[0]->table->s->rec_buff_length,
                                      MYF(MY_ZEROFILL | MY_WME));

            /* check for end of file. Store result in eof array */
            qn->eof[1] =
              qn->relations[1]->table->file->ha_rnd_next(rbuff->rec_buf);
            if (qn->eof[1] != HA_ERR_END_OF_FILE)
              qn->eof[1] = false;
            else
            {
              rbuff = NULL;
              qn->eof[1] = true;
            }
          }
          /* if the right buffer is not null, get a new row from table */
          if (rbuff != NULL)
          {
            /* we need the table information for processing fields */
            if (qn->right == NULL)
              rtable = qn->relations[1]->table;
            else
              rtable = get_table(qn->right);
            if (rtable != NULL)
              memcpy((uchar *)rtable->record[0], (uchar *)rbuff->rec_buf,
                rtable->s->rec_buff_length);

            /* get the join expression */
            expr = qn->join_expr->get_expression(0);
            for (Field **field = rtable->field; *field; field++)
              if (strcasecmp((*field)->field_name, ((Field *)expr->right_op)->field_name)==0)
                fright = (*field);

            /*
               If field was found, add the row to the in-memory buffer
               ordered by the join column.
            */
            if ((fright != NULL) && (!fright->is_null()))
              insertion_sort(false, fright, rbuff);
          }
        } while (rbuff != NULL);
        left_record_buffer_ptr = left_record_buff;
        right_record_buffer_ptr = right_record_buff;
        qn->preempt_pipeline = false;
      }
      /*
        This is where the actual join code begins.
        We get a tuple from each table and start the compare.
      */

      /*
         if lbuff is null and the left record buffer has data
         get the row from the buffer
      */
      if ((lbuff == NULL) && (left_record_buffer_ptr != NULL))
      {
        lbuff = left_record_buffer_ptr->record;
        lprev = left_record_buffer_ptr;
        left_record_buffer_ptr = left_record_buffer_ptr->next;
      }

      /*
         if rbuff is null and the right record buffer has data
         get the row from the buffer
      */
      if ((rbuff == NULL) && (right_record_buffer_ptr != NULL))
      {
        rbuff = right_record_buffer_ptr->record;
        rprev = right_record_buffer_ptr;
        right_record_buffer_ptr = right_record_buffer_ptr->next;
      }

      /*
        if the left buffer was null, check to see if a row is
        available from left child.
      */
      if (ltable == NULL)
      {
        if (qn->left == NULL)
          ltable = qn->relations[0]->table;
        else
          ltable = get_table(qn->left);
      }
      /*
        if the right buffer was null, check to see if a row is
        available from right child.
      */
      if (rtable == NULL)
      {
        if (qn->right == NULL)
          rtable = qn->relations[1]->table;
        else
          rtable = get_table(qn->right);
      }
      /*
        If there are two rows to compare, copy the record buffers
        to the table record buffers. This transfers the data
        from the internal buffer to the record buffer. It enables
        us to reuse the MySQL code for manipulating fields.
      */
      if ((lbuff != NULL) && (rbuff != NULL))
      {
        memcpy((uchar *)ltable->record[0], (uchar *)lbuff->rec_buf,
          ltable->s->rec_buff_length);
        memcpy((uchar *)rtable->record[0], (uchar *)rbuff->rec_buf,
          rtable->s->rec_buff_length);

        /* evaluate the join condition */
        i = qn->join_expr->compare_join(qn->join_expr->get_expression(0),
          ltable, rtable);

        /* if there is a match...*/
        if (i == 0)
        {
          /* return the row in the next_tup pointer */
          next_tup = lbuff;

          /* store next rows from buffer (already advanced 1 row) */
          record_buff *left = left_record_buffer_ptr;
          record_buff *right = right_record_buffer_ptr;

          /*
             Check to see if either buffer needs to be rewound to
             allow us to process many rows on one side to one row
             on the other
          */
          check_rewind(left_record_buffer_ptr, lprev,
            right_record_buffer_ptr, rprev);

          /* set poointer to null to force read on next loop */
          lbuff = NULL;
          rbuff = NULL;

          /*
            If the left buffer has been changed and if the
            buffer is not at the end, set the buffer to the next row.
          */
          if (left != left_record_buffer_ptr)
          {
            if (left_record_buffer_ptr != NULL)
            {
              lbuff = left_record_buffer_ptr->record;
            }
          }

          /*
            If the right buffer has been changed and if the
            buffer is not at the end, set the buffer to the next row.
          */
          if (right != right_record_buffer_ptr)
          {
            if (right_record_buffer_ptr != NULL)
            {
              rbuff = right_record_buffer_ptr->record;
            }
          }

          /* Now check for end of file and save results in eof array */
          if (left_record_buffer_ptr == NULL)
            qn->eof[2] = true;
          else
            qn->eof[2] = false;
          if (right_record_buffer_ptr == NULL)
            qn->eof[3] = true;
          else
            qn->eof[3] = false;
        }

        /* if the rows didn't match...*/
        else
        {
          /* get next rows from buffers (already advanced) */
          record_buff *left = left_record_buffer_ptr;
          record_buff *right = right_record_buffer_ptr;

          /*
             Check to see if either buffer needs to be rewound to
             allow us to process many rows on one side to one row
             on the other. The results of this rewind must be
             saved because there was no match and we may have to
             reuse one or more of the rows.
          */
          check_rewind(left_record_buffer_ptr, lprev,
            right_record_buffer_ptr, rprev);

          /*
            If the left buffer has been changed and if the
            buffer is not at the end, set the buffer to the next row
            and copy the data into the record buffer/
          */
          if (left != left_record_buffer_ptr)
          {
            if (left_record_buffer_ptr != NULL)
            {
              memcpy((uchar *)ltable->record[0],
                (uchar *)left_record_buffer_ptr->record->rec_buf,
                ltable->s->rec_buff_length);
              lbuff = left_record_buffer_ptr->record;
            }
          }

          /*
            If the right buffer has been changed and if the
            buffer is not at the end, set the buffer to the next row
            and copy the data into the record buffer/
          */
          if (right_record_buffer_ptr != NULL)
            if ((right_record_buffer_ptr->next == NULL) &&
               (right_record_buffer_ptr->prev == NULL))
               lbuff = NULL;
          if (right != right_record_buffer_ptr)
          {
            if (right_record_buffer_ptr != NULL)
            {
              memcpy((uchar *)rtable->record[0],
                (uchar *)right_record_buffer_ptr->record->rec_buf,
                rtable->s->rec_buff_length);
              rbuff = right_record_buffer_ptr->record;
            }
          }

          /* Now check for end of file and save results in eof array */
          if (left_record_buffer_ptr == NULL)
            qn->eof[2] = true;
          else
            qn->eof[2] = false;
          if (right_record_buffer_ptr == NULL)
            qn->eof[3] = true;
          else
            qn->eof[3] = false;
          next_tup = NULL;
        }
      }
      else
      {
        next_tup = NULL; /* at end, return null */
      }
      break;
    }

    /* placeholder for exercise... */
    case (jnCROSSPRODUCT) :
    {
      break;
    }
    /*
      placeholder for exercises...
      Union and intersect are mirrors of each other -- same code will
      work for both except the dupe elimination/inclusion part (see below)
    */
    case (jnUNION) :
    case (jnINTERSECT) :
    {
      break;
    }
  }
  DBUG_RETURN(next_tup);
}

请注意,在代码中,在匹配之外的任何条件下,从代码返回的记录都被设置为NULL。这允许get_next()方法中的循环重复调用do_join()方法,直到返回一个匹配。这类似于进行do_restrict()方法调用的方式。

我还没有实现任何其他连接操作的代码。主要原因是它允许你试验代码(见本章末尾的练习)。幸运的是,您应该发现可以通过一些简单的修改来修改代码,以允许处理外部连接。为叉积、并集和交集运算添加代码可以通过实现本章第一部分描述的理论算法来完成。

在研究了该方法的伪代码之后,您会发现阅读代码变得更容易了。这段代码最复杂的部分是check_rewind()方法。这是作为类中的一个函数实现的,目的是使代码不那么复杂,更易于阅读。下一节将更详细地描述其他几个助手方法。

其他方法

几个 helper 方法组成了 DBXP 执行引擎 。表 14-1 列出了新方法及其用途。更复杂的方法将在下文中详细描述。

表 14-1 。DBXP 执行引擎助手方法

Class::方法 描述
Query_tree::get_next() 从子节点中检索下一个元组。
查询树::插入排序() 创建 READ_RECORD 指针的有序缓冲区。在连接操作中用于对传入的元组进行排序。
Query_tree::eof() 检查存储引擎或临时缓冲区的文件结束条件。
Query_tree::check_rewind() 检查记录缓冲区是否需要调整,以便为多个匹配重新读取元组。
send_data() 向客户端发送数据。请参见 sql_dbxp_parse.cc。
Expression::evaluate() 计算限制操作的 WHERE 子句。
表达式::compare_join() 计算连接操作的连接条件。
Handler::rnd_init() 初始化从存储引擎的读取(参见第 10 章)。
Handler::rnd_next() 从存储引擎读取下一个元组(参见第 10 章)。

get_next()方法

get_next()方法是 DBXP 中查询执行流程的核心。它负责调用实现查询操作的do_...方法。它在DBXP_select_command()方法的while循环中被调用一次。一旦该方法第一次被启动,它就对当前节点执行操作,调用子节点来获取它们的结果。以递归方式重复该过程,直到当前节点中的所有子节点都返回了单个元组。清单 14-24 显示了get_next()方法的代码。

清单 14-24。get _ next()方法

/*
  Get the next tuple (row) in the result set.

  SYNOPSIS
    Eof()
    query_node *qn IN the operational node in the query tree.

  DESCRIPTION
    This method is used to get the next READ_RECORD from the pipeline.
    The idea is to call prepare() after you've validated the query then call
    get_next to get the first tuple in the pipeline.

  RETURN VALUE
    Success = next tuple in the result set
    Failed = NULL
*/
READ_RECORD *Query_tree::get_next(query_node *qn)
{
  READ_RECORD *next_tup = NULL;
  DBUG_ENTER("get_next");

  /*
    For each of the possible node types, perform the query operation
    by calling the method for the operation. These implement a very
    high-level abstraction of the operation. The real work is left
    to the methods.
  */
  switch (qn->node_type)
  {
    /* placeholder for exercises... */
    case Query_tree::qntDistinct :
      break;

    /* placeholder for exercises... */
    case Query_tree::qntUndefined :
      break;

    /* placeholder for exercises... */
    case Query_tree::qntSort :
      if (qn->preempt_pipeline)
        qn->preempt_pipeline = false;
      break;

    /*
      For restrict, get a row (tuple) from the table and
      call the do_restrict method looping until a row is returned
      (data matches conditions), then return result to main loop
      in DBXP_select_command.
    */
    case Query_tree::qntRestrict :
      do
      {
        /* if there is a child, get row from child */
        if (qn->left != NULL)
          next_tup = get_next(qn->left);

        /* else get the row from the table stored in this node */
        else
        {
          /* create space for the record buffer */
          if (next_tup == NULL)
            next_tup = (READ_RECORD *) my_malloc(sizeof(READ_RECORD),
                                              MYF(MY_ZEROFILL | MY_WME));
          next_tup->rec_buf = (uchar *) my_malloc(qn->relations[0]->table->s->rec_buff_length,
                                            MYF(MY_ZEROFILL | MY_WME));

          /* read row from table (storage handler */
          qn->eof[0] = qn->relations[0]->table->file->ha_rnd_next(next_tup->rec_buf);

          /* check for end of file */
          if (qn->eof[0] != HA_ERR_END_OF_FILE)
            qn->eof[0] = false;
          else
          {
            qn->eof[0] = true;
            next_tup = NULL;
          }
        }

        /* if there is a row, call the do_restrict method */
        if (next_tup)
          if(!do_restrict(qn, next_tup))
          {
            /* if no row to return, free memory used */
            my_free(next_tup->rec_buf);
            my_free(next_tup);
            next_tup = NULL;
          }
      } while ((next_tup == NULL) && !Eof(qn));
      break;

    /*
      For project, get a row (tuple) from the table and
      call the do_project method. If successful,
      return result to main loop in DBXP_select_command.
    */
    case Query_tree::qntProject :
      /* if there is a child, get row from child */
      if (qn->left != NULL)
      {
        next_tup = get_next(qn->left);
        if (next_tup)
          if (!do_project(qn, next_tup))
          {
            /* if no row to return, free memory used */
            my_free(next_tup->rec_buf);
            my_free(next_tup);
            next_tup = NULL;
          }
      }

      /* else get the row from the table stored in this node */
      else
      {
        /* create space for the record buffer */
        if (next_tup == NULL)
          next_tup = (READ_RECORD *) my_malloc(sizeof(READ_RECORD),
                                            MYF(MY_ZEROFILL | MY_WME));
        next_tup->rec_buf = (uchar *) my_malloc(qn->relations[0]->table->s->rec_buff_length + 20,
                                          MYF(MY_ZEROFILL | MY_WME));

        /* read row from table (storage handler) */
        qn->eof[0] = qn->relations[0]->table->file->ha_rnd_next(next_tup->rec_buf);

        /* check for end of file */
        if (qn->eof[0] != HA_ERR_END_OF_FILE)
        {
          qn->eof[0] = false;
        }
        else
        {
          qn->eof[0] = true;
          next_tup = NULL;
        }

        /* if there is a row, call the do_project method */
        if (next_tup)
        {
          if (!do_project(qn, next_tup))
          {
            /* no row to return, free memory used */
            my_free(next_tup->rec_buf);
            my_free(next_tup);
            next_tup = NULL;
          }
        }
      }
      break;

    /*
      For join, loop until either a row is returned from the
      do_join method or we are at end of file for both tables.
      If successful (data matches conditions),
      return result to main loop in DBXP_select_command.
    */
    case Query_tree::qntJoin :
      do
      {
        if (next_tup)
        {
          /* if no row to return, free memory used */
          my_free(next_tup->rec_buf);
          my_free(next_tup);
          next_tup = NULL;
        }
        next_tup = do_join(qn);
      }
      while ((next_tup == NULL) && !Eof(qn));
      break;
  }
  DBUG_RETURN(next_tup);
}

send_data()方法

send_data()方法 是一个帮助路由,它使用 MySQL Protocol类向客户端写入数据,以处理通信杂务。这个方法是从 MySQL 源代码借鉴来的,并稍微改写了一下,以适应 DBXP 执行引擎的(相对)简单的执行。在这种情况下,Item超类用于通过item->send()方法将字段值发送给客户端。清单 14-25 显示了send_data()方法的代码。

清单 14-25。send _ data()方法

/*
  Send data

  SYNOPSIS
    send_data()
    Protocol *p        IN the Protocol class
    THD *thd           IN the current thread
    List<Item> *items  IN the list of fields identified in the row

  DESCRIPTION
    This method sends the data to the clien using the protocol class.

  RETURN VALUE
    Success = 0
    Failed = 1
*/
bool send_data(Protocol *protocol, List<Item> &items, THD *thd)
{

  /* use a list iterator to loop through items */
  List_iterator_fast<Item> li(items);

  char buff[MAX_FIELD_WIDTH];
  String buffer(buff, sizeof(buff), &my_charset_bin);
  DBUG_ENTER("send_data");

  /* this call resets the transmission buffers */
  protocol->prepare_for_resend();

  /* for each item in the list (a field), send data to the client */
  Item *item;
  while ((item=li++))
  {
    /*
      Use the MySQL send method for the item class to write to network.
      If unsuccessful, free memory and send error message to client.
    */
    if (item->send(protocol, &buffer))
    {
      protocol->free();        /* Free used buffer */
      my_message(ER_OUT_OF_RESOURCES, ER(ER_OUT_OF_RESOURCES), MYF(0));
      break;
    }
  }
  /* increment row count */
  thd->inc_sent_row_count(1);

  /* if network write was ok, return */
  if (thd->vio_ok())
    DBUG_RETURN(protocol->write());

  DBUG_RETURN(0);
}

该方法使用item类,调用send()方法并传入一个指向protocol类实例的指针。这就是将字段项的数据写入客户端的方式。send_data()方法是一种处理投影和连接列列表以完成操作的方法。这是 MySQL 源代码中最好的一点。但是,MySQL 类如何知道发送什么列呢?回头看看build_query_tree()法。回想一下,在select_lex类中有一个列表。DBXP 代码在这里显示的代码行中捕获这些字段。该列表直接来自SELECT命令中的列列表,并由解析器代码填充。

qt->result_fields = lex->select_lex.item_list;

这些字段在线程扩展结构中被捕获。MySQL 代码只是写出这个字段列表中的所有数据。

check_rewind()方法

这个方法是数据库文本中最常被忽略的连接算法的一部分。该方法调整来自表的行的缓冲区,以允许算法重用已处理的行。这是必要的,因为一个表中的一行可能与另一个表中的多行匹配。虽然该方法的概念相对简单,但自己编写代码可能是一个挑战。幸运的是,我帮你省了这个麻烦。

该代码通过检查记录缓冲区中的行来工作。它将指向记录缓冲区的指针以及缓冲区中的前一个记录指针作为输入。记录缓冲区被实现为一个双向链表,允许在缓冲区中来回移动。

这段代码必须处理几个条件,以保持数据流向do_join()方法。这些条件是在检测到匹配后对连接条件进行评估的结果。失败匹配的结果在do_join()方法中处理。

  • 如果左缓冲区中的下一条记录与右缓冲区匹配,则倒带右缓冲区,直到右缓冲区的连接条件小于左缓冲区。
  • 如果左缓冲区中的下一条记录与右缓冲区不匹配,则将右缓冲区设置为前一条右记录指针。
  • 如果左记录缓冲区位于末尾,并且右缓冲区中仍有记录,并且如果前一个左记录指针的联接值与右记录指针匹配,则将左记录指针设置为前一个左记录指针。

该方法在偏向左记录缓冲器的情况下实现。换句话说,代码保持右缓冲区与左缓冲区同步(也称为左深度连接执行)。清单 14-26 显示了check_rewind()方法的代码。

清单 14-26。check _ rewind()方法

/*
  Adjusts pointers to record buffers for join.

  SYNOPSIS
    check_rewind()
    record_buff *cur_left IN the left record buffer
    record_buff *cur_left_prev IN the left record buffer previous
    record_buff *cur_right IN the left record buffer
    record_buff *cur_right_prev IN the left record buffer previous

  DESCRIPTION
    This method is used to push a tuple back into the buffer
    during a join operation that preempts the pipeline.

  NOTES
    Now, here's where we have to check the next tuple in each
    relation to see if they are the same. If one of them is the
    same and the other isn't, push one of them back.

    We need to rewind if one of the following is true:
    1\. The next record in R2 has the same join value as R1
    2\. The next record in R1 has the same join value as R2
    3\. The next record in R1 has the same join value and R2 is
       different (or EOF)
    4\. The next record in R2 has the same join value and R1 is
       different (or EOF)

  RETURN VALUE
    Success = int index number
    Failed = -1
*/
int Query_tree::check_rewind(record_buff *cur_left,
                             record_buff *curr_left_prev,
                             record_buff *cur_right,
                             record_buff *curr_right_prev)
{
  record_buff *left_rcd_ptr = cur_left;
  record_buff *right_rcd_ptr = cur_right;
  int i;
  DBUG_ENTER("check_rewind");

  /*
    If the next tuple in right record is the same as the present tuple
      AND the next tuple in right record is different, rewind until
      it is the same
    else
      Push left record back.
  */

  /* if both buffers are at EOF, return -- nothing to do */
  if ((left_rcd_ptr == NULL) && (right_rcd_ptr == NULL))
    DBUG_RETURN(0);

  /* if the currently processed record is null, get the one before it */
  if (cur_right == NULL)
    right_rcd_ptr = curr_right_prev;

  /*
    if left buffer is not at end, check to see
    if we need to rewind right buffer
  */
  if (left_rcd_ptr != NULL)
  {
    /* compare the join conditions to check order */
    i = memcmp(left_rcd_ptr->field_ptr, right_rcd_ptr->field_ptr,
      left_rcd_ptr->field_length < right_rcd_ptr->field_length ?
      left_rcd_ptr->field_length : right_rcd_ptr->field_length);

    /*
      i == 0 means the rows are the same. In this case, we need to
      check to see if we need to advance or rewind the right buffer.
    */
    if (i == 0)
    {
      /*
        If there is a next row in the right buffer, check to see
        if it matches the left row. If the right row is greater
        than the left row, rewind the right buffer to one previous
        to the current row or until we hit the start.
      */
      if (right_rcd_ptr->next != NULL)
      {
        right_rcd_ptr = right_rcd_ptr->next;
        i = memcmp(left_rcd_ptr->field_ptr, right_rcd_ptr->field_ptr,
          left_rcd_ptr->field_length < right_rcd_ptr->field_length ?
          left_rcd_ptr->field_length : right_rcd_ptr->field_length);
        if (i > 0)
        {
          do
          {
            if (right_rcd_ptr->prev != NULL)
            {
              right_rcd_ptr = right_rcd_ptr->prev;
              i = memcmp(left_rcd_ptr->field_ptr, right_rcd_ptr->field_ptr,
                left_rcd_ptr->field_length < right_rcd_ptr->field_length ?
                left_rcd_ptr->field_length : right_rcd_ptr->field_length);
            }
          }
          while ((i == 0) && (right_rcd_ptr->prev != NULL));

          /* now advance one more to set pointer to correct location */
          if (right_rcd_ptr->next != NULL)
            right_rcd_ptr = right_rcd_ptr->next;
        }
        /* if no next right row, rewind to previous row */
        else
          right_rcd_ptr = right_rcd_ptr->prev;
      }
      /*
        If there is a next row in the left buffer, check to see
        if it matches the right row. If there is a match and the right
        buffer is not at start, rewind the right buffer to one previous
        to the current row.
      */
      else if (left_rcd_ptr->next != NULL)
      {
        if (right_rcd_ptr->prev != NULL)
        {
          i = memcmp(left_rcd_ptr->field_ptr, right_rcd_ptr->prev->field_ptr,
            left_rcd_ptr->field_length < right_rcd_ptr->prev->field_length ?
            left_rcd_ptr->field_length : right_rcd_ptr->prev->field_length);
        }
        if ((i == 0) && (right_rcd_ptr->prev != NULL))
          right_rcd_ptr = right_rcd_ptr->prev;
      }
    }
    /* if the left row is less than right row, rewind right buffer */
    else if (i < 0)
    {
      if (right_rcd_ptr->prev != NULL)
        right_rcd_ptr = right_rcd_ptr->prev;
    }
    /* if the right row is less than the left row, advance right row */
    else
    {
      if (right_rcd_ptr->next != NULL)
        right_rcd_ptr = right_rcd_ptr->next;
    }
  }
  /*
    Rows don't match, so advance the right buffer and check match again.
    if they still match, rewind left buffer.
  */
  else
  {
    if (right_rcd_ptr->next != NULL)
    {
      i = memcmp(curr_left_prev->field_ptr, right_rcd_ptr->field_ptr,
        curr_left_prev->field_length < right_rcd_ptr->field_length ?
        curr_left_prev->field_length : right_rcd_ptr->field_length);
      if (i == 0)
        left_rcd_ptr = curr_left_prev;
    }
  }
  /* set buffer pointers to adjusted rows from buffers */
  left_record_buffer_ptr = left_rcd_ptr;
  right_record_buffer_ptr = right_rcd_ptr;
  DBUG_RETURN(0);
}

既然您已经仔细查看了 DBXP 查询执行的源代码,那么是时候编译代码并进行测试了。

编译和测试代码

如果还没有,下载本章的源代码,并将文件放在源代码根目录下的/sql目录中。在示例代码中,您还会发现一个差异文件,您可以使用它将更改应用到服务器源代码文件(例如 mysqld.cc sql_cmd.h 等)。).或者你可以使用第 13 章中的修改,因为对服务器代码的修改是一样的。

花一些时间浏览源代码,以便熟悉这些方法。如果您需要调试代码以使用您的配置,或者如果您想要添加其他增强功能或进行练习,现在花时间浏览代码将会有所帮助。

image 提示第 11 章关于如何将源文件添加到项目中并编译它们的细节。

一旦安装并编译了新代码,就可以运行服务器并执行测试了。您可以运行之前创建的测试,或者在 MySQL 客户端实用程序中输入命令。清单 14-27 显示了运行测试中列出的命令的预期输出。

清单 14-27。 示例测试运行

mysql> SELECT DBXP first_name, last_name, sex, id FROM staff;

+------------+------------+------+-----------+
| first_name | last_name  | sex  | id        |
+------------+------------+------+-----------+
| John       | Smith      | M    | 333445555 |
| William    | Walters    | M    | 123763153 |
| Alicia     | St.Cruz    | F    | 333444444 |
| Goy        | Hong       | F    | 921312388 |
| Rajesh     | Kardakarna | M    | 800122337 |
| Monty      | Smythe     | M    | 820123637 |
| Richard    | Jones      | M    | 830132335 |
| Edward     | Engles     | M    | 333445665 |
| Beware     | Borg       | F    | 123654321 |
| Wilma      | Maxima     | F    | 123456789 |
+------------+------------+------+-----------+
10 rows in set (0.01 sec)

mysql> DBXP_SELECT id FROM staff;
+-----------+
| id        |
+-----------+
| 333445555 |
| 123763153 |
| 333444444 |
| 921312388 |
| 800122337 |
| 820123637 |
| 830132335 |
| 333445665 |
| 123654321 |
| 123456789 |
+-----------+
10 rows in set (0.00 sec)

mysql> DBXP_SELECT dir_name FROM directorate;
+-----------------+
| dir_name        |
+-----------------+
| Development     |
| Human Resources |
| Management      |
+-----------------+
3 rows in set (0.00 sec)

mysql> DBXP_SELECT id, dir_name FROM staff
JOIN directorate ON staff.mgr_id = directorate.dir_head_id;
+-----------+-----------------+
| id        | dir_name        |
+-----------+-----------------+
| 123763153 | Human Resources |
| 921312388 | Human Resources |
| 333445555 | Management      |
| 123654321 | Management      |
| 800122337 | Development     |
| 820123637 | Development     |
| 830132335 | Development     |
| 333445665 | Development     |
| 123456789 | Development     |
+-----------+-----------------+
9 rows in set (0.00 sec)

mysql> DBXP_SELECT id, dir_name FROM staff, directorate
WHERE staff.mgr_id = directorate.dir_head_id;
+-----------+-----------------+
| id        | dir_name        |
+-----------+-----------------+
| 123763153 | Human Resources |
| 921312388 | Human Resources |
| 333445555 | Management      |
| 123654321 | Management      |
| 800122337 | Development     |
| 820123637 | Development     |
| 830132335 | Development     |
| 333445665 | Development     |
| 123456789 | Development     |
+-----------+-----------------+
9 rows in set (0.00 sec)

mysql> DBXP_SELECT * FROM staff WHERE staff.id = '123456789';
+-----------+------------+----------+-----------+------+--------+-----------+
| id        | first_name | mid_name | last_name | sex  | salary | mgr_id    |
+-----------+------------+----------+-----------+------+--------+-----------+
| 123456789 | Wilma      | N        | Maxima    | F    |  43000 | 333445555 |
+-----------+------------+----------+-----------+------+--------+-----------+
1 row in set (0.00 sec)

mysql> DBXP_SELECT first_name, last_name FROM staff join directorate ON staff.mgr_id = directorate.dir_head_id WHERE directorate.dir_code = 'N41';
+------------+------------+
| first_name | last_name  |
+------------+------------+
| Rajesh     | Kardakarna |
| Monty      | Smythe     |
| Richard    | Jones      |
| Edward     | Engles     |
| Wilma      | Maxima     |
+------------+------------+
5 rows in set (0.00 sec)

mysql> DBXP_SELECT * FROM directorate JOIN building ON directorate.dir_code = building.dir_code;
+----------+-----------------+-------------+----------+----------+
| dir_code | dir_name        | dir_head_id | dir_code | building |
+----------+-----------------+-------------+----------+----------+
| M00      | Management      | 333444444   | M00      | 1000     |
| N01      | Human Resources | 123654321   | N01      | 1453     |
| N41      | Development     | 333445555   | N41      | 1300     |
| N41      | Development     | 333445555   | N41      | 1301     |
| N41      | Development     | 333445555   | N41      | 1305     |
+----------+-----------------+-------------+----------+----------+
5 rows in set (0.00 sec)

mysql> DBXP_SELECT directorate.dir_code, dir_name, building, dir_head_id
FROM directorate JOIN building ON directorate.dir_code = building.dir_code;
+----------+-----------------+----------+-------------+
| dir_code | dir_name        | building | dir_head_id |
+----------+-----------------+----------+-------------+
| M00      | Management      | 1000     | 333444444   |
| N01      | Human Resources | 1453     | 123654321   |
| N41      | Development     | 1300     | 333445555   |
| N41      | Development     | 1301     | 333445555   |
| N41      | Development     | 1305     | 333445555   |
+----------+-----------------+----------+-------------+
5 rows in set (0.00 sec)

mysql>

摘要

我在本章中介绍了内部数据库查询执行操作。您了解了如何扩展查询树的概念,以包含一个在执行过程中使用树结构的查询执行引擎。这些技术的知识应该能够让您更好地理解 DBXP 引擎,以及如何使用它来更深入地研究数据库技术。

你已经读到了这本书的结尾,可能想知道还有什么要做。本书的这一部分为您提供了一个基于 MySQL 的实验引擎,它将允许您探索自己的内部数据库技术的实现。最重要的是,您可以随意调整 DBXP 代码。也许您只是想试验一下,但也可能想实现 union 和 intersect 操作,或者只是扩展 DBXP 引擎来实现 MySQL 中的全套查询功能。无论您选择用从本书的这一部分学到的知识做什么,您总是可以通过实现 MySQL 的替代查询引擎来让您的朋友和同事感到惊讶!

练习

下面列出了几个需要进一步探索的领域。它们代表了您可能希望作为探索关系数据库技术的实验(或课堂作业)来进行的活动类型。

  1. 完成do_join()方法的代码,以支持 MySQL 中支持的所有连接类型。提示:在开始优化之前,您必须能够识别连接的类型。有关详细信息,请查看解析器。
  2. 检查Query_tree类中check_rewind()方法的代码。将实现更改为使用临时表,以避免在连接大型表时占用大量内存。
  3. 评估 DBXP 查询引擎的性能。运行多个测试运行并记录执行时间。将这些结果与使用原生 MySQL 查询引擎的相同查询进行比较。DBXP 引擎与 MySQL 相比如何?
  4. 为什么相交操作不需要删除重复项操作?有什么条件是假的吗?如果是,它们是什么?
  5. (高级)MySQL 目前不支持叉积或交集操作(按日期定义)。更改 MySQL 解析器以识别这些新的关键字并处理查询,如SELECT * FROM A CROSS B and SELECT * FROM A INTERSECT B,,并将这些函数添加到执行引擎中。提示:参见do_join()方法。
  6. (高级)形成一个更完整的测试查询列表,并检查 DBXP 引擎的限制。为了扩展 DBXP 引擎的功能,需要做哪些修改?

1 为了简单起见,我交替使用了属性/列、元组/行和关系/表术语。

2 自然连接是去掉多余(重复)条件属性值的等价连接。

3 原谅我的概括。它们可能不是完全不匹配,因为这取决于存储的数据及其解释。

4 集合运算(交集、并集)和排序被实现为连接运算的特殊形式。

5 实际上,所有的元组都是通过引用传递的,所以返回的项是同一个指针。

有些人会认为这是一种天生的弱点,我倾向于同意这种观点。

十五、附录

本附录包含本书中使用的参考文献的综合列表,以及示例中使用的示例数据库的描述。

文献学

以下参考书目包含有趣文章和论文的其他来源。参考书目是按主题排列的。

数据库理论

  • A.贝鲁西,e .贝尔蒂诺和 b .卡塔尼亚。约束数据库的扩展代数 (IEEE 知识与数据工程汇刊 10.5(1998):686–705)。
  • C.j .戴特和 h .达文。未来数据库系统的基础:第三宣言。(阅读:艾迪森-韦斯利,2000 年)。
  • C.日期。数据库关系模型:回顾与分析。(阅读:艾迪森-韦斯利,2001 年)。
  • R.埃尔马斯里和 S. B .纳瓦特尔。数据库系统基础。第四版。(波士顿:艾迪森-韦斯利出版社,2003 年)。
  • 米(meter 的缩写))j .富兰克林 B. T .琼森和 d .科斯曼。客户机-服务器查询处理的性能权衡(1996 年 ACM SIGMOD 数据管理国际会议记录,加拿大蒙特利尔,1996 年。149–160).
  • 页(page 的缩写)Gassner,G. M. Lohman,K. B. Schiefer 和 Y. Wang。IBM DB2 家族中的查询优化(数据工程技术委员会公告 16.4(1993):4–17)。
  • Y.E. Ioannidis,R. T. Ng,K. Shim 和 T. Sellis。参数查询优化 (VLDB 期刊 6(1997):132–151)。
  • D.科斯曼和 k .斯托克。迭代动态编程:一类新的查询优化算法 (ACM 数据库系统学报 25.1(2000):43–82)。
  • C.李、施振荣和陈奕利。优化涉及方法的查询的图论模型。(《VLDB 日报》2001 年第 9 期,第 327 页至第 343 页)。
  • 页(page 的缩写)塞林格,M. M .阿斯特拉姆,D. D .钱伯林,R. A .洛里和 T. G .普莱斯。关系数据库管理系统中的访问路径选择。苏格兰阿伯丁:1979 年。23–34).
  • 米(meter 的缩写))斯通布雷克,e .黄,p .克雷普斯。INGRES 的设计和实现(ACM 数据库系统学报 1.3(1976):189–222)。
  • 米(meter 的缩写))斯通布雷克和 J. L .海勒斯坦。数据库系统第三版,迈克尔·斯通布雷克版。,(摩根·考夫曼出版社,1998 年)。
  • A.b .塔克。计算机科学手册。第二版。(佛罗里达州博卡拉顿:LLCC 儿童权利委员会出版社,2004 年)。
  • 布莱恩·沃恩。SQL 查询优化器内部(Progress Worldwide Exchange 2001,华盛顿特区 2001) http://www.peg.com/techpapers/2001Conf/

一般

  • D.罗森伯格,m .斯蒂芬斯,m .柯林斯-科普。ICONIX 过程的敏捷开发(加州伯克利:Apress,2005)。

关系型数据库

  • 罗伯特·a·伯格曼,安德鲁·s·格罗夫,菲利普·e·梅萨,战略动态。(纽约:麦格劳-希尔公司,2006 年)。
  • 米(meter 的缩写))克鲁肯伯格和 j .派普斯。Pro MySQL (加州伯克利:Apress,2005 年)。

开放源码

  • “开源和闭源软件产品的实证研究”,IEEE 软件工程汇刊,第 30 卷,2004 年 4 月第 5 期。

网站

  • www.opensource.org——开源联盟。
  • http://dev.mysql.com - MySQL 的开发者网站。
  • 所有 MySQL 的东西。
  • www.gnu.org/licenses/gpl.html-GNU 公共许可证。
  • http://www.activestate.org - ActivePerl for Windows。
  • http://www.gnu.org/software/diffutils/diffutils.html -不同于 Linux。
  • http://www.gnu.org/software/patch/GNU 补丁。
  • GDB 文件。
  • ftp://www.gnu.org/gnu/ddd - GNU 数据显示调试器。
  • http://undo-software.com -撤销软件。
  • http://gnuwin32.sourceforge.net/packages/bison.htm——野牛。
  • http://www.gnu.org—yacc。
  • http://www.postgresql.org/ - Postgresql。

样本数据库

下面的示例数据库将在本文后面的章节中使用。下面的清单显示了数据库的 SQL 转储。

列举 A-1。样本数据库创建报表

-- MySQL dump 10.10
--
-- Host: localhost    Database: expert_mysql
-- ------------------------------------------------------
-- Server version       5.1.9-beta-debug-DBXP 1.0

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0
*/;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;

CREATE DATABASE IF NOT EXISTS expert_mysql;

--
-- Table structure for table 'expert_mysql'.'building'
--

DROP TABLE IF EXISTS 'expert_mysql'.'building';
CREATE TABLE 'expert_mysql'.'building' (
  'dir_code' char(4) NOT NULL,
  'building' char(6) NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

--
-- Dumping data for table 'expert_mysql'.'building'
--

/*!40000 ALTER TABLE 'expert_mysql'.'building' DISABLE KEYS */;
LOCK TABLES 'expert_mysql'.'building' WRITE;
INSERT INTO 'expert_mysql'.'building' VALUES
('N41','1300'),
('N01','1453'),
('M00','1000'),
('N41','1301'),
('N41','1305');
UNLOCK TABLES;
/*!40000 ALTER TABLE 'expert_mysql'.'building' ENABLE KEYS */;

--
-- Table structure for table 'expert_mysql'.'directorate'
--

DROP TABLE IF EXISTS 'expert_mysql'.'directorate';
CREATE TABLE 'expert_mysql'.'directorate' (
  'dir_code' char(4) NOT NULL,
  'dir_name' char(30) DEFAULT NULL,
  'dir_head_id' char(9) DEFAULT NULL,
  PRIMARY KEY ('dir_code')
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

--
-- Dumping data for table 'expert_mysql'.'directorate'
--

/*!40000 ALTER TABLE 'expert_mysql'.'directorate' DISABLE KEYS */;
LOCK TABLES 'expert_mysql'.'directorate' WRITE;
INSERT INTO 'expert_mysql'.'directorate' VALUES
('N41','Development','333445555'),
('N01','Human Resources','123654321'),
('M00','Management','333444444');
UNLOCK TABLES;
/*!40000 ALTER TABLE 'directorate' ENABLE KEYS */;

--
-- Table structure for table 'expert_mysql'.'staff'
--

DROP TABLE IF EXISTS 'expert_mysql'.'staff';
CREATE TABLE 'expert_mysql'.'staff' (
  'id' char(9) NOT NULL,
  'first_name' char(20) DEFAULT NULL,
  'mid_name' char(20) DEFAULT NULL,
  'last_name' char(30) DEFAULT NULL,
  'sex' char(1) DEFAULT NULL,
  'salary' int(11) DEFAULT NULL,
  'mgr_id' char(9) DEFAULT NULL,
  PRIMARY KEY ('id')
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

--
-- Dumping data for table 'expert_mysql'.'staff'
--

/*!40000 ALTER TABLE 'expert_mysql'.'staff' DISABLE KEYS */;
LOCK TABLES 'expert_mysql'.'staff' WRITE;
INSERT INTO 'expert_mysql'.'staff' VALUES
('333445555','John','Q','Smith','M',30000,'333444444'),
('123763153','William','E','Walters','M',25000,'123654321'),
('333444444','Alicia','F','St.Cruz','F',25000,NULL),
('921312388','Goy','X','Hong','F',40000,'123654321'),
('800122337','Rajesh','G','Kardakarna','M',38000,'333445555'),
('820123637','Monty','C','Smythe','M',38000,'333445555'),
('830132335','Richard','E','Jones','M',38000,'333445555'),
('333445665','Edward','E','Engles','M',25000,'333445555'),
('123654321','Beware','D','Borg','F',55000,'333444444'),
('123456789','Wilma','N','Maxima','F',43000,'333445555');
UNLOCK TABLES;
/*!40000 ALTER TABLE 'expert_mysql'.'staff' ENABLE KEYS */;

--
-- Table structure for table 'tasking'
--

DROP TABLE IF EXISTS 'expert_mysql'.'tasking';
CREATE TABLE 'expert_mysql'.'tasking' (
  'id' char(9) NOT NULL,
  'project_number' char(9) NOT NULL,
  'hours_worked' double DEFAULT NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

--
-- Dumping data for table 'tasking'
--

/*!40000 ALTER TABLE 'tasking' DISABLE KEYS */;
LOCK TABLES 'expert_mysql'.'tasking' WRITE;
INSERT INTO 'expert_mysql'.'tasking' VALUES
('333445555','405',23),
('123763153','405',33.5),
('921312388','601',44),
('800122337','300',13),
('820123637','300',9.5),
('830132335','401',8.5),
('333445555','300',11),
('921312388','500',13),
('800122337','300',44),
('820123637','401',500.5),
('830132335','400',12),
('333445665','600',300.25),
('123654321','607',444.75),
('123456789','300',1000);
UNLOCK TABLES;
/*!40000 ALTER TABLE 'expert_mysql'.'tasking' ENABLE KEYS */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

# Source on localhost: ... connected.

# Exporting metadata from bvm

DROP DATABASE IF EXISTS bvm;

CREATE DATABASE bvm;

USE bvm;

# TABLE: bvm.books

CREATE TABLE 'books' (

  'ISBN' varchar(15) DEFAULT NULL,

  'Title' varchar(125) DEFAULT NULL,

  'Authors' varchar(100) DEFAULT NULL,

  'Quantity' int(11) DEFAULT NULL,

  'Slot' int(11) DEFAULT NULL,

  'Thumbnail' varchar(100) DEFAULT NULL,

  'Description' text,

  'Pages' int(11) DEFAULT NULL,

  'Price' double DEFAULT NULL,

  'PubDate' date DEFAULT NULL

) ENGINE=MyISAM DEFAULT CHARSET=latin1;

# TABLE: bvm.settings

CREATE TABLE 'settings' (

  'FieldName' char(30) DEFAULT NULL,

  'Value' char(250) DEFAULT NULL

) ENGINE=MyISAM DEFAULT CHARSET=latin1;

#...done.

USE bvm;

# Exporting data from bvm

# Data for table bvm.books:

INSERT INTO bvm.books VALUES (978–1590595053, 'Pro MySQL', 'Michael Kruckenberg, Jay Pipes and Brian
Aker', 5, 1, 'bcs01.gif', NULL, 798, 49.99, '2005-07-15');

INSERT INTO bvm.books VALUES (978–1590593325, 'Beginning MySQL Database Design and Optimization',
'Chad Russell and Jon Stephens', 6, 2, 'bcs02.gif', NULL, 520, 44.99, '2004-10-28');

INSERT INTO bvm.books VALUES (978–1893115514, 'PHP and MySQL 5', 'W. Jason Gilmore', 4, 3, 'bcs03.gif',
NULL, 800, 39.99, '2004-06-21');

INSERT INTO bvm.books VALUES (978–1590593929, 'Beginning PHP 5 and MySQL E-Commerce', 'Cristian
Darie and Mihai Bucica', 5, 4, 'bcs04.gif', NULL, 707, 46.99, '2008-02-21');

INSERT INTO bvm.books VALUES (978–1590595091, 'PHP 5 Recipes', 'Frank M. Kromann, Jon Stephens,
Nathan A. Good and Lee Babin', 8, 5, 'bcs05.gif', NULL, 672, 44.99, '2005-10-04');

INSERT INTO bvm.books VALUES (978–1430227939, 'Beginning Perl', 'James Lee', 3, 6, 'bcs06.gif',
NULL, 464, 39.99, '2010-04-14');

INSERT INTO bvm.books VALUES (978–1590595350, 'The Definitive Guide to MySQL 5', 'Michael Kofler',
2, 7, 'bcs07.gif', NULL, 784, 49.99, '2005-10-04');

INSERT INTO bvm.books VALUES (978–1590595626, 'Building Online Communities with Drupal, phpBB, and
WordPress', 'Robert T. Douglass, Mike Little and Jared W. Smith', 1, 8, 'bcs08.gif', NULL, 560, 49.99, '2005-12-16');

INSERT INTO bvm.books VALUES (978–1590595084, 'Pro PHP Security', 'Chris Snyder and Michael
Southwell', 7, 9, 'bcs09.gif', NULL, 528, 44.99, '2005-09-08');

INSERT INTO bvm.books VALUES (978–1590595312, 'Beginning Perl Web Development', 'Steve Suehring',
8, 10, 'bcs10.gif', NULL, 376, 39.99, '2005-11-07');

# Blob data for table books:

UPDATE bvm.books SET 'Description' = "Pro MySQL is the first book that exclusively covers
intermediate and advanced features of MySQL, the world's most popular open source database server.
Whether you are a seasoned MySQL user looking to take your skills to the next level, or youre a
database expert searching for a fast-paced introduction to MySQL's advanced features, this book is
for you." WHERE 'ISBN' = 978–1590595053;

UPDATE bvm.books SET 'Description' = "Beginning MySQL Database Design and Optimization shows you
how to identify, overcome, and avoid gross inefficiencies. It demonstrates how to maximize the many
data manipulation features that MySQL includes. This book explains how to include tests and branches
in your queries, how to normalize your database, and how to issue concurrent queries to boost
performance, among many other design and optimization topics. You'll also learn about some features
new to MySQL 4.1 and 5.0 like subqueries, stored procedures, and views, all of which will help you
build even more efficient applications." WHERE 'ISBN' = 978–1590593325;

UPDATE bvm.books SET 'Description' = "Beginning PHP 5 and MySQL: From Novice to Professional offers
a comprehensive introduction to two of the most popular open-source technologies on the planet: the
PHP scripting language and the MySQL database server. You are not only exposed to the core features
of both technologies, but will also gain valuable insight into how they are used in unison to create
dynamic data-driven web applications, not to mention learn about many of the undocumented features
of the most recent versions." WHERE 'ISBN' = 978–1893115514;

UPDATE bvm.books SET 'Description' = "Beginning PHP 5 E-Commerce: From Novice to Professional is
an ideal reference for intermediate PHP 5 and MySQL developers, and programmers familiar with web
development technologies. This book covers every step of the design and build process, and provides
rich examples that will enable you to build high-quality, extendable e-commerce websites." WHERE
'ISBN' = 978–1590593929;

UPDATE bvm.books SET 'Description' = "We are confident PHP 5 Recipes will be a useful and welcome
companion throughout your PHP journey, keeping you on the cutting edge of PHP development, ahead
of the competition, and giving you all the answers you need, when you need them." WHERE 'ISBN' =
978–1590595091;

UPDATE bvm.books SET 'Description' = "This is a book for those of us who believed that we didn't
need to learn Perl, and now we know it is more ubiquitous than ever. Perl is extremely flexible and
powerful, and it isn't afraid of Web 2.0 or the cloud. Originally touted as the duct tape of the
Internet, Perl has since evolved into a multipurpose, multiplatform language present absolutely
everywhere: heavy-duty web applications, the cloud, systems administration, natural language
processing, and financial engineering. Beginning Perl, Third Edition provides valuable insight into
Perl's role regarding all of these tasks and more." WHERE 'ISBN' = 978–1430227939;

UPDATE bvm.books SET 'Description' = "This is the first book to offer in-depth instruction about the
new features of the world's most popular open source database server. Updated to reflect changes
in MySQL version 5, this book will expose you to MySQL's impressive array of new features: views,
stored procedures, triggers, and spatial data types." WHERE 'ISBN' = 978–1590595350;

UPDATE bvm.books SET 'Description' = "Building Online Communities with Drupal, phpBB, and Wordpress
is authored by a team of experts. Robert T. Douglass created the Drupal-powered blog site NowPublic.com.
Mike Little is a founder and contributing developer of the WordPress project. And Jared W. Smith has
been a longtime support team member of phpBBHacks.com and has been building sites with phpBB since
the first beta releases." WHERE 'ISBN' = 978–1590595626;

UPDATE bvm.books SET 'Description' = "Pro PHP Security is one of the first books devoted solely
to PHP security. It will serve as your complete guide for taking defensive and proactive security
measures within your PHP applications. The methods discussed are compatible with PHP versions 3, 4,
and 5." WHERE 'ISBN' = 978–1590595084;

UPDATE bvm.books SET 'Description' = "Beginning Perl Web Development: From Novice to Professional
introduces you to the world of Perl Internet application development. This book tackles all areas
crucial to developing your first web applications and includes a powerful combination of real-world
examples coupled with advice. Topics range from serving and consuming RSS feeds, to monitoring
Internet servers, to interfacing with e-mail. You'll learn how to use Perl with ancillary packages
like Mason and Nagios." WHERE 'ISBN' = 978–1590595312;

# Data for table bvm.settings:

INSERT INTO bvm.settings VALUES ('ImagePath', 'c://mysql_embeddeimg//');

#...done.

章节练习笔记

本节包含一些提示和有用的指导,用于第 12 章、 13 章14 章中的练习。有些练习是实践练习,因此解答太长,无法包含在附录中。对于那些需要编程来解决的练习,我提供了一些关于如何编写解决方案的代码的提示。在其他情况下,我会提供一些有助于您完成练习的附加信息。

第十二章

以下问题来自第十二章“内部查询表示”。

问题 1。图 12-1 中的查询暴露了一个表格中的设计缺陷。这是什么?该缺陷是否违反了任何标准形式?如果有,是哪一个?

看学期属性。数据代表多少个值?如果您需要访问属性(字段)的一部分,像这样打包数据会导致一些性能很差的查询。例如,要查询 2001 年的所有学期,您必须使用一个WHERE子句和一个LIKE操作符:WHERE semester LIKE '%2001'。这种打包字段(也称为多值字段)的做法违反了第一范式。

问题 2。浏览表结构并更改 SELECT DBXP 存根以返回关于表及其字段的信息

修改代码以返回信息,就像我们在第 8 章中探索show_disk_usage_command()方法时所做的那样。只是这一次,包括关于表的元数据。提示:参见 table 类。

问题 3。更改 EXPLAIN SELECT DBXP 命令,产生类似于 MySQL EXPLAIN SELECT 命令的输出

更改代码,在一个类似 MySQL EXPLAIN命令的表格中生成信息。注意,您将需要在Query_tree类中使用额外的方法来收集关于优化查询的信息。

问题 4。修改 build_query_tree 函数以识别和处理 LIMIT 子句

对代码的修改要求您识别查询何时包含LIMIT子句,并相应地简化结果。作为提示,下面是捕获LIMIT子句的值的代码。您需要修改DBXP_select_command()方法中的代码来处理剩余的操作。

SELECT_LEX_UNIT *unit= &lex->unit;
unit->set_limit(unit->global_parameters);

问题 5。如何更改查询树 query_node 结构以适应 HAVING、GROUP BY 和 ORDER 子句?

最好的设计是忠于查询树概念的设计。也就是说,考虑这样一种设计,其中每个子句都是树中的一个单独节点。还要考虑是否有任何适用于这些操作的试探法。提示:处理离叶节点最近的HAVING子句不是更有效吗?最后,考虑控制树中每个节点数量的规则。

第十三章

以下问题来自第十三章“查询优化”。

问题 1。完成 balance_joins()方法的代码。提示:您需要创建一个可以移动合取连接的算法,以便首先执行限制性最强的连接(在树中最低)

这个练习主要是关于如何在树中移动连接,将最具限制性的连接下移。棘手的部分是使用表的统计信息来确定哪个连接将产生最少的结果。查看handlertable类以获取关于访问这些数据的信息。除此之外,您将需要助手方法来遍历树并获得关于表的信息。这是必要的,因为有可能(也很可能)连接在树的更高层,并且可能不包含对表的直接引用。

问题 2。完成 cost_optimization()方法的代码。提示:您需要遍历树并指出可以使用索引的节点

这个练习要求您询问handlertable类,以确定哪些表有索引以及那些列是什么。

问题 3。检查启发式优化器的代码。它涵盖了所有可能的查询吗?如果没有,是否有其他规则(启发法)可以用来完成覆盖范围?

您应该会发现有许多这样的试探法,并且这个优化器只覆盖最有效的试探法。例如,您可以实现试探法,将GROUP BYHAVING操作考虑在内,以类似于 project 的方式处理它们,或者限制沿着树向下推节点以获得更好的优化。

问题 4。检查查询树和启发式优化器的代码。如何实现查询树类中列出的 distinct 节点类型?提示:参见 heuristic_optimization()方法中 prune_tree()方法后面的代码

本练习的大部分提示都在示例代码中。下面的摘录显示了如何识别在查询中指定了 DISTINCT 选项。

问题 5。如何更改代码来识别无效查询?确定查询无效的条件是什么?如何测试这些条件?

本练习的部分解决方案已经为您完成。例如,语法不正确的查询语句将被解析器检测到,并显示相应的错误。但是,对于那些语法上正确但语义上无意义的查询,您需要添加额外的错误处理代码来检测。尝试语法正确但引用了错误字段的查询。创建这种性质的测试,并跟踪(调试)代码。您应该看到代码中可以放置额外错误处理的地方。最后,您还可以在Query_tree类中创建一个方法来验证查询树本身。如果您试图创建额外的节点类型或实现其他启发式方法,这可能特别方便。

问题 6。(高级)MySQL 目前不支持 INTERSECT 操作(按日期定义)。更改 MySQL 解析器以识别新的关键字并处理查询,如来自 INTERSECT B 的 SELECT *。该操作有任何限制吗?它们是否反映在优化器中?

听起来非常困难的问题有非常直接的解决方法。考虑添加一个名为“intersect”的新节点类型,它有两个子节点。该操作只返回两个表中的那些行。提示:使用许多合并排序变量中的一个来完成这个任务。

问题 7。(高级)如何实现 GROUP BY、ORDER BY 和 HAVING 子句?对优化器进行更改以启用这些子句。

有许多方法可以做到这一点。为了与Query_tree类的设计保持一致,这些操作中的每一个都可以表示为另一个节点类型。您可以构建一个方法来处理这些,就像我们处理 restrict、project 和 join 一样。但是请注意,HAVING子句与GROUP BY子句一起使用,而ORDER BY子句通常最后处理。

第十四章

以下问题来自第十四章“查询执行”。

问题 1。完成 do_join()方法的代码,以支持 MySQL 中支持的所有连接类型。提示:在开始优化之前,您需要能够识别连接的类型。有关详细信息,请查看解析器

为了完成这个练习,您可能想要在do_join()方法中重新构建代码。我使用的例子将所有代码放在一起,但是一个更好的解决方案是在do_join()方法中的 select-case 语句为每种类型的连接调用帮助器方法,并且可能为常见操作调用其他帮助器方法(即,参见preempt_pipeline代码)。其他形式的连接代码将与示例代码中实现的连接非常相似。

问题 2。检查 Query_tree 类中 check_rewind()方法的代码。将实现更改为使用临时表,以避免在连接大型表时占用大量内存

这个练习也有一个简单明了的解决方案。参见sql_select.cc文件中的 MySQL 代码,了解如何创建临时表的详细信息。提示:这与创建表和插入非常相似。您还可以使用基本的Spartan类并创建一个临时表来存储记录缓冲区。

问题 3。评估 DBXP 查询引擎的性能。运行多个测试运行并记录执行时间。将这些结果与使用原生 MySQL 查询引擎的相同查询进行比较。DBXP 引擎与 MySQL 相比如何?

有许多方法可以记录执行时间。您可以使用一个简单的秒表并根据观察记录时间,或者您可以添加捕获系统时间的代码。后一种方法也许是确定相对速度最快最可靠的方法。我之所以说是相对的,是因为有许多与环境和执行时正在运行的程序有关的因素会影响性能。当您进行测试运行时,请确保使用多个测试运行,并对结果进行统计分析。这将为您提供一组标准化的数据进行比较。

问题 4。为什么相交操作不需要删除重复项操作?有什么条件是假的吗?如果是,它们是什么?

让我们考虑一下什么是交集操作。它只是出现在每个相关表中的行(您可以在两个以上的表上相交)。在这种情况下,如果表本身没有副本,就不可能有副本。但是,如果表是在下面的树中执行的操作的结果,并且没有删除重复项,并且查询中包含了DISTINCT操作,那么您需要删除重复项。基本上,这是一个“视情况而定”的答案。

问题 5。(高级)MySQL 目前不支持叉积或交集操作(由日期定义)。更改 MySQL 解析器以识别这些新的关键字,并处理查询,如 SELECT * FROM A CROSS B 和 SELECT * FROM A INTERSECT B,并将这些函数添加到执行引擎中。提示:请参见 do_join()方法

您需要更改的文件与我们在添加 DBXP 关键字时更改的文件相同。这些包括lex.hsql_yacc.yy。您可能需要扩展sql_lex结构,以包含记录操作类型的规定。

问题 6。(高级)形成一个更完整的测试查询列表,并检查 DBXP 引擎的限制。为了扩展 DBXP 引擎的功能,需要做哪些修改?

首先,查询树应该扩展到包含 HAVING、GROUP BY 和ORDER BY子句。您还应该考虑添加处理聚合函数的功能。这些集合函数(例如,max()min()等)。)可以放入Expression类,从而创建新的方法来解析和评估聚合函数。

第一部分:MySQL 开发入门

本节向您介绍开发和修改开源系统的概念。第 1 章引导你了解开源系统集成商的好处和责任。它强调了 MySQL 的快速增长及其在开源和数据库系统市场的重要性。第 2 章讲述了什么是数据库系统以及如何构建数据库系统的基础知识。第 3 章完整介绍了本章介绍的 MySQL 源代码以及如何获取和构建该系统。第 4 章介绍了为 MySQL 系统生成高质量扩展的一个关键要素。您将了解软件测试以及测试大型系统的常见实践。

第二部分:扩展 MySQL

通过动手实践,本节提供了探索和扩展 MySQL 系统所需的工具。它向您介绍了如何修改 MySQL 代码,并解释了如何将该系统用作嵌入式数据库系统。第 5 章回顾了有助于简化开发和减少失败的调试技巧和技术。它介绍了几种调试技术,以及每种技术的优缺点。第 6 章包含了如何在企业应用中嵌入 MySQL 系统的教程。第 7 章展示了对 MySQL 代码最流行的修改。您将学习如何修改 SQL 命令来添加新的参数和函数,以及如何添加新的 SQL 命令。第 8 章介绍了 MySQL 的高可用性组件。您将了解如何扩展 MySQL 以用于您自己定制的高可用性解决方案。第 9 章给出了一个关于构建认证插件的教程,用于创建连接 MySQL 的认证机制。第 10 章研究了 MySQL 可插拔存储引擎的能力,并提供了允许您构建示例存储引擎的示例和项目。

第三部分:高级数据库内部原理

MySQL 开发入门第 3 部分深入探讨了 MySQL 系统,并让您从内部人员的角度了解系统的工作原理。第 11 章回顾了 MySQL 架构中查询执行的主题,并介绍了如何使用源代码进行实验。第 12 章介绍了 MySQL 内部查询表示,并提供了一个替代查询表示的例子。第十三章介绍 MySQL 内部查询优化器;它描述了一个替代查询优化器的例子,该优化器使用了前一章中的内部表示实现。本章还向您展示了如何修改 MySQL 源代码来实现替代的查询优化器。第 14 章结合了前几章的技术来实现另一个查询处理引擎。

posted @ 2024-10-01 20:56  绝不原创的飞龙  阅读(160)  评论(0)    收藏  举报