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/
?