Apusic应用服务器的数据源管理

Apusic应用服务器的数据源管理

 

1 前言
在基于J2EE平台的应用开发中,大多数的应用都需要跟数据库打交道;而自从接触JDBC起,我们便不止一次的被告之:数据库资源是十分宝贵的系统资源,一定要谨慎使用。但令人遗憾的是,在笔者见过的大部分跟数据库相关的应用开发中,针对数据库资源的使用总是充斥着这样或者那样的问题。在本文中,笔者针对常见的一些错误或者不当的使用数据库资源的案例进行介绍与分析,并阐述金蝶Apusic应用服务器提供的一些增值特性,通过这些特性能够有效的避免某些错误的发生。
2 常见数据库资源错误/不当用法的案例分析
2.1 未正确的关闭数据库连接
申请了数据库连接,却没有及时的关闭它,这几乎是最常见的数据库连接使用错误。犯这种错误的原因有很多,以下是常见的一种低级错误:

 
public void foo() {
1  Connection conn = getConnection();
 
  Statement stmt = null;
 
  try {
 2   conn = getConnection();
 
    stmt = conn.createStatement();
 
  } catch (Exception e) {
 
  } finally {
 
    close(stmt, conn);
 
  }
 
}

 

<示例代码一>

在上述案例中的第1 行代码中,作者已经申请了一个Connection,但在第2 行代码中,又申请了一个新的Connection,并且丢失了第一次申请的connection的引用,至此,当程序每调一次foo方法,将导致申请一个新的Connection而没有释放它,如此一来,当数据库达到能够承受的最大连接数时,将导致整个应用的运行失败。
避免这种错误的方法有很多,譬如,可采用类似于 FindBugs(注1)的代码分析工具对应用的源码进行分析,找出可能产生错误的代码。
此外,在应用中,我们需要非常频繁的对申请的数据库连接进行关闭与释放,此时,建议封装成某些工具类使用,并且要尽可能安全的关闭数据库连接。下面,我们以关闭Statement及Connection的通用close方法的不同实现方案来比较:
不安全的关闭方法:

 

 
private void close(Statement stmt, Connection conn) {
 
  try {
1   stmt.close();
2   conn.close();
 
  } catch (Exception e) {
 

 

 
<示例代码二>

在上述代码中,倘若第 1行代码中的stmt为空,或者stmt.close()方法出错并抛出异常,都将使第2 行代码不能够正常调用,从而导致数据库连接无法释放,那么,更安全的写法应该是:
安全的关闭数据库资源方法:

 

 
private void close(Statement stmt, Connection conn) {
 
  try {
1   if(stmt!=null) stmt.close();
 
  } catch (Exception e) {}
 
  try {
2   if(conn!=null) conn.close();
 
  } catch (Exception e) {}
 

 

 
<示例代码三>

在修订后的代码中,我们可以看到,无论第1 行代码中关闭stmt是否成功,程序都能够保证向下执行,从而正确的关闭conn。
这些常用的数据库资源操作公用类,可以使用Apache的 Commons DbUtils(注2)组件。
2.2 任意的申请数据库连接
不考虑事务上下文,任意的申请数据库连接资源,也是常见的一种不当用法。但这种问题往往是难以克服的,根源在于Java是一种面向对象的语言,而数据库的事务却是一种批量化的操作过程。我们以常见的“序列号”的实现方案为例:在某些应用场景中,我们需要一种自增长的整数型字段,但由于不同的数据库有不同的实现,所以,为达到各个数据库兼容的目的,我们常用的解决方案是,新建一张 T_SEQUENCE表,它可能包含的字段有: NAME varchar(100), CURRENT_VAL number(10);其中, NAME存放序列的名称,而 CURRENT_VAL存放序列的当前值。假设某一业务对象Customer需要新增一笔记录时,为获得不重复且自增长的Customer ID,需要将 T_SEQUENCE表中的与该业务表对应的序列号加1并更新,然后将更新后的值作为Customer的ID,如下述表格所示:

T_SEQUENCE
NAME
CURRENT_VAL
CUSTOMER
10

 

T_CUSTOMER
ID
CUSTOMER_NAME
9
Kevin
10
Mary

 

于是,在Java语言中,我们以面向对象的方法来实现,可能会是这样(常见写法,未必是最优实现):

  public class Customer {

  public void sequencePlus() {
    Connection conn = null;
    Statement stmt = null;
    try {
   conn = getConnection();
      stmt = conn.createStatement();
      String sql =  "update T_SEQUENCE set CURRENT_VAL = CURRENT_VAL + 1 "
                    + " where NAME = 'CUSTOMER'"
;

      stmt.execute(sql);
    catch (Exception e) {
      e.printStackTrace();
    finally {
      DbUtils.closeQuietly(stmt);
      DbUtils.closeQuietly(conn);
    }
  }
  
  public int getSequenceCurrentVal() {
    Connection conn = null;
    Statement stmt = null;
    ResultSet rset = null;
    int id = 0;
    try {
      conn = getConnection();
      stmt = conn.createStatement();
      String sql = "select CURRENT_VAL from T_SEQUENCE where NAME = 'CUSTOMER'";
      rset = stmt.executeQuery(sql);      
      if (rset.next()) {
        id = rset.getInt(1);
      }      
    catch (Exception e) {
      e.printStackTrace();
    finally {
      DbUtils.closeQuietly(conn, stmt, rset);
    }
    return id;
  }
  
  public void addCustomer(String name) {
    Connection conn = null;
    PreparedStatement stmt = null;
    ResultSet rset = null;
    try {
      sequencePlus();
      int id = getSequenceCurrentVal();
   conn = getConnection();
       stmt = conn.prepareStatement(
          
"insert into T_CUSTOMER(ID, CUSTOMER_NAME) values(?, ?)");  

      stmt.setInt(1, id);
      stmt.setString(2, name == null "" : name);
      stmt.execute();
    catch (Exception e) {
      e.printStackTrace();
    finally {
      DbUtils.closeQuietly(stmt);
      DbUtils.closeQuietly(conn);
    }
  }  
} 
<示例代码四>

针对这种应用场景,我们首先需要认识到:上述的三个方法应该属于同一个数据库事务,否则,在并发情况下,将出现由于主键重复而导致数据插入失败的情况。但同时,我们也需要看到:即便上述三个方法的执行位于同一个事务中,但三个方法使用的是不同的数据库连接,虽然在 sequencePlus方法中将T_SEQUENCE表中的数据加1 ,但在事务并未提交的情况下,由于Connection隔离级别的原因,在getSequenceCurrentVal方法中,是看不到sequencePlus方法中更新以后的数据的,这样,也将导致数据插入失败,因为主键势必跟旧有ID值重复。
因此,传统的编程方法中,为克服上述问题,只有在上述的方法中使用同一个Connection,才能够保证业务数据的正确。但这样一来,将影响我们以OO方法分析问题时的“纯洁”性,很容易让人厌倦。
2.3 将Connection作为成员变量
另外一种常见的不当编程模式是将Connection作为类的成员变量。一般来说,针对Connection,我们采取的策略是:用时再申请,用完立即释放。而将Connection作为成员变量,将是对该规则的严重挑战,容易引起若干编程错误。举例而言:成员变量级的Connection,何时创建?何时释放?倘若在每一个方法体内进行Connection的创建与释放,那么将Connection作为成员变量又失去了意义;倘若在类的构造期内进行Connection的创建,那么又在何时释放它呢?因为在Java语言内,你是无法控制对象的生命周期的。
将Connection作为成员变量还会产生另外一个问题:资源的闲置浪费。因为在申请连接以后,该资源将在这个对象的生命之期之内一直有效,即使该对象处于非使用状况,这无疑是一种资源的浪费。更有甚者,倘若这种对象过多,将造成数据库达到最大连接数,造成应用运行失败。
3 金蝶Apusic应用服务器的数据源管理
金蝶Apusic应用服务器支持业界主流的各种数据库,在Apusic应用服务器之内进行数据源的配置与使用都非常简单,同时,它提供了许多增值特性,能够为应用的正常运行提供额外的保障。
3.1 数据库连接池的逻辑连接与物理连接
我们注意到:java.sql.Connection是一个Interface,那么,真正实现这个接口的类是什么呢?
我们可以做一个简单的测试案例,在普通的Java Application中,调用如下方法:

public void showConnection() {
    Connection conn = null;
    try {
      Class.forName("oracle.jdbc.driver.OracleDriver");
      conn = DriverManager.getConnection(

 

"jdbc:oracle:thin:@localhost:1521:KEVINORA""system""manager");
      System.out.println("Connection Class is:" + conn.getClass().getName());
    catch (Exception e) {
      e.printStackTrace();    
    finally {
      DbUtils.closeQuietly(conn);
    }    
  }
<示例代码五>

得到的输出结果是: Connection Class is: oracle.jdbc.driver.T4CConnection
而在Apusic应用服务器中运行如下方法:  

 

public void showConnection() {
    Connection conn = null;
    try {
      Context ctx = new InitialContext();
      ds = (DataSource) ctx.lookup("jdbc/oracle");
      conn = ds.getConnection();
      System.out.println("Connection Class is:" + conn.getClass().getName());
    catch (Exception e) {
      e.printStackTrace();
    finally {
      DbUtils.closeQuietly(conn);
    }
  }
<示例代码六>

得到的输出结果是: Connection Class is: com.apusic.jdbc.adapter.ConnectionHandle

 

明明用相同的JDBC Driver连接同一个数据库,为什么取得的Connection却是不同的类呢?事实上,通过Apusic应用服务器获得的数据库连接其实只是一个逻辑连接,真正的物理连接隐藏在该逻辑连接之内,这是一个典型的Delegate模式,而恰恰是这个模式,通过 Apusic应用服务器对数据源进行管理,将给我们的应用开发带来很多好处:
3.2当事务结束以后,在该事务上下文中申请的物理连接,都将主动释放
我们以一个最简单的Stateless Session Bean为例: 

public class SimpleBean implements SessionBean {

  public void foo() {    
    Connection conn = null;
    try {
      Context ctx = new InitialContext();
      DataSource ds = (DataSource) ctx.lookup("jdbc/oracle");
      conn = ds.getConnection();
      System.out.println("not release connection");
    catch (Exception e) {
      e.printStackTrace();
    finally {      
//      Not close the connection
//      DbUtils.closeQuietly(conn);
    }    
  }
}
<示例代码七>

SimpleBean中的foo方法的事务属性设置为Required,在该方法中,我们申请了一个数据库连接,但并没有释放它,在运行之前,我们通过SQLPlus观察Oracle数据库的Session,得到的结果是:
SQL> select count(*) from v$session;
  COUNT(*)
----------
        18

<图一 执行方法之前的Oracle Session>

而在执行完SimpleBean的foo方法之后,我们再次观察Oracle数据库的Session,得到的结果是:

SQL> select count(*) from v$session;
 
  COUNT(*)
----------
        18
<图二:执行方法之后的Oracle Session>

由此,我们可以得知:即便由于程序的书写错误,没能够释放申请的数据库连接,但Apusic应用服务器在事务完成之后,能够把该事务上下文中申请的物理连接主动释放,这对提升应用的容错性带来一定的好处。
3.3 当jsp/servlet运行结束以后,在jsp/servlet中申请的物理连接,都将主动释放
同事务中申请的数据库连接会主动释放一样,在jsp/servlet中申请的数据库物理连接,当jsp/servlet运行完毕以后,如果用户没有释放这些连接,Apusic应用服务器也将予以主动释放。读者可以尝试自己做一个案例:在jsp中申请一个连接,故意不释放,在jsp执行完毕以后,可以通过SQLPlus或者Apusic性能监控工具,查看连接是否已经被应用服务器主动释放。
由上述两节内容我们可以看到,Apusic应用服务器能够有效避免2.1节中所描述的问题。
3.4 Connection Sharing:同一个事务上下文中申请的物理连接可以共享
通过共享连接可以更有效地使用资源及提高性能,并且可以防止连接之间的资源锁定问题。
例如两个EJB组件A和B,它们的事务属性都设置为Required。在调用EJB A的方法时打开了一个数据库连接,并对数据库中的某个表进行了更新操作,而在关闭连接之前EJB A调用了EJB B的某个方法,同样EJB B打开同一个数据库的连接,也对数据库中同一个表进行了更新操作。倘若没有连接共享机制,这两个连接指向的是两个不同的物理连接,在其上执行的数据库操作将会互相锁定,而这种死锁状态是无法恢复的。现在有了连接共享机制可以有效地解决这个问题。在EJB A和B中所获得的连接对象实际上都指向同一个物理连接。这一个过程可以简单描述如下:

con1 = getConnection();
Transaction.begin
perform database operation on con1
con2 = getConnection();
perform database operation on con2
con2.close();
Transaction.commit();
con1.close();
<示例代码八>

无论两个连接是在事务边界之内或之外打开和关闭都没有问题。只有在一个事务边界之内连接才会被共享,如果一个连接是在事务边界之外打开的,那么在事务开始时会将此连接参与到事务中,并找到一个具有正确事务场景的物理连接和连接对象相关联。在离开事务场景之后如果连接对象仍未关闭,则将其关联到一个不具有事务场景的物理连接。
可以在部署描述中指定一个资源引用的res-sharing-scope属性来允许或禁止连接共享,属性值shareable为允许共享,unshareable为禁止共享,缺省情况下为允许共享。
回到2.2节中Customer那个测试案例,我们已经说过,Customer的 sequencePlus方法、getSequenceCurrentVal方法、以及addCustomer方法,需要放在一个事务中处理。但在这三个方法中,使用的是不同的Connection,而由于Connection的隔离级别,将导致插入T_CUSTOMER表中的ID主键将重复,最终导致事务回滚。利用Apusic应用服务器连接共享特性,能够很好的解决这个问题。也就是说:虽然这三个方法申请的逻辑连接是不同的,但逻辑连接内部所使用的物理连接是同一个,这样,将保证不同方法中对数据库的操作结果相见可见,从而保证事务的正常提交。
举例如下:假设在一个jsp文件中,这样调用:

<%
    InitialContext ctx = new InitialContext();
    String txName = "java:comp/UserTransaction";
    UserTransaction tx = (UserTransaction)ctx.lookup(txName);
    tx.begin();
    new Customer().addCustomer("eric");
    tx.commit();
%>


     InitialContext ctx =  new  InitialContext();
     String txName =  "java:comp/UserTransaction" ;
     UserTransaction tx = (UserTransaction)ctx.lookup(txName);
     tx.begin();
     new  Customer().addCustomer( "eric" );
     tx.commit();
 在上述代码中,通过 UserTransaction 启动一个事务,然后在该事务上下文中,增加一笔Customer的记录,我们发觉,在不需要更改Customer类的情况下,上述方法能够正常完成。

由此可以得知:在Apusic应用服务器中进行应用的开发,我们无需因为考虑数据库Connection的隔离级别而影响我们对系统的面向对象的分析方法,Apusic应用服务器将替我们保证在同一事务上下文中,使用相同的物理连接。
通过Apusic应用服务器的这个特性,能够有效的解决2.2节中描述的问题。
3.5 Lazy Connection Association Optimization: 数据库连接延迟关联的优化机制
在3.1节中我们谈到:通过Apusic应用服务器管理的数据库连接分逻辑连接与物理连接,物理连接隐藏在逻辑连接的背后。那么,逻辑连接何时与一个真正的物理连接相关联的呢?在关联的过程之中,Apusic应用服务器又提供了哪些优化机制呢?举例如下:
J2EE组件可能会将连接对象保存在其实例变量中从而可以在多个事务之间重复使用,但是如果这个组件在使用一次之后就很少再被用到,那么系统资源将会被组件白白占用而得不到释放,当连接池被占满时就再也无法获得新的连接。Lazy Connection Association Optimization是这样一种机制,当J2EE组件方法调用完成时,释放连接对象所指向的物理连接以供其他组件使用,连接对象进入一个Inactive状态,在这个状态下它不和任何物理连接相关联。当J2EE组件需要使用该连接对象时,容器将其激活,将其和一个实际的物理连接相关联。这一过程对于应用组件来说是完全透明的。J2EE程序员经常犯的一个错误是忘记关闭连接,特别是发生异常时没有执行正确的清理,过去我们解决这一问题是在方法调用完成时强制关闭所有的连接,现在有了Lazy Connection Association Optimization机制可以更完美地解决这一问题。
Connection Sharing和Lazy Connection Association Optimization是同时起作用的,例如,当一个连接被激活时,它将被包含在当前事务场景中,并与同一事务场景中的其他逻辑连接共享同一个物理连接。
我们在2.3节中强调:将Connection作为成员变量是一种糟糕的设计模式,但同时,我们也看到:哪怕用户旧有系统中存在这样的用法,Apusic应用服务器也能够很好的解决由于这种糟糕的设计所带来的缺陷。
4 总结
本文首先与读者分析了一些错误或者不当的数据库资源使用方法,然后简要介绍了金蝶Apusic应用服务器在数据源管理上的一些特性。这些特性,对应用的健壮性及容错性带来一定的好处。但需要再次提醒的是:应用服务器提供的一些增值特性,仅能够当作保障我们应用正常运行的最后一道屏障,我们切不可依赖于这些特性而忽视程序自身的编码质量。一个J2EE应用能否正常的运行,程序自身的设计与编码永远是主要因素。
5 参考资料
1FindBugsSourceforge上的一个开源工具,能够对源码进行分析从而发现可能出现的编程错误,http://findbugs.sourceforge.net/

 

2Commons DbUtils: Apache Jakarta项目的Commons组件,http://jakarta.apache.org/commons/index.html

 

3 金蝶Apusic应用服务器:国内首家通过J2EE 1.4认证的应用服务器,请参考http://www.apusic.com
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值