1 前言
在基于J2EE平台的应用开发中,大多数的应用都需要跟数据库打交道;而自从接触JDBC起,我们便不止一次的被告之:数据库资源是十分宝贵的系统资源,一定要谨慎使用。但令人遗憾的是,在笔者见过的大部分跟数据库相关的应用开发中,针对数据库资源的使用总是充斥着这样或者那样的问题。在本文中,笔者针对常见的一些错误或者不当的使用数据库资源的案例进行介绍与分析,并阐述金蝶Apusic应用服务器提供的一些增值特性,通过这些特性能够有效的避免某些错误的发生。
2 常见数据库资源错误/不当用法的案例分析
2.1 未正确的关闭数据库连接
申请了数据库连接,却没有及时的关闭它,这几乎是最常见的数据库连接使用错误。犯这种错误的原因有很多,以下是常见的一种低级错误:
|
|
public void foo() {
|
|
|
Connection conn = getConnection();
|
|
|
Statement stmt = null;
|
|
|
try {
|
|
|
conn = getConnection();
|
|
|
stmt = conn.createStatement();
|
|
|
} catch (Exception e) {
|
|
|
} finally {
|
|
|
close(stmt, conn);
|
|
|
}
|
|
|
}
<示例代码一>
|
在上述案例中的第 行代码中,作者已经申请了一个Connection,但在第 行代码中,又申请了一个新的Connection,并且丢失了第一次申请的connection的引用,至此,当程序每调一次foo方法,将导致申请一个新的Connection而没有释放它,如此一来,当数据库达到能够承受的最大连接数时,将导致整个应用的运行失败。
避免这种错误的方法有很多,譬如,可采用类似于FindBugs(注1)的代码分析工具对应用的源码进行分析,找出可能产生错误的代码。
此外,在应用中,我们需要非常频繁的对申请的数据库连接进行关闭与释放,此时,建议封装成某些工具类使用,并且要尽可能安全的关闭数据库连接。下面,我们以关闭Statement及Connection的通用close方法的不同实现方案来比较:
不安全的关闭方法:
|
|
private void close(Statement stmt, Connection conn) {
|
|
|
try {
|
|
|
stmt.close();
|
|
|
conn.close();
|
|
|
} catch (Exception e) {
|
|
|
}
|
|
|
<示例代码二>
|
在上述代码中,倘若第 行代码中的stmt为空,或者stmt.close()方法出错并抛出异常,都将使第 行代码不能够正常调用,从而导致数据库连接无法释放,那么,更安全的写法应该是:
安全的关闭数据库资源方法:
|
|
private void close(Statement stmt, Connection conn) {
|
|
|
try {
|
|
|
if(stmt!=null) stmt.close();
|
|
|
} catch (Exception e) {}
|
|
|
try {
|
|
|
if(conn!=null) conn.close();
|
|
|
} catch (Exception e) {}
|
|
|
}
|
|
|
<示例代码三>
|
在修订后的代码中,我们可以看到,无论第 行代码中关闭stmt是否成功,程序都能够保证向下执行,从而正确的关闭conn。
这些常用的数据库资源操作公用类,可以使用Apache的Commons DbUtils(注2)组件。
1.1 任意的申请数据库连接
不考虑事务上下文,任意的申请数据库连接资源,也是常见的一种不当用法。但这种问题往往是难以克服的,根源在于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方法分析问题时的“纯洁”性,很容易让人厌倦。
1.2 将Connection作为成员变量
另外一种常见的不当编程模式是将Connection作为类的成员变量。一般来说,针对Connection,我们采取的策略是:用时再申请,用完立即释放。而将Connection |