MySQL 5 存储过程:遗迹还是革命?
存储过程(或存储例程,使用 MySQL 的官方术语)是在数据库服务器内部存储和执行的程序。自 1990 年代初期以来,诸如 Oracle 之类的闭源关系数据库中就已包含存储过程。但是,MySQL 仅在最新的 5.0 版本中添加了存储过程支持,因此,构建在 LAMP 堆栈之上的应用程序通常不包含存储过程。因此,现在是时候考虑是否应将存储过程纳入您的 MySQL 应用程序中了。
数据库存储程序最早在 1980 年代后期和 1990 年代初期,在客户端-服务器革命期间开始流行。在当时的客户端-服务器应用程序中,存储程序具有一些明显的优势
客户端-服务器应用程序通常必须仔细平衡客户端 PC 和(相对)更强大的服务器机器之间的处理负载。使用存储程序是减轻客户端负载的一种方法,否则客户端可能会过载。
网络带宽通常是客户端-服务器应用程序的严重制约因素;在单个存储程序中执行多个服务器端操作可以减少网络流量。
在客户端-服务器环境中维护正确版本的客户端软件通常很麻烦。将至少一部分处理集中在服务器上,可以更好地控制核心逻辑。
存储程序提供了明显的安全优势,因为在那些日子里,应用程序用户通常直接连接到数据库,而不是通过中间层。正如我在本文后面讨论的那样,存储过程允许您将数据库帐户仅限于定义明确的过程调用,而不是允许该帐户执行任何和所有 SQL 语句。
随着三层架构和 Web 应用程序的出现,从应用程序内部使用存储程序的一些动机消失了。应用程序客户端现在通常是基于浏览器的,安全性主要由中间层处理,并且中间层具有封装业务逻辑的能力。客户端-服务器应用程序中使用存储程序的大多数功能现在都可以在中间层代码(PHP、Java、C# 等)中实现。
然而,存储过程的许多传统优势仍然存在,因此让我们更深入地考虑这些优势以及一些缺点。
存储过程受适用于其他数据库对象(表、索引、视图等)的大多数安全限制的约束。用户需要特定的权限才能创建存储程序,同样,执行程序也需要特定的权限。
存储程序安全模型与其他数据库对象以及其他编程语言的安全模型不同的地方在于,存储程序可以使用创建存储过程的用户的权限执行,而不是使用执行存储过程的用户的权限。此模型允许用户通过存储过程执行操作,而这些操作是他们无权使用普通 SQL 执行的。
这种设施,有时称为定义者权限安全,使我们能够加强数据库安全性,因为我们可以确保用户仅通过存储程序代码访问表,该代码限制了可以对这些表执行的操作类型,并且可以实施各种业务和数据完整性规则。例如,通过建立一个存储程序作为某些表插入或更新的唯一可用机制,我们可以确保所有这些操作都被记录,并且我们可以防止任何无效数据条目进入表中。
如果此应用程序帐户被泄露(例如,如果密码被破解),攻击者仍然只能执行我们的存储程序,而不是能够运行任何即席 SQL。尽管这种情况构成严重的安全性漏洞,但至少我们可以确保攻击者将受到与普通应用程序用户相同的检查和日志记录。他们也将被剥夺检索有关底层数据库架构的信息的机会(因为运行标准 SQL 的能力将授予过程,而不是用户),这将阻碍进一步恶意活动的尝试。
存储程序固有的另一个安全优势是它们对 SQL 注入攻击的抵抗力。当恶意用户设法将 SQL 代码“注入”到应用程序正在构建的 SQL 代码中时,可能会发生 SQL 注入攻击。存储程序并非唯一提供针对 SQL 注入攻击的保护措施,但完全依赖存储程序与数据库交互的应用程序在很大程度上可以抵抗此类攻击(前提是这些存储程序本身不会在未完全验证其输入的情况下构建动态 SQL 字符串)。
通常,将数据访问代码与业务逻辑和表示逻辑分离是一种好的做法。数据访问例程通常被多个程序模块使用,并且可能由不同的开发人员组维护。一种非常常见的场景需要更改底层数据结构,同时最大限度地减少对更高级别逻辑的影响。数据抽象使这更容易实现。
存储程序的使用提供了一种实现数据访问层的便捷方法。通过创建一组存储程序来实现应用程序所需的所有数据访问例程,我们实际上是在构建一个供应用程序用于所有数据库交互的 API。
在某些情况下,存储程序可以通过减少网络流量来显着提高应用程序性能。
应用程序接受最终用户的输入,读取数据库中的一些数据,决定接下来要执行的语句,检索结果,做出决定,执行一些 SQL 等等,这是很常见的。如果应用程序代码完全在数据库外部编写,则每个步骤都需要数据库和应用程序之间的网络往返。执行这些网络往返所花费的时间很容易在总体用户响应时间中占据主导地位。
考虑一下银行客户与 ATM 机之间的典型交互。用户请求在两个帐户之间转移资金。应用程序必须从数据库中检索每个帐户的余额,检查提款限额以及可能的其他策略信息,发出相关的 UPDATE 语句,最后发出提交,所有这些都在告知客户交易成功之前完成。即使对于这种相对简单的交互,也必须发出至少六个单独的数据库查询,每个查询在应用程序服务器和数据库之间都有自己的网络往返。图 1 显示了在没有存储程序的情况下所需的交互序列。
另一方面,如果使用存储程序来实现资金转移逻辑,则只需要进行一次数据库交互。存储程序负责检查余额、提款限额等。图 2 显示了由此产生的网络往返次数的减少。
当应用程序需要在数据库中对非常大的记录集执行某种聚合处理时,网络往返也可能变得很重要。例如,如果应用程序需要检索数百万行才能计算某种难以使用本机 SQL 轻松计算的业务指标,例如完成订单的平均时间,则可能会导致大量的往返。在这种情况下,网络延迟再次可能成为应用程序响应时间的主导因素。在存储程序中执行计算将减少网络开销,这可能会缩短总体响应时间,但您需要确保考虑到原始计算速度的差异,我将在本文稍后讨论。
尽管 MySQL 数据库通常服务于单个应用程序,但多个应用程序共享单个数据库的情况也并不少见。这些应用程序可能在不同的机器上运行,并以不同的语言编写;这些应用程序可能很难或不可能共享代码。在存储程序中实现通用代码可能允许这些应用程序共享关键的通用例程。
例如,在银行应用程序中,资金转移交易可能来自多个来源,包括银行柜员控制台、Internet 浏览器、ATM 或电话银行应用程序。这些应用程序中的每一个都可能具有以很大程度上不兼容的语言编写的自己的数据库访问代码,并且如果没有存储程序,我们可能必须在多个位置和多种语言中复制事务逻辑,包括日志记录、死锁处理和乐观锁定策略。在这种情况下,将逻辑整合到数据库存储过程中可能很有意义。
如果我们期望 MySQL 存储程序语言的第一个版本速度快如闪电,那将是非常不公平的。毕竟,Perl 和 PHP 等语言已经进行了大约十年的调整和优化,而最新一代的编程语言——.NET 和 Java——也受到了世界上一些最大的软件公司进行的较短但更密集的优化过程。因此,从一开始,我们就可以预料到 MySQL 存储程序语言与其他 MySQL 世界中常用的语言相比会滞后。
尽管如此,了解该语言的原始性能仍然很重要。首先,让我们看看存储程序语言处理数字的速度有多快。第一个示例将计算素数的存储过程与以替代语言实现的相同算法进行了比较。
在这个计算密集型试验中,MySQL 的性能与其他语言相比表现不佳——比 PHP 或 Perl 慢五倍,比 Java、.NET 或 C 慢数十倍(图 3)。
大多数时候,存储程序都受数据库访问时间的支配,由于其较低的网络开销,存储程序在数据库访问时间方面比其他编程语言具有天然的性能优势。但是,如果您正在编写一个数字运算例程,并且您可以在存储程序语言或另一种语言(例如 PHP 或 Java)中实现它之间进行选择,那么您明智地决定不使用存储程序解决方案。
如果前面的示例让您对存储程序性能感到不太热情,那么下一个示例应该会让您振作起来。虽然存储程序在数字运算方面不是特别快,但通常情况下,您通常不会仅仅为了执行数学运算而编写存储程序;存储程序几乎总是处理来自数据库的数据。在这些情况下,存储程序与 PHP 或 Java 性能之间的差异通常是最小的,除非网络开销是一个很大的因素。当程序需要处理来自数据库的大量行时,存储程序可以显着优于用客户端语言编写的程序,因为它不必等待行在网络上传输——存储程序在数据库内部运行。图 4 显示了即使从远程主机跨网络调用,聚合数百万行的存储过程也能表现良好,而具有相同逻辑的 Java 程序则遭受严重的网络驱动响应时间下降。
虽然将数据访问逻辑封装在存储程序内部通常很有用,但通过在存储程序中实现部分业务和应用程序逻辑,并在中间层或应用程序客户端中实现其余部分,来“碎片化”业务和应用程序逻辑通常是不可取的。
调试涉及存储程序代码和其他应用程序代码之间交互的应用程序错误可能比调试完全封装在应用程序层中的代码困难得多。例如,目前没有调试器可以跟踪从应用程序代码到 MySQL 存储程序代码的程序流。
此外,如果您的应用程序依赖于存储过程,那么这是您或您的团队将必须获得和维护的额外技能。
对象关系映射 (ORM) 框架越来越普遍地用于调解应用程序和数据库之间的交互。ORM 在 Java(Hibernate 和 EJB)中非常常见,在 Ruby on Rails(ActiveRecord)中几乎不可避免,而在 PHP 中则不太常见(尽管有越来越多的 PHP ORM 包可用)。ORM 系统生成 SQL 以维护程序对象和数据库表之间的映射。尽管大多数 ORM 系统允许您使用自己的代码(例如存储过程调用)覆盖 ORM SQL,但这样做会抵消 ORM 系统的一些优势。简而言之,当与 ORM 结合使用时,存储过程变得更难使用,并且吸引力大大降低。
尽管所有关系数据库都实现了一组通用的 SQL 语法,但每个 RDBMS 都为此标准 SQL 提供了专有扩展,MySQL 也不例外。如果您正在尝试编写一个旨在独立于底层数据库的应用程序,您可能希望在应用程序中避免这些扩展。但是,有时您需要使用特定的语法才能充分利用服务器。例如,在 MySQL 中,您通常会希望使用 MySQL 提示、执行非 ANSI 语句(例如 LOCK TABLES)或使用 REPLACE 语句。
使用存储程序可以帮助您避免应用程序层中依赖于 RDBMS 的代码,同时允许您继续利用 RDBMS 特定的优化。从理论上讲,可以使针对不同数据库的存储程序调用从应用程序的角度来看看起来和行为都相同。您可以将所有数据库相关的代码封装在存储过程中。当然,需要为每个 RDBMS 重写底层存储程序代码,但至少您的应用程序代码将具有相对的可移植性。
但是,在各种数据库服务器处理存储过程调用的方式上存在差异,特别是当这些调用返回结果集时。MySQL、SQL Server 和 DB2 存储过程从应用程序的角度来看行为非常相似。但是,Oracle 和 Postgres 调用可能看起来和行为不同,特别是当您的存储过程调用返回一个或多个结果集时。
因此,尽管使用存储过程可以提高应用程序的可移植性,同时仍然允许您利用特定于供应商的语法,但它们并不能使您的应用程序完全可移植。
除了传统的应用程序逻辑之外,MySQL 存储程序还可以用于各种任务
触发器是数据修改语言 (DML) 语句执行时触发的存储程序。触发器可以自动化反规范化并强制执行业务规则,而无需更改应用程序代码,并且将对访问数据库的所有应用程序(包括即席 SQL)生效。
5.1 版本中引入的 MySQL 事件调度器允许以定期间隔执行存储过程代码。这对于运行常规应用程序维护任务(例如清除和存档)非常方便。
MySQL 存储程序语言可用于创建可以从标准 SQL 调用的函数。这允许您将复杂的应用程序计算封装在一个函数中,然后在 SQL 调用中使用该函数。这可以集中逻辑,提高可维护性,如果谨慎使用,还可以提高性能。
Guy Harrison 是 Quest Software (www.quest.com) 数据库解决方案的首席架构师。本文使用了他与 Steven Feuerstein 合著的书籍 MySQL 存储过程编程 (O'Reilly 2006) 中的一些材料。可以通过 guy.harrison@quest.com 联系 Guy。