在企业Web应用方面的6点JDBC提示作者:William C. R. Crawford 这些简单的指导能够帮助你提升Java应用程序的性能。 最近几年,从客户/服务器体系结构向以Web为中心的体系结构的转变已经改变了开发人员创建企业应用程序的方式。今天编程方面面临的挑战不再集中在建立高效的桌面客户端上,而是集中到创建高效的服务器端Web组件上。服务器端应用程序的开发产生了完全不同的一系列问题。 Java语言由于在基于Web的开发和企业应用程序方面的适用性,已经成为众多Oracle开发者的首选语言。由于Java的灵活性、可移植性和可扩展性,企业应用程序开发者都乐于采用它。对于数据库应用程序来说,开发者可以使用JDBC(Java database connectivity API)从Java应用程序中访问关系数据库。 JDBC可以在任意种类的Java程序中使用,包括applet和单独的应用程序。在这篇文章中,我们将侧重介绍在Java servlet和企业应用程序中的Oracle和JDBC。Servlet驻留在一个Web容器中,并提供相应的内容来响应来自Web浏览器的HTTP请求。这些Web应用程序产生了其特有的一些问题:它们运行几天、几个星期或几个月就要重新启动一次,而且必须易于扩展,来满足大量用户的访问需求。恰当处理Oracle连接性问题能够在所有方面产生显著的性能提升。 这篇文章并不详细讨论JDBC或servlet的细节。如果你已经学习过API,那么这里提供的信息将帮助你有效地运用它们。如果你刚刚接触Oracle和Java,当你在处理实际的实现时,你将会知道可能发生什么,该记住什么。 提示1:把连接放入缓冲池中在Java应用程序能够访问一个Oracle数据库中的数据之前,它们需要访问一个激活的数据库连接。这些数据库连接是稀少的、I/O密集的资源,它们每一次被创建时都会导致一些性能损失。甚至当一个数据库和应用程序客户端驻留在同一台机器上的时候,创建一个数据库连接都要花费几秒钟时间。对于桌面应用程序来说,因为连接一直保持到用户会话结束,所以这点性能损失通常不是问题。主要的损耗发生在应用程序启动时和一系列活动结束后连接被重新使用时。在Java servlet环境中,与这个方法等效的是,在每个用户请求的开始时创建一个数据库连接并在请求结束时释放它。这个方法很快就不适用了。在一个Web就应用程序中,一个单独的用户会话可以产生几个请求/响应循环。为每个新的请求创建和关闭数据库连接将会带来极大的延迟。相关链接 
Oracle JDBC 驱动器下载、相关文档, 以及更多的信息请访问
otn.oracle.com/software/tech/java/sqlj_jdbc 
 
Servlet有一个简单的生命周期:它们由服务器创建,调用init()方法时初始化,为一系列请求服务,最终通过调用destroy()方法使其无效。大多数经过良好编码的Web应用程序利用这个生命周期来创建性能损耗大的资源,如数据库连接,在初始化阶段使用它们为请求服务,然后在进程结束时释放它们。 当使用servlet时,一个有吸引力的解决方法或许就是创建一个单一的java.spl.Connection对象,然后把它作为全局变量存储在servlet中。尽管这样解决了跨多个请求管理数据库连接的问题,但它也带来其他几个问题。首先,使用事务变得不可能了,因为默认情况下,servlet是多线程的;其次,这种方法会导致每个servlet至少要一个数据库连接,如果系统调用上百个selvlet来处理少量用户发来的请求的话,就有问题了;再次,没有连接维护机制,不能在应用程序内部执行一系列的检查,缺乏必要时重新创建Connection对象的能力。这些额外的工作会产生许多额外的代码,并且会导致一些多线程复杂性。 更好的解决方法是创建一个连接池。连接池是一个可被servlet中的代码根据需要来访问的 Connection对象集。代码可以从连接池中请求一个现存的连接,该请求一旦被同意,它就独占地使用那个连接直到显式地把它返回到连接池。 因为JDBC 2.0 在API中提供了对连接共享的支持,完整的J2EE实现使连接共享更容易。可以在应用服务器级对连接池进行配置,可以通过JNDI(Java Naming and Directory Interface,Java命名和目录接口)检索独立的Connection对象。由JNDI调用返回的对象实际上是底层Connection对象的封装,所以调用close()会把连接返还到连接池而不是真的关闭它:
Connection con = null;
try {
 ds = (DataSource)myContext.lookup("jdbc/oracleServer");
 pooledCon = ds.getConnection("scott", "tiger");
 // Processing Code goes here
} catch (Exception ignored) {
// catch JNDI or JDBC exceptions here
} finally {
if(pooledCon != null)
   pooledCon.close();
}
注意在上面的例子中,在try...catch...finally块中嵌入了所有数据库访问代码。即使捕获了一个异常,在finally块中的代码也将会执行。通过这种方式关闭连接可以防止“连接泄漏(connection leak)”,连接池泄漏时,连接池会逐渐耗尽,由于需要连接池创建更多的连接并最终完全锁住应用程序,而降低了性能。

