作者:自由Java传道士 来自:水木清华 速度瓶颈问题的提出 在企业级的Java应用中,访问数据库是一个必备的环节。数据库作为数据资源的集散地,往往位于企业级软件体系的后方,供前方的应用程序访问。在Java技术的体系中,应用程序是通过JDBC(Java Database Connectivity)接口来访问数据库的,JDBC支持"建立连接、SQL语句查询、处理结果"等基本功能。在应用JDBC接口访问数据库的过程中,只要根据规范来操作,这些功能的实现不会出差错。但是,有些时候进行数据查询的效率着实让开发人员懊恼不已,明明根据规范编写的程序,却得不到预期的运行效果,造成了整个软件的执行效率不高。 起初,我们把问题归结于Java字节码加载和执行速度的缓慢,紧接着硬件的功能普遍得到了增强,证明这样的想法些许是错误的,还没有抓到真正的根本原因。本文将逐步解剖JDBC访问数据库的机制,深层分析造成这种速度瓶颈问题的原因,并提出在现有的Java技术框架下解决这个速度瓶颈问题的思路和方法。 JDBC访问数据库的机制 ![](https://i-blog.csdnimg.cn/blog_migrate/a2851cfbc31f3b690944c4e24a81eb48.png) 图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接口,因而我们在面对一个应用时,要慎重选择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等,对它们进行比较才能更有利于你的选择。因为暂时不存在第三方提供的数据比较结果,所以这些问题需要你对上述内容有了透彻理解之后自行解决。 Java程序中SQL语句格式的优化 这个时候,你也许还在为找不到合适的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,它们为加快数据存取速度提供了一些可以在系统设计阶段值得我们借鉴的想法。 Data Access Object 将商业逻辑从数据存取逻辑中分离出来,把存取的资源改编,从而使资源可以容易和独立地转变。 依赖于底层数据资源的特殊要素(例如数据库的供应商)的商业组件,常将商业逻辑和数据存取逻辑配合起来,只能使用特殊类型的资源,而使用不同类型的资源时,复用将会非常困难,因此,只能服务于有限的市场领域。DAO(Data Access Object)即是将数据存取逻辑从EJB中抽去出来抽象为一个独立的接口,EJB根据接口的操作执行商业逻辑,而接口针对使用的数据资源实现为DAO对象。 在Java Pet Shop这个例子中,OrderEJB组件通过关联的OrderDAO类访问数据库,自身则关注于商业逻辑的实现。在调度阶段,将配置某一类(OrderDAOCS、OrderDAOOracle或OrderDAOSybase)为OrderDAO的实现,而OrderEJB无须任何更改。图3更能帮助你明白其中的道理: ![](https://i-blog.csdnimg.cn/blog_migrate/cf7e1d835564526d7103b84918a0fdc7.png) 图3 Data Access Object的设计模式 此举增加了数据存取的弹性、资源的独立性和扩展性,但复杂度有相应的提高,其它附带的问题我们不在这儿讨论。 Fast Lane Reader 抛弃EJB,加速只读数据的存取。 有些时候,高效地存取数据比获得最新的数据更重要。在Java Pet Store中,当一个用户浏览商店的目录时,屏幕与数据库内容吻合不是至关紧要的,相反,迅速显示和重新获得非常重要。FLR模式可以加速从资源中重新获得大型的列数据项的速度,它不用EJB,而是更直接地通过DAO来存取数据,从而消除EJB的经常开支(例如远程方法调用、事务管理和数据序列化等)。 在Java Pet Store这个例子中,当一个用户浏览目录时,通过CatalogDAO(而不是CatalogEJB)从数据库加载数据项,而CatalogDAO是一个Fast Lane Reader的实例,使得读访问变得迅速,如图4: ![](https://i-blog.csdnimg.cn/blog_migrate/f6b916c73a440388fb97e80eb45c7825.png) 图4 Fast Lane Reader设计模式 与DAO模式不同的是,FLR是一个优化的模式,但不是要替代原有的访问机制,而是作为补充使其完备。当你频繁地只读大型的列数据和不必存取最新的数据时,使用FLR将是非常合适的。 Page-by-Page Iterator 为了高效地存取大型的远程数据列,一下子通过重新获得其元素为一个子列的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: ![](https://i-blog.csdnimg.cn/blog_migrate/8ac370e413a32c6832c9ff55634f7151.png) 图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感兴趣的话,那么我会写第三篇文章把其详细介绍给大家,并给出示例应用。 |