在企业级的 Java 应用中,访问数据库是一个必备的环节。数据库作为数据资源的集散地,往往位于企业级软件体系的后方,供前方的应用程序访问。在 Java 技术的体系中,应用程序是通过 JDBC(Java Database Connectivity)接口来访问数据库的,JDBC 支持"建立连接、SQL 语句查询、处理结果"等基本功能。在应用 JDBC 接口访问数据库的过程中,只要根据规范来操作,这些功能的实现不会出差错。但是,有些时候进行数据查询的效率着实让开发人员懊恼不已,明明根据规范编写的程序,却得不到预期的运行效果,造成了整个软件的执行效率不高。
起初,我们把问题归结于 Java 字节码加载和执行速度的缓慢,紧接着硬件的功能普遍得到了增强,证明这样的想法些许是错误的,还没有抓到真正的根本原因。本文将逐步解剖 JDBC 访问数据库的机制,深层分析造成这种速度瓶颈问题的原因,并提出在现有的 Java 技术框架下解决这个速度瓶颈问题的思路和方法。
图 1 |
图 2 |
图 1 和图 2 描述了 Java 应用程序通过 JDBC 接口访问数据库的 4 种驱动模式,也就是底层实现 JDBC 接口的模式。对于这些模式,我们逐一介绍:
模式 4:图 1 左边的分支称为模式 4,它一般是数据库厂商才能实现的纯 Java 的基于本地协议的驱动,直接调用 DBMS(数据库管理系统)使用的网络协议,对于企业内部互联网来说,是一个实用的解决方案。
模式 3:图 1 右边的分支称为模式 3,它同样是一个纯 Java 驱动,不同于模式 4 的是基于网络协议。它的机制是将 JDBC 调用转换为中间网络协议,然后转换为 DBMS 协议。中间网络协议层起到一个读取数据库的中间件的作用,能够连接许多类型的数据库,因而是最灵活的 JDBC 模式。这种模式的产品比较适用于企业内部互联网,如若支持国际互联网,还需添加对安全、穿过防火墙访问等的支持。
模式 1:图 2 左边的分支称为模式 1,即通常由 Sun 公司提供的 JDBC-ODBC 桥接器。它提供了经由一种或多种 ODBC 驱动进行访问的 JDBC 接口,而 ODBC 驱动,在很多情况下也即数据库的客户端,必须加载到客户机。因而,它适用于下载和自动安装 Java 程序不重要、实验用途或者没有其它 JDBC 驱动可用的情况下。
模式 2:图 2 右边的分支成为模式 2,类似于 JDBC-ODBC 桥接器,需要加载到客户机,却是一个部分用 Java 实现的驱动接口。它将 JDBC 调用转换为对数据库(Oracle、Sybase、Informix、DB2 等)客户端接口的调用。
以上阐述的 JDBC 接口的模式不同,让我们可以把 JDBC 接口按照实现的模式分为四类。有些同仁可能有这样的体会,选择不同的 JDBC 接口会有不同的访问速度,为何会出现这样的情况?这个问题的答案是,不同的应用需要不同模式的 JDBC 接口,因而我们在面对一个应用时,要慎重选择 JDBC 接口。
通常的 DBMS 都支持微软提出的 ODBC 规范,因而模式 1 可当作您在设计和实现软件时的选择,它易于配置的特性能够让你把选择 JDBC 等烦恼的问题暂且抛在一边,让自己的 Java 程序能够及早地正常工作起来。
一般说来,商业 DBMS 的提供者往往会为自己的数据库提供一个 JDBC 接口,应用的是模式 4。这种模式的优势在于和数据库本身结合比较紧密,而且是纯 Java 的实现,在企业级的软件应用中,应该是首选。例如,对于 Oracle 数据库来说,有 Oracle、SilverStream、DataDirect 等公司提供这种类型的驱动,其性能往往被评价为最高效的、最可靠的驱动程序。但偶尔也有比较麻烦的情况,例如微软就不会提供 MS SQL 的 JDBC 接口,这时就需要到 Sun 的网站( http://industry.java.sun.com/products/jdbc/drivers)查找相关的模式 4 驱动,上面提到的 DataDirect 公司( http://www.datadirect-technologies.com/jdbc/jdbc.asp)就提供了支持 MS SQL 的模式 4 驱动,只是你需要支付 750$ 购买这个 JDBC 驱动。
同样是纯 Java 实现的模式 3,与模式 4 相比,优势在于对多种数据库的支持,体现了其灵活性。在大型的企业级的软件应用中,后台数据库往往不是一个,而且是由不同的厂商支持的。不过,模式 3 的 JDBC 驱动往往提供许多企业级的特征,例如 SSL 安全、支持分布式事务处理和集中管理等,因而会对你特殊的用途有很大的帮助。是否选用,还在于你对扩展应用是否有需求以及对多 DBMS 的支持。
谈到这儿,我对模式 3 和模式 4 作一个总结:两者都是纯 Java 实现的驱动,因而不需要数据库厂商提供附加的软件,就可以运行在任何标准的 Java 平台,性能上比较高效、可靠。
了解上述 3 种 JDBC 的实现模式之后,模式 2 就更容易阐释了,你可以理解它为前三者利弊平衡的妥协产物:
| 1 | 借鉴模式 1 利用客户机本地代码库,加速数据访问的执行,但却摒除 ODBC 标准,而是支持厂商自己指定的性能扩展 |
| 2 | 借鉴模式 3 利用多层结构,上层用 Java 实现,利于跨平台应用和支持多数据库,但下层却改为本地代码,加速执行速度 |
| 3 | 借鉴模式 4 和数据库结合紧密的优点,部分用 Java 实现,更是对数据库性能有很大的扩展 |
这种开放和高性能的特征得到了业界的肯定,因而被主要的数据库厂商强烈推荐。尽管它需要你下载本地代码库到客户机,但相对于你访问数据库速度的提高,这些应该只是举手之劳了。下面对 4 种实现 JDBC 的模式选择,归纳一下选择的顺序(当然是指你有选择余地的时候,不存在的话向后推延):
| 编号 | 选择过程分析 | 选择顺序 |
| 1 | 实验性环境下,尽可能选择易于配置的驱动,利于 Java 程序的开发,后期可在对应用环境进行判断后,再对 JDBC 模式进行选择 | 1>2>3>4 |
| 2 | 小型企业级环境下,不需要对多数据库的支持,因而模式 2 和 3 的有些优点并不能体现出来,强烈推荐你选择模式 4 的 JDBC 驱动 | 4>2=3>1 |
| 3 | 大型企业级环境下,需要对多数据库的支持,模式 2 和 3 各有千秋,但是更多情况下是你会选择速度较快的模式 2 | 2>3>4>1 |
对于不同厂商提供的但应用相同模式的 JDBC 接口,理论上比较不出效率的高低,你只有通过一定的工具,例如 Benchmark 等,对它们进行比较才能更有利于你的选择。因为暂时不存在第三方提供的数据比较结果,所以这些问题需要你对上述内容有了透彻理解之后自行解决。
这个时候,你也许还在为找不到合适的 JDBC 驱动而一筹莫展,也许为自己在凌晨 3 点下载的 JDBC 驱动通过了测试而欣喜若狂,但是并不说明你对程序的优化工作已经无关紧要了。切记,对整个软件系统的优化,包括每个环节的优化,要不有可能你会前功尽弃。我在这儿不和大家讨论 Java 程序的算法,而是简单阐述一下选择 SQL 语句格式的必要和如何选择对自己有利的 SQL 语句格式。看下面两段程序片断:
Code Fragment 1:
String updateString = "UPDATE COFFEES SET SALES = 75 "
+ "WHERE COF_NAME LIKE 'Colombian'";
stmt.executeUpdate(updateString);
|
Code Fragment 2:
PreparedStatement updateSales = con.prepareStatement(
"UPDATE COFFEES SET SALES = ? WHERE COF_NAME LIKE ? ");
updateSales.setInt(1, 75);
updateSales.setString(2, "Colombian");
updateSales.executeUpdate();
|
片断 2 和片断 1 的区别在于,后者使用了 PreparedStatement 对象,而前者是普通的 Statement 对象。PreparedStatement 对象不仅包含了 SQL 语句,而且大多数情况下这个语句已经被预编译过,因而当其执行时,只需 DBMS 运行 SQL 语句,而不必先编译。当你需要执行 Statement 对象多次的时候,用 PreparedStatement 对象将会大大降低运行时间,当然也加快了访问数据库的速度。
这种转换也给你带来很大的便利,不必重复 SQL 语句的句法,而只需更改其中变量的值,便可重新执行 SQL 语句。选择 PreparedStatement 对象与否,在于相同句法的 SQL 语句是否执行了多次,而且两次之间的差别仅仅是变量的不同。如果仅仅执行了一次的话,它应该和普通的 Statement 对象毫无差异,体现不出它预编译的优越性。
在我阅读 J2EE 蓝图和 JDO 草案的过程中,我发现了访问模式对数据库访问的影响,因而想在本文中阐述如何针对自己的软件需求选择合适的软件模式。
J2EE 蓝图的设计者在 Java Pet Store 示例应用中使用了 MVC(Model-View-Controller)体系,给许多 J2EE 设计模式提供了背景。我要谈及的三种设计模式是:Data Access Object、Fast Lane Reader、Page-by-Page Iterator,它们为加快数据存取速度提供了一些可以在系统设计阶段值得我们借鉴的想法。
将商业逻辑从数据存取逻辑中分离出来,把存取的资源改编,从而使资源可以容易和独立地转变。
依赖于底层数据资源的特殊要素(例如数据库的供应商)的商业组件,常将商业逻辑和数据存取逻辑配合起来,只能使用特殊类型的资源,而使用不同类型的资源时,复用将会非常困难,因此,只能服务于有限的市场领域。DAO(Data Access Object)即是将数据存取逻辑从 EJB 中抽去出来抽象为一个独立的接口,EJB 根据接口的操作执行商业逻辑,而接口针对使用的数据资源实现为 DAO 对象。
在 Java Pet Shop 这个例子中,OrderEJB 组件通过关联的 OrderDAO 类访问数据库,自身则关注于商业逻辑的实现。在调度阶段,将配置某一类(OrderDAOCS、OrderDAOOracle 或 OrderDAOSybase)为 OrderDAO 的实现,而 OrderEJB 无须任何更改。图 3 更能帮助你明白其中的道理:
图 3 Data Access Object 的设计模式
此举增加了数据存取的弹性、资源的独立性和扩展性,但复杂度有相应的提高,其它附带的问题我们不在这儿讨论。
抛弃 EJB,加速只读数据的存取。
有些时候,高效地存取数据比获得最新的数据更重要。在 Java Pet Store 中,当一个用户浏览商店的目录时,屏幕与数据库内容吻合不是至关紧要的,相反,迅速显示和重新获得非常重要。FLR 模式可以加速从资源中重新获得大型的列数据项的速度,它不用 EJB,而是更直接地通过 DAO 来存取数据,从而消除 EJB 的经常开支(例如远程方法调用、事务管理和数据序列化等)。
在 Java Pet Store 这个例子中,当一个用户浏览目录时,通过 CatalogDAO(而不是 CatalogEJB)从数据库加载数据项,而 CatalogDAO 是一个 Fast Lane Reader 的实例,使得读访问变得迅速,如图 4:
图 4 Fast Lane Reader 设计模式
与 DAO 模式不同的是,FLR 是一个优化的模式,但不是要替代原有的访问机制,而是作为补充使其完备。当你频繁地只读大型的列数据和不必存取最新的数据时,使用 FLR 将是非常合适的。
为了高效地存取大型的远程数据列,一下子通过重新获得其元素为一个子列的 Value Object(提高远程传输效率的设计模式,这儿不详尽描述)。
分布式数据库的应用经常需要用户考虑一长列数据项,例如一个目录或一个搜索结果的集合。在这些情况下,立刻提供全列的数据经常不必要(用户并不是对所有的数据项感兴趣)或不可能(没有足够的空间)。此外,当重新获得一列数据项时,使用 Entity Bean 的代价将非常高昂,一个开销来自于使用远程探测器来收集 Requested Bean,另外,更大的开销来自对每个 Bean 产生远程调用以从用户获得数据。
通过 Iterator,客户机对象能一下子重新获得一个子列或者页的 Value Object,每一页都刚好满足客户机的需求,因此,程序使用较少的资源满足了客户机的立刻需求。
在 Java Pet Store 这个例子中,JSP 页面 product.jsp 任何时候只显示一个数据列的一部分,从 ProductItemListTag(Page-by-Page Iterator)重新获得数据项,当客户机希望看到列中的别的数据项,product.jsp 再次调用 Iterator 重新获得这些数据项,流程见图 5:
图 5 Page-by-Page Iterator 设计模式
以上设计模式的应用向我们表明,在某些特殊情况下,优化对数据库的访问模型,既可满足用户的需求又可提高访问数据库的效率。这给我们一个思路,就是:在你的硬件环境可能会产生瓶颈的情况下,可以通过对软件模型的优化来达到满足需求的目的。上述三种设计模式的应用情形为:
| Data Access Object | 需要将商业逻辑和数据存取逻辑分离; 在调度的时刻,需要支持选择数据源的类型; 使用的数据源的类型的变化,对商业对象或其它客户端完成数据存取没有影响。 |
| Fast Lane Reader | 面对大型的列数据,需要经常的只读访问; 访问最新的数据并不是至关紧要的事情。 |
| Page-by-Page Iterator | 存取大型的服务器端数据列; 任何时刻,用户只对列的一部分内容感兴趣; 整个列的数据不适合在客户端显示; 整个列的数据不适合在存储器中保存; 传输整个列的数据将耗费太多的时间。 |
在显示商品目录的时候,我们选择了 DAO 和 FLR 的结合,因为它们两者的条件都得到了满足(需要分离商业逻辑和数据存取逻辑,经常的只读访问和对即时性不敏感),此时应用将会大大发挥它们的优点。而在进行内容检索的时候,我们会选择 PPI,因为也许检索出了上千条的记录,但是用户没有兴趣立即阅读全部内容,而是一次十条地阅读,或者他在阅读完前十条记录后发觉自己的目的已经达到,接下来浏览别的网页了,都不必我们一次性地传输上千条记录给他,所以也是 PPI 的应用条件得到了满足,结果则是此模式的优点得到了发挥,又不影响全局的数据访问。
在进行软件模型的设计时,整体的框架可以应用某些优秀的、通用的设计模式,这样既加快模型的建立速度,又能和其它系统集成。但是,碰到一些瓶颈问题的情况下,我们就需要对局部的设计模式做一些调整,以优化整个系统,上述三个模式就是对原有体系的补充,它们并没有对整体的框架做出巨大的改变,却突破了某些瓶颈(瓶颈往往是局部的)障碍,让我们的产品更好地服务于用户。
开篇至今,我们主要探讨了软件层次上的解决问题,但是,必须肯定一点,如果你的硬件环境非常差(运行 Java 都有困难)或非常好(额外的存储空间、超快的运算速度和富裕的网络带宽),上述途径对你来说很难有大的帮助。前一种情况,我建议你升级硬件设备到软件厂商推荐的配置(强烈反对最小配置),以使应用服务器、数据库、Java 等软件能够运行自如;后一种情况,我没什么话可说,花钱是解决这个问题最好的办法。
本文并未谈及线程池和告诉缓冲这两个非常重要的概念,因为笔者认为,它们是针对局部时间高访问量的瓶颈问题的解决,不能理解为简单的速度瓶颈问题,所以我会在下一篇文章中分析这种特殊的情况和提出解决问题的办法。也许你对这一点更关心一些,认为自己的问题就出在这个地方,这是非常好的思考问题的方式,你已经抓住了问题的关键。但是,我还是建议你通读一下本文,让自己对速度瓶颈问题有更好的理解,并掌握在解决问题的过程中,分辨常态和暂态,从而选择不同的思路入手。其实,本文谈及的就是速度瓶颈问题的常态,而下一篇文章讨论的将会是暂态,希望你能够渐入佳境。
JDO(Java Data Object)是需要我们关注的一个 API,它定义了新的数据存取模型,直接借鉴了 DAO 设计模式。不同的数据源,有不同的数据存取技术,就有不同的 API 供开发人员使用。JDO 正是为了解决这个问题而产生的,它实现了即插即用的数据存取的实现和持久信息(包括企业数据和本地存储的数据)以 Java 为中心的视图。因此,开发人员只关注创建那些实现商业逻辑的类和用它们来表现数据源的数据,而这些类和数据源之间的映射则由那些 EIS 领域的专家来完成。如果大家对 JDO 感兴趣的话,那么我会写第三篇文章把其详细介绍给大家,并给出示例应用。
- BBS 水木清华站(telnet://smth.org)
- JDBC 的官方网站(
http://java.sun.com/products/jdbc/index.html)
- DataDirect 的 JDBC 驱动(
http://www.datadirect-technologies.com/jdbc/jdbc.asp)
- J2EE 的设计模式(
http://java.sun.com/blueprints/patterns/j2ee_patterns/index.html)
- Java 指南第二版(
http://java.sun.com/docs/books/tutorial/index.html)