解决方案 »

  1.   

    如果你不是在支持JDBC 2.0连接共享的完整J2EE环境中运行程序,那么你可以并且应该使用连接代理来利用连接池。你可以写你自己的实现或是使用在互联网上可用的许多成熟的实现中的一个。大多数外部代理不依赖于封装,而是需要应用程序显式地把连接传给代理: 
     Broker b = new Broker(...);
     Connection con = b.getConnection();
     try {
    Statement s = con.createStatement();
    ...
     } catch (SQLException e) {
    e.printStackTrace();
     } finally {
    if (con != null)
    b.freeConnection(con);
     }
    提示2:利用PreparedStatements执行许多SQL语句的JDBC程序产生大量的Statement和PreparedStatement对象。通常认为PreparedStatement对象比Statement对象更有效,特别是如果带有不同参数的同一SQL语句被多次执行的时候。PreparedStatement对象允许数据库预编译SQL语句,这样在随后的运行中可以节省时间并增加代码的可读性。 然而,在Oracle环境中,开发人员实际上有更大的灵活性。当使用Statement或PreparedStatement对象时,Oracle数据库会缓存SQL语句以便以后使用。在一些情况下,由于驱动器自身需要额外的处理和在Java应用程序和Oracle服务器间增加的网络活动,执行PreparedStatement对象实际上会花更长的时间。 然而,除了缓冲的问题之外,至少还有一个更好的原因使我们在企业应用程序中更喜欢使用PreparedStatement对象,那就是安全性。传递给PreparedStatement对象的参数可以被强制进行类型转换,使开发人员可以确保在插入或查询数据时与底层的数据库格式匹配。当处理公共Web站点上的用户传来的数据的时候,安全性的问题就变得极为重要。传递给PreparedStatement的字符串参数会自动被驱动器忽略。最简单的情况下,这就意味着当你的程序试着将字符串“D'Angelo”插入到VARCHAR2中时,该语句将不会识别第一个“,”,从而导致悲惨的失败。几乎很少有必要创建你自己的字符串忽略代码。 在Web环境中,有恶意的用户会利用那些设计不完善的、不能正确处理字符串的应用程序。特别是在公共Web站点上,在没有首先通过PreparedStatement对象处理的情况下,所有的用户输入都不应该传递给SQL语句。此外,在用户有机会修改SQL语句的地方,如HTML的隐藏区域或一个查询字符串上,SQL语句都不应该被显示出来。提示3: 关闭PreparedStatement对象大多数使用Oracle的程序员都会遇到另一个不充足的数据库资源:游标。Oracle为每一个SQL SELECT语句都创建一个数据库游标,并且维护那个光标直到应用程序关闭该语句。在JDBC应用程序中,关闭该语句就意味着调用用来运行该SQL语句的Statement对象 或 PreparedStatement对象的close()方法。从一个现存的Statement对象或PreparedStatement对象执行一个新的查询也会关闭与前一个查询相关的游标。考虑下面的代码:
    PreparedStatement pstmt = dbCon.prepareStatement("select * from emp where emp_id=?");
    pstmt.setInt(1, 422);
    ResultSet rs = pstmt.executeQuery();
    // processing of result set
    pstmt = dbCon.prepareStatement("select * from customers where cust_id = ?");
    // etc
    上面的代码段有一个主要的问题。如果该代码是用普通的Statement对象写的,那么它很可能已经通过多次调用executeQuery(String)方法创建了ResultSet对象。但是,因为上面的代码使用的是PreparedStatement对象而不是普通的Statement对象,它需要显式地关闭第一个对象而不是简单地在同一个地方创建一个新的对象。上面的代码,在连接自身被关闭之前将会用"select * from emp where emp_id=?"游标扰乱数据库。 尽管这个bug似乎相当明显,Java和Oracle的程序员往往都假设这些资源会被自动释放。由于Oracle JDBC驱动器的体系结构,Java垃圾收集器(garbage collector)不会清理那些孤立的PreparedStatement对象,并且,在大容量的应用程序中游标溢出(cursor leak)会很快累积。因此,假设PreparedStatement对象会自动释放是程序员最常犯的错误之一。 如果你已经访问了SYS.V_$OPEN_CURSOR视图,下面的SQL语句能够容易地发现游标溢出: 
    select user_name, sql_text, count(*) from sys.v_$open_cursor  
    group by user_name, sql_text
    如果你在一个常规的查询或一个触发器中单独使用该语句执行PL/SQL过程,那么关闭Statement对象尤为重要。如果PL/SQL过程创建了多个隐式游标,在父语句被关闭之前,即使它们的工作完成了,它们有时也不能被恰当地关闭。 提示4:正确地使用事务在创建可靠的系统时,正确地使用事务是非常关键的:如果登记一项销售额涉及到改变三个独立的表,那么数据库应该在这三个表中反映出这些变化或者所有表中都不反映。在Oracle中,通过执行一系列SQL 语句来处理事务,然后把它们“提交”到数据库,或者“回滚”到事务开始。高效的事务设计通常是系统成功部署的关键。核心的JDBC包支持4种“transaction isolation modes(事务隔离模式)”,允许程序指定它们希望的事务行为。Oracle支持它们中的两种:默认的 “read committed(读提交数据)” 和更安全的“serializable(可串行化)”模式。“read committed” 模式支持不可重复读(non-repeatable read),在这种情况下,一个事务所做的修改对查询中的另一个事务是可见的;还支持错误读取(phantom read),这种情况下,被其他事务所做的修改对于运行的查询是可见的。依赖于在多个操作中数据库的完全一致视图的应用程序应该选择损失一定的性能,使用更严格的“serializable(可串行化)”设置。
      

  2.   

    JDBC连接在默认的情况下也在“auto-commit”(自动提交)模式下运行,这意味着每个语句一执行完就立即提交给数据库后。这种模式有一些优点,开发人员不需要在每次更新或插入操作之后调用commit() 方法。它还可以确保最新的数据库视图。另一方面,关闭自动提交将可以稍微提高一点性能,并且将允许多语句事务。 在servlet环境中使用JDBC事务时,与使用连接共享时应用了许多相同的规则。在代码的结尾处或者回滚或者提交事务是非常重要的。确保这一点的最好方法是使用try...catch...finally 块。不完整的事务可能产生不可预见的后果,包括从不完整或不一致的数据库视图到当事务一个接一个排队等候访问被锁定的资源时使整个应用程序被锁定的种种情况。 下面的例子说明如何使用JDBC事务:
    Connection con = null;
    try {
     ds = (DataSource)myContext.lookup("jdbc/oracleServer");
     pooledCon = ds.getConnection("scott", "tiger");
     pooledCon.setAutoCommit(false); pooledCon.setTransactionIsolation(
     Connection.TRANSACTION_SERIALIZABLE);
     // ..
     pooledCon.commit();
    } catch (Exception ignored) {
    try { pooledCon.rollback(); } catch (SQLException ig) {}
    } finally {
    if(pooledCon != null) {
        pooledCon.setAutoCommit(true);
      pooledCon.close();
    }
    }
    这个例子更新了前面的例子以包含一个事务(新的程序行用粗体标出)。注意,我们设置自动提交为false,设置事务隔离值,然后在主try {}块的底部提交这个事务。如果有异常,则调用rollback()方法撤销事务,并在这两种情况下在最后的语句中将事务的状态设置回自动提交。这个例子中的最后一步很可能是多余的,因为JDBC连接池会负责关闭数据库连接,但如果你正在使用其他的共享模式,确保用相同的方式关闭你从连接池中得到的每一个连接是极为重要的。尽管常规事务很有吸引力,但无论你使用哪种连接管理模型,都绝不应该将它们推广应用到对服务器的多个请求(一个或更多servlet的多个调用)。用户决不应该在一个事务没有完成时就发出第二个请求。需要跨多个对象或多个请求中分布事务的应用程序应该考虑其他可选的体系结构。举例来说,同Java事务处理API (JTA)一起的JDBC 2.0可选包,使与完善的事务处理服务器的集成成为可能。 如果你正在运行Oracle9i第2版,你将可以获取JDBC 3.0中引入的一些新的事务特性。这些特性中最重要的是事务存储点(Transaction Savepoint),它允许你将事务回滚到某一特定点,而不是要么全部回滚、要么全不回滚。 提示5:有效地划分你的Java和数据库代码应用程序后台的功能强大的数据库的重要之处在于,开发者能够依赖它快速、有效地进行数据管理。Oracle能执行复杂的数据操作,如合并、排序和比经过最佳调优的Java代码更快的连接操作。通过让数据库做尽可能多的工作,你可以获得很大的性能提升。另一方面,当你需要将数据提供给那些不能或不应该访问底层SQL表的开发人员时,将处理功能从SQL语句上转到Java代码中就很有意义。这通常通过写Enterprise JavaBeans(或普通的JavaBeans)并使它们对servlet、Java Server Pages (JSP)或其他资源可用来实现。然而,即使当你使用Java对象时,你仍要确保你的应用设计高效地使用数据库。考虑一个有两个表的例子--CUSTOMERS和ORDERS--它们通过客户号连接起来。如果要查找最近一个订单的客户的名字,开发人员可以查找最近的订单日期,在第二个查询中开发者可以使用查到的订单日期找到相关的客户号,然后在第三个查询中使用该客户号就可以查到客户名了。但代替执行以上三个查询的一个更好的方法是创建一个单一的、更复杂的查询:
    select cust_no, cust_name from customers where cust_no = 
    (select max(cust_no) from orders where order_date = 
    (select max(order_date) from orders))
    这个单一查询可以通过一个JDBC调用被发送到数据库。因为它需要更少的网络传输,所有的Oracle优化可以一次执行,并且Java虚拟机JVM运行应用程序只需做更少的工作就可以创建一个完整的进程,所以它比三个独立的查询运行得更快。 此外,请注意,Oracle9i对Java存储过程的支持提供了在所有应用程序和服务器上划分Java代码的能力。OracleJVM是一个符合J2SE 1.3的Java虚拟机,它允许开发人员在数据库中直接部署Java程序。这种方法提供了对PL/SQL存储过程的另一种开放的、可移植的机制,并且可以更容易地划分代码。提示6:选择正确的驱动程序如果不涉及部署的问题,关于Java-Oracle性能的讨论就是不完整的。这个问题涉及可靠性、易用性和性能之间的折衷处理。 首先,你应该选择正确的Oracle JDBC驱动程序。有两种类型可以选择:纯Java驱动程序和Java/Net8混合型。一个有益的提示,所有由Oracle在Oracle8i或之后的产品中提供的驱动程序产品都是非常稳定的,并且在高性能环境中很好地运行。在很多情况中,混合驱动程序的性能更好--但你要对它进行你自己的测试,因为两种驱动程序的性能都依赖于你的特定环境。 不像混合驱动程序,纯Java驱动程序避免了将Java代码和本地代码混合时潜在的可靠性问题,能够简单地在所有支持Java的平台上进行部署. 驱动程序类型间的特性集已经不再是一个重要的因素了,因为现在纯Java驱动程序也能够提供混合型驱动程序的功能。 知道底层体系结构--网络和服务器物理地点也是非常重要的。花十分钟优化数据库服务器和Web服务器间的网络连接--甚或是独立机器的网络配置--都会比花一周时间进行SQL语句优化产生更大的性能提升。特别是要避免应用程序的任意两个组件之间有一个广域网。总结即使你按照本文的所有建议正确地部署了你的系统,并写出了近乎完美的代码,测试和评估性能仍然是至关重要的。但是,只要你的设计和编码正确,你的应用程序就会成功地通过上述测试和评估。William C. R. Crawford是位于麻省剑桥的Invantage公司的首席技术官。他是Java Enterprise in a Nutshell第二版和Java Servlet Programming第二版的作者(全部由 O'Reilly & Associates出版)。可以通过 www.williamcrawford.info与他联系。 
      

  3.   

    原帖URL:http://www.oracle.com/global/cn/oramag/oracle/02-sep/index.html?o52jdbc.html