1.
前言
在基于
J2EE
平台的应用开发中,大多数的应用都需要跟数据库打交道;而自从接触
JDBC
起,我们便不止一次的被告之:数据库资源是十分宝贵的系统资源,一定要谨慎使用。但令人遗憾的是,在笔者见过的大部分跟数据库相关的应用开发中,针对数据库资源的使用总是充斥着这样或者那样的问题。在本文中,笔者针对常见的一些错误或者不当的使用数据库资源的案例进行介绍与分析,并阐述金蝶
Apusic
应用服务器提供的一些增值特性,通过这些特性能够有效的避免某些错误的发生。
2.
常见数据库资源错误/不当用法的案例分析
2.1.
未正确的关闭数据库连接
申请了数据库连接,却没有及时的关闭它,这几乎是最常见的数据库连接使用错误。犯这种错误的原因有很多,以下是常见的一种低级错误:
publicvoidfoo(){
Connectionconn=getConnection();
Statementstmt=null;
try{
conn=getConnection();
stmt=conn.createStatement();
}catch(Exceptione){
}finally{
close(stmt,conn);
}
}
<
示例代码一
>
在上述案例中的第
2
行代码中,作者已经申请了一个
Connection
,但在第
5
行代码中,又申请了一个新的
Connection
,并且丢失了第一次申请的
connection
的引用,至此,当程序每调一次
foo
方法,将导致申请一个新的
Connection
而没有释放它,如此一来,当数据库达到能够承受的最大连接数时,将导致整个应用的运行失败。
避免这种错误的方法有很多,譬如,可采用类似于
FindBugs(
注
1)
的代码分析工具对应用的源码进行分析,找出可能产生错误的代码。
此外,在应用中,我们需要非常频繁的对申请的数据库连接进行关闭与释放,此时,建议封装成某些工具类使用,并且要尽可能安全的关闭数据库连接。下面,我们以关闭
Statement
及
Connection
的通用
close
方法的不同实现方案来比较:
不安全的关闭方法:
privatevoidclose(Statementstmt,Connectionconn){
try{
stmt.close();
conn.close();
}catch(Exceptione){}
}
<
示例代码二
>
在上述代码中,倘若第
3
行代码中的
stmt
为空,或者
stmt.close()
方法出错并抛出异常,都将使第
4
行代码不能够正常调用,从而导致数据库连接无法释放,那么,更安全的写法应该是:
安全的关闭数据库资源方法:
privatevoidclose(Statementstmt,Connectionconn){
try{
if(stmt!=null)stmt.close();
}catch(Exceptione){}
try{
if(conn!=null)conn.close();
}catch(Exceptione){}
}
<
示例代码三
>
在修订后的代码中,我们可以看到,无论第
3
行代码中关闭
stmt
是否成功,程序都能够保证向下执行,从而正确的关闭
conn
。
这些常用的数据库资源操作公用类,可以使用
Apache
的
CommonsDbUtils(
注
2)
组件。
2.2.
任意的申请数据库连接
不考虑事务上下文,任意的申请数据库连接资源,也是常见的一种不当用法。但这种问题往往是难以克服的,根源在于
Java
是一种面向对象的语言,而数据库的事务却是一种批量化的操作过程。我们以常见的“序列号”的实现方案为例:在某些应用场景中,我们需要一种自增长的整数型字段,但由于不同的数据库有不同的实现,所以,为达到各个数据库兼容的目的,我们常用的解决方案是,新建一张
T_SEQUENCE
表,它可能包含的字段有:
NAMEvarchar(100),CURRENT_VALnumber(10)
;其中,
NAME
存放序列的名称,而
CURRENT_VAL
存放序列的当前值。假设某一业务对象
Customer
需要新增一笔记录时,为获得不重复且自增长的
CustomerID
,需要将
T_SEQUENCE
表中的与该业务表对应的序列号加
1
并更新,然后将更新后的值作为
Customer
的
ID
,如下述表格所示:
T_SEQUENCE
|
NAME
|
CURRENT_VAL
|
CUSTOMER
|
10
|
T_CUSTOMER
|
ID
|
CUSTOMER_NAME
|
9
|
Kevin
|
10
|
Mary
|
于是,在
Java
语言中,我们以面向对象的方法来实现,可能会是这样(常见写法,未必是最优实现):
publicclassCustomer{
publicvoidsequencePlus(){ Connectionconn=null; Statementstmt=null; try{ conn=getConnection(); stmt=conn.createStatement(); Stringsql="updateT_SEQUENCEsetCURRENT_VAL
=CURRENT_VAL+1" +"whereNAME='CUSTOMER'"; stmt.execute(sql); }catch(Exceptione){ e.printStackTrace(); }finally{ DbUtils.closeQuietly(stmt); DbUtils.closeQuietly(conn); } }
publicintgetSequenceCurrentVal(){ Connectionconn=null; Statementstmt=null; ResultSetrset=null; intid=0; try{ conn=getConnection(); stmt=conn.createStatement(); Stringsql="selectCURRENT_VALfromT_SEQUENCE
whereNAME='CUSTOMER'"; rset=stmt.executeQuery(sql); if(rset.next()){ id=rset.getInt(1); } }catch(Exceptione){ e.printStackTrace(); }finally{ DbUtils.closeQuietly(conn,stmt,rset); } returnid; }
publicvoidaddCustomer(Stringname){ Connectionconn=null; PreparedStatementstmt=null; ResultSetrset=null; try{ sequencePlus(); intid=getSequenceCurrentVal(); conn=getConnection(); stmt=conn.prepareStatement( "insertintoT_CUSTOMER(ID,CUSTOMER_NAME)values(?,?)"); stmt.setInt(1,id); stmt.setString(2,name==null?"":name); stmt.execute(); }catch(Exceptione){ 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
,那么,真正实现这个接口的类是什么呢?
我们可以做一个简单的测试案例,在普通的
JavaApplication
中,调用如下方法:
publicvoidshowConnection(){ Connectionconn=null; try{ Class.forName("oracle.jdbc.driver.OracleDriver"); conn=DriverManager.getConnection(
"jdbc:oracle:thin:@localhost:1521:KEVINORA",
"system","manager"); System.out.println("ConnectionClassis:"+conn.getClass().getName()); }catch(Exceptione){ e.printStackTrace(); }finally{ DbUtils.closeQuietly(conn); } }
|
<
示例代码五
>
|
得到的输出结果是:
ConnectionClassis:
oracle.jdbc.driver.T4CConnection
而在
Apusic
应用服务器中运行如下方法:
publicvoidshowConnection(){ Connectionconn=null; try{ Contextctx=newInitialContext(); ds=(DataSource)ctx.lookup("jdbc/oracle"); conn=ds.getConnection(); System.out.println("ConnectionClassis:"+
conn.getClass().getName()); }catch(Exceptione){ e.printStackTrace(); }finally{ DbUtils.closeQuietly(conn); } }
|
<
示例代码六
>
|
得到的输出结果是:
ConnectionClassis:com.apusic.jdbc.adapter.ConnectionHandle
明明用相同的
JDBCDriver
连接同一个数据库,为什么取得的
Connection
却是不同的类呢?事实上,通过
Apusic
应用服务器获得的数据库连接其实只是一个逻辑连接,真正的物理连接隐藏在该逻辑连接之内,这是一个典型的
Delegate
模式,而恰恰是这个模式,通过
Apusic
应用服务器对数据源进行管理,将给我们的应用开发带来很多好处:
3.2.
当事务结束以后,在该事务上下文中申请的物理连接,都将主动释放
我们以一个最简单的
StatelessSessionBean
为例:
publicclassSimpleBeanimplementsSessionBean{
publicvoidfoo(){ Connectionconn=null; try{ Contextctx=newInitialContext(); DataSourceds=(DataSource)ctx.lookup("jdbc/oracle"); conn=ds.getConnection(); System.out.println("notreleaseconnection"); }catch(Exceptione){ e.printStackTrace(); }finally{ //Notclosetheconnection //DbUtils.closeQuietly(conn); } } }
|
<
示例代码七
>
|
SimpleBean
中的
foo
方法的事务属性设置为
Required
,在该方法中,我们申请了一个数据库连接,但并没有释放它,在运行之前,我们通过
SQLPlus
观察
Oracle
数据库的
Session
,得到的结果是:
SQL> select count(*) from v$session;
COUNT(*)
----------
18
|
<
图一执行方法之前的
OracleSession>
|
而在执行完
SimpleBean
的
foo
方法之后,我们再次观察
Oracle
数据库的
Session
,得到的结果是:
SQL> select count(*) from v$session;
COUNT(*)
----------
18
|
<
图二:执行方法之后的
OracleSession>
|
由此,我们可以得知:即便由于程序的书写错误,没能够释放申请的数据库连接,但
Apusic
应用服务器在事务完成之后,能够把该事务上下文中申请的物理连接主动释放,这对提升应用的容错性带来一定的好处。
3.3.
当jsp/servlet运行结束以后,在jsp/servlet中申请的物理连接,都将主动释放
同事务中申请的数据库连接会主动释放一样,在
jsp/servlet
中申请的数据库物理连接,当
jsp/servlet
运行完毕以后,如果用户没有释放这些连接,
Apusic
应用服务器也将予以主动释放。读者可以尝试自己做一个案例:在
jsp
中申请一个连接,故意不释放,在
jsp
执行完毕以后,可以通过
SQLPlus
或者
Apusic
性能监控工具,查看连接是否已经被应用服务器主动释放。
由上述两节内容我们可以看到,
Apusic
应用服务器能够有效避免
2.1
节中所描述的问题。
3.4.
ConnectionSharing:同一个事务上下文中申请的物理连接可以共享
通过共享连接可以更有效地使用资源及提高性能,并且可以防止连接之间的资源锁定问题。
例如两个
EJB
组件
A
和
B
,它们的事务属性都设置为
Required
。在调用
EJBA
的方法时打开了一个数据库连接,并对数据库中的某个表进行了更新操作,而在关闭连接之前
EJBA
调用了
EJBB
的某个方法,同样
EJBB
打开同一个数据库的连接,也对数据库中同一个表进行了更新操作。倘若没有连接共享机制,这两个连接指向的是两个不同的物理连接,在其上执行的数据库操作将会互相锁定,而这种死锁状态是无法恢复的。现在有了连接共享机制可以有效地解决这个问题。在
EJBA
和
B
中所获得的连接对象实际上都指向同一个物理连接。这一个过程可以简单描述如下:
con1=getConnection(); Transaction.begin performdatabaseoperationoncon1 con2=getConnection(); performdatabaseoperationoncon2 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();
%>
|
<
示例代码九
>
|
在上述代码中,通过
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
机制可以更完美地解决这一问题。
ConnectionSharing
和
Lazy Connection Association Optimization
是同时起作用的,例如,当一个连接被激活时,它将被包含在当前事务场景中,并与同一事务场景中的其他逻辑连接共享同一个物理连接。
我们在
2.3
节中强调:将
Connection
作为成员变量是一种糟糕的设计模式,但同时,我们也看到:哪怕用户旧有系统中存在这样的用法,
Apusic
应用服务器也能够很好的解决由于这种糟糕的设计所带来的缺陷。
4.
总结
本文首先与读者分析了一些错误或者不当的数据库资源使用方法,然后简要介绍了金蝶
Apusic
应用服务器在数据源管理上的一些特性。这些特性,对应用的健壮性及容错性带来一定的好处。但需要再次提醒的是:应用服务器提供的一些增值特性,仅能够当作保障我们应用正常运行的最后一道屏障,我们切不可依赖于这些特性而忽视程序自身的编码质量。一个
J2EE
应用能否正常的运行,程序自身的设计与编码永远是主要因素。
5.
参考资料
注
1
:
FindBugs
:
Sourceforge
上的一个开源工具,能够对源码进行分析从而发现可能出现的编程错误,
http://findbugs.sourceforge.net/
注
2
:
CommonsDbUtils:ApacheJakarta
项目的
Commons
组件,
http://jakarta.apache.org/commons/index.html
注
3
:金蝶
Apusic
应用服务器:国内首家通过
J2EE1.4
认证的应用服务器,请参考
http://www.apusic.com/