本文從數(shù)據(jù)庫管理員的角度,討論了保護(hù)數(shù)據(jù)庫的重要性。作者建議通過添加Java數(shù)據(jù)庫框架,在開發(fā)人員和數(shù)據(jù)庫之間構(gòu)建一個(gè)經(jīng)過反復(fù)試驗(yàn)的穩(wěn)固的中間層,以降低風(fēng)險(xiǎn),并提供跟蹤及報(bào)告問題的工具。
沒有什么比這樣一只Java開發(fā)小組更影響數(shù)據(jù)庫性能的了:他們有一堆需求,又要使用數(shù)據(jù)庫,但卻僅僅了解了一些Java數(shù)據(jù)庫連接(JDBC)的皮毛。連接會打開并閑置好幾個(gè)小時(shí),一旦連接超時(shí),問題就會扔給數(shù)據(jù)庫管理員(DBA)。未關(guān)閉的語句和占用系統(tǒng)資源的結(jié)果將是數(shù)據(jù)庫管理員頭痛的問題。編程人員使用含糊的JDBC方法和動(dòng)態(tài)SQL導(dǎo)致性能低下。
因此,本文討論了如何使用Java數(shù)據(jù)庫框架幫助數(shù)據(jù)庫遠(yuǎn)離隨意操作的開發(fā)人員。它提供了連接池和管理、跟蹤及報(bào)告JDBC對象、有選擇地刪除性能低下的結(jié)構(gòu)和方法。目的在于防止開發(fā)人員影響性能、避免沾上常見的不良開發(fā)習(xí)慣,并且在無法防止這類活動(dòng)的情況下提供跟蹤機(jī)制。這樣一來,數(shù)據(jù)庫管理員就可以找到問題的根源,在系統(tǒng)進(jìn)入生產(chǎn)環(huán)境之前改正問題。使用Java數(shù)據(jù)庫框架的另一個(gè)目的在于,讓一切開發(fā)工作都保持簡單,那樣開發(fā)人員就可以盡快熟悉情況。
與任何框架一樣,Java數(shù)據(jù)庫框架的目的在于隱藏復(fù)雜性,并為處理復(fù)雜任務(wù)提供一套標(biāo)準(zhǔn)操作程序。同樣重要的一個(gè)方面是讓執(zhí)行任務(wù)的方式具有一致性。這可以改進(jìn)封裝、大大提高代碼的可維護(hù)性。只要設(shè)想一下:假如每個(gè)人都構(gòu)建各自的類和方法來建立數(shù)據(jù)庫連接,勢必會導(dǎo)致混亂、無序的局面。除了數(shù)據(jù)庫外,框架通常適用的一些方面包括:進(jìn)程間通信、多線程管理和圖形用戶界面(GUI)標(biāo)準(zhǔn)。
本文描述的框架旨在供所有中間件開發(fā)人員使用,它在由數(shù)據(jù)庫開發(fā)商提供的實(shí)際的JDBC實(shí)現(xiàn)上添加了一層(如圖1)。
JDBC問題和陷阱
記得下面這一點(diǎn)很重要:JDBC是數(shù)據(jù)庫開發(fā)商提供的實(shí)現(xiàn),但不是所有的實(shí)現(xiàn)都是相同的。但是,在數(shù)據(jù)庫框架讓,開發(fā)人員可以在某種程度上讓它們相同。在個(gè)別情況下,同一家開發(fā)商提供的JDBC驅(qū)動(dòng)程序的各個(gè)版本之間會存在差異。不同開發(fā)商提供的JDBC驅(qū)動(dòng)程序免不了總是會存在差異。差異通常出現(xiàn)在以下幾方面:連接管理;存儲過程和返回ResultSets;處理ResultSets;元數(shù)據(jù)支持;因語句和ResultSets未結(jié)束而消耗資源;連接未關(guān)閉帶來的問題;性能異常及實(shí)現(xiàn)緩慢;數(shù)據(jù)庫優(yōu)化器從一個(gè)版本到下一個(gè)版本所出現(xiàn)的變化;數(shù)據(jù)庫從一個(gè)版本到下一個(gè)版本添加了新特性。
筆者曾有幸參于來自Sybase和Oracle的JDBC實(shí)現(xiàn),它們采用的方法形成了鮮明對比。筆者常開玩笑說,Oracle好比是“父親”,Sybase好比是“母親”。如果你在冬天沒穿衣服就出去,母親會叫你停下來,穿上衣服,免得感冒;而父親會一言不發(fā)地看著,覺得要是天氣寒冷,你會曉得自己添衣服。Sybase驅(qū)動(dòng)程序在連接、語句和結(jié)果集管理方面可以為開發(fā)人員做大量工作;而Oracle驅(qū)動(dòng)程序只會做開發(fā)人員讓它做的那些事情。如果開發(fā)人員沒有結(jié)束語句,它恐怕不會自動(dòng)結(jié)束,也肯定不會把會話、進(jìn)程及打開的游標(biāo)清理干凈。這里不會去研究哪個(gè)方向是正確的,我們只是為框架添加了代碼,讓它們看上去很相似。
圖2顯示了框架示例,旨在處理上面討論的JDBC問題。它還在數(shù)據(jù)庫上提供了抽象層,那樣開發(fā)人員可以更迅速、更安全地訪問數(shù)據(jù)庫。CWDatabase類負(fù)責(zé)管理開發(fā)人員的所有訪問,它利用CWConnectionPool管理連接、利用CWSqlRepository管理SQL字符串。較低級的Connection和Statement類都進(jìn)行了封裝,以便提供跟蹤機(jī)制,并保證連接重新簽入到連接池后,所有語句和結(jié)果集都已關(guān)閉。下文討論了這些類,隨后討論了比較高級的框架特性,用于跟蹤執(zhí)行性能、限制JDBC特性及報(bào)告連接池。下面的所有框架類都以代表筆者所在公司CodeWorks Software的“CW”開頭,這樣它們很容易識別。
重要的類
數(shù)據(jù)庫接口類:CWDatabase和CWParamList
為開發(fā)人員添加用于數(shù)據(jù)庫訪問的一個(gè)簡單類。通過創(chuàng)建框架類的實(shí)例,他們可以獲得運(yùn)行SQL命令及存儲過程的連接及簡單方法。異常處理得到了適當(dāng)?shù)奶幚砑皥?bào)告;通過使用CWParamList允許用戶創(chuàng)建參數(shù)列表,數(shù)據(jù)庫管理員就可以牢牢地控制Java數(shù)據(jù)類型及它們?nèi)绾谓壎ǖ綌?shù)據(jù)庫中的基本數(shù)據(jù)類型。目的在于絕對不允許用戶直接控制jdbc.Connection實(shí)例。牢牢獲得這種控制權(quán)的另一個(gè)好處是,通??梢垣@得很高的數(shù)據(jù)速率,因?yàn)閿?shù)據(jù)庫管理員可以控制數(shù)據(jù)庫訪問。
public class CWDatabase
{
Connection m_conn = null;
public CWDatabase()
{
m_conn = CWConnectionPool.checkOut();
}
public int executeUpdate(String queryName, CWParamList plist)
public int executeUpdate(String queryName)
public ResultSet executeQuery(String queryName, CWParamList plist)
public ResultSet executeQuery(String queryName)
private void processException(String msg, Throwable ex)
}
有了上述這個(gè)類,用戶可以運(yùn)行如下的簡單查詢:
public void updateSensorType()
{
CWDatabase theDB = null;
try
{
// 創(chuàng)建數(shù)據(jù)庫實(shí)例和參數(shù)列表實(shí)例
theDB = new CWDatabase();
CWParamList plist = new CWParamList();
// 添加參數(shù)
plist.addParameter(1,"TYPE1");
plist.addParameter(2,1);
plist.addParameter(3,"ACT");
// 執(zhí)行更新
int numupdate = theDB.executeUpdate("sensor.updateSensorType",plist);
}
catch( Exception exception )
{
CWExceptionReporter.write(this,CWExceptionReporter.FATAL,"Error updating sensor type.");
}
finally
{
theDB.close();
}
return;
} // End方法
連接池:CWConnectionPool
建立連接很費(fèi)資源,所以通過重復(fù)使用連接,就可以避免每次重新建立連接帶來的成本。系統(tǒng)啟動(dòng)時(shí),可以為連接池提供可隨時(shí)使用的幾個(gè)連接。用戶創(chuàng)建數(shù)據(jù)庫接口類的實(shí)例后,連接就會簽出。用戶調(diào)用關(guān)閉命令后,連接重新簽入,供其他用戶使用。
雖然重復(fù)使用連接是連接池的主要目的,但還有許多其他好處。因?yàn)樗羞B接都在一個(gè)地方加以管理及創(chuàng)建,所以數(shù)據(jù)庫管理員就能夠嚴(yán)格管理隔離級別和數(shù)據(jù)庫選項(xiàng)。SQL Anywhere在這方面的例子包括:DELAYED_COMMITS、ISOLATION_LEVEL和 COOPERATIVE_COMMITS,以及面向原始設(shè)備制造商(OEM)版本的軟件的授權(quán)代碼方面的設(shè)置。
不過,筆者在建立連接池時(shí)發(fā)現(xiàn)了一個(gè)問題,它們會留下一些“行李(baggage)”。這樣一來,多次重復(fù)使用會漸漸減慢連接速度。而且,筆者根本查不出這個(gè)問題的根源,不過懷疑它與PreparedStatements、ResultSets或者當(dāng)時(shí)出現(xiàn)的其他某種內(nèi)部跟蹤機(jī)制有關(guān)。鑒于所有連接都由連接池管理,這樣就有可能跟蹤連接存在了多久;重新簽入后,可以“刷新”連接。通過在過了限定時(shí)間后丟棄連接,就可以更好地維持很高的性能比率。
這種嚴(yán)密跟蹤機(jī)制的另一個(gè)好處就是,還可以監(jiān)控誰把連接簽出了、時(shí)間有多久。長時(shí)間保持的連接,尤其是作為成員變量,可能會導(dǎo)致問題。如果連接好幾個(gè)小時(shí)都處于休眠狀態(tài),就會超時(shí),進(jìn)而導(dǎo)致問題。有了這種額外的跟蹤機(jī)制,數(shù)據(jù)庫管理員就可以報(bào)告誰擁有哪個(gè)連接、打開狀態(tài)保持了多久。如果知道簽出問題連接的那一行代碼,就能找到相應(yīng)的開發(fā)人員,告訴他如何使用框架。因?yàn)楦欉B接需要一定開銷,筆者在編寫框架時(shí)在默認(rèn)狀態(tài)下禁用了這項(xiàng)特性,不過測試過程中可以啟用它。
public class CWConnectionPool
{
// 維護(hù)閑置及簽出列表上的連接
private static ArrayList m_freePool = null;
private static HashMap m_outPool = null;
public CWConnectionPool()
{
m_freePool = new ArrayList();
m_outPool = new HashMap();
}
public static void initialize()
public static synchronized Connection checkOut()
public static synchronized void checkIn(Connection conn)
public static void discardConnection(Connection conn)
private static CWConnection createConnection()
public static reportConnectionPool()
}
SQL存儲庫:CWSqlRepository
SQL語句最好保存在不同文件中,那樣不必重新編譯代碼就可以修改語句。譬如說,如果發(fā)現(xiàn)了某個(gè)性能問題,經(jīng)過分析,發(fā)現(xiàn)是數(shù)據(jù)庫優(yōu)化器選錯(cuò)了索引,這時(shí)就很容易添加SQL提示。為了管理SQL語句,筆者使用了CWSQLRepository類。該存儲庫還允許重復(fù)使用代碼;又因?yàn)樗蠸QL語句都在同一個(gè)地方,數(shù)據(jù)庫管理員就更容易找到可能受模式改變影響的所有語句。
public class CWSqlRepository
{
private static CWSqlRepository m_SQLRepository;
private static Properties m_SQLrepositoryTable;
public static void initialize()
{
if (m_SQLRepository == null)
m_SQLRepository = new CWSqlRepository();
return;
}
private static void loadSQLrepository(String sqlfile)
public static String getSQLString(String tag)
}
封裝JDBC類
說到簡化數(shù)據(jù)庫訪問,通過提供上面討論的那幾個(gè)簡單類,就能得到很大成效。不過說到消除JDBC實(shí)現(xiàn)在較低層面上的差異,從事重復(fù)工作毫無意義。只要封裝JDBC類,就可以處理問題、添加功能。JDBC文檔齊全,開發(fā)人員很熟悉它,數(shù)據(jù)庫管理員也是一樣。另外,很容易教人學(xué)會,并提供合理使用的示例。通過創(chuàng)建封裝器類,數(shù)據(jù)庫管理員可以添加自己需要的任何跟蹤、定時(shí)及報(bào)告機(jī)制,還可以消除差異。只有在極少數(shù)情況下,開發(fā)人員才真正知道自己在使用Connection.prepareStatement()的框架實(shí)現(xiàn),而不是實(shí)際的實(shí)現(xiàn)。
CWConnection
要封裝的最重要的一個(gè)類是java.sql.Connection。數(shù)據(jù)庫管理員可以在這里跟蹤某連接簽出了多久,并維護(hù)所有已創(chuàng)建語句的列表。為了調(diào)試,數(shù)據(jù)庫管理員可以維護(hù)堆棧跟蹤信息(stack trace)。這樣在建立連接后,一旦發(fā)現(xiàn)“連接濫用”,就能更準(zhǔn)確地找到建立該連接的代碼,譬如說,連接簽出時(shí)間超過規(guī)定。
public class CWConnection implements Connection
{
// 連接的基本信息
private Connection _conn = null;
//封裝的java.sql.Connection
private int _connNum ;
// 跟蹤號碼
private long _createTime;
// 設(shè)定時(shí)間
private ArrayList _stmtTracker;
// 跟蹤語句
public int getConnectionNum()
public int getElapseTime()
public void closeStatements()
private ArrayList getStatementTracker()
private void clearStatementTracker()
String reportConnnection()
// 封裝的JDBC方法
public Statement createStatement() throws SQLException
{
CWStatement stmt = new CWStatement(_conn.createStatement());
_stmtTracker.add(stmt);
return stmt;
}
}
CWStatement、CWPreparedStatement和CWCallableStatement
數(shù)據(jù)庫管理員可以編寫這樣的框架:很少允許開發(fā)人員可以控制Statements、CallableStatements和PreparedStatements。封裝這些類的主要原因是可以跟蹤時(shí)間設(shè)定,如果返回結(jié)果集的話,還可以跟蹤ResultSet實(shí)例。雖然在下面討論了串行化的結(jié)果集,但筆者并不建議把結(jié)果集隱藏起來,不讓開發(fā)人員看到,因?yàn)榻涌诜矫娴奈臋n很齊全。主要的濫用現(xiàn)象就是讓結(jié)果集打開著,不過對此進(jìn)行跟蹤卻相當(dāng)簡單。連接簽入后,所有語句都被關(guān)閉,每個(gè)語句保證結(jié)果集被關(guān)閉。筆者仍封裝了ResultSet,但主要目的是消除性能低下的方法,那樣開發(fā)人員就沒法用它們。稍后會討論這個(gè)話題。
public class CWStatement implements Statement
{
// 語句的基本信息
private Statement m_stmt = null;
//封裝的java.sql.Statement
private int m_stmtNum;
// 跟蹤號碼
private long m_createTime;
// 設(shè)定時(shí)間
protected ResultSet m_rsTracker;
// 跟蹤結(jié)果集
private String m_sqlTracker;
// 隨該語句一起發(fā)出的Sql
public String reportStatement()
public CWStatement( Statement stmt, int stmtNum)
{
m_stmt = stmt;
m_stmtNum = stmtNum;
m_createTime = System.currentTimeMillis();
m_sqlTracker = null;
m_rsTracker = null;
}
public ResultSet executeQuery(String sql) throws SQLException
{
CWResultSet rs = new CWResultSet(m_stmt.executeQuery(sql));
m_rsTracker = rs;
m_sqlTracker = sql;
return rs;
}
}
框架的先進(jìn)思想
上面討論的話題集中于簡化數(shù)據(jù)庫接口、連接管理,并提供防范常見JDBC問題的方法。接下來會介紹框架的附加部分,它們?yōu)楸O(jiān)控及控制使用數(shù)據(jù)庫的開發(fā)人員提供了更有效的機(jī)制,包括:解決結(jié)果集的問題、限制性能低下的操作、監(jiān)控SQL性能。
CWResultSetSerialized
許多Java編程人員沒有認(rèn)識到(或者忘了)ResultSets實(shí)際上是數(shù)據(jù)庫游標(biāo)。它們傳遞引用、把它們存儲為成員變量,往往從不關(guān)閉,因而占用了數(shù)據(jù)庫資源。為了消除所有風(fēng)險(xiǎn),可利用JDBC結(jié)果集來創(chuàng)建串行化的結(jié)果集。這還可以讓結(jié)果集通過遠(yuǎn)程方法調(diào)用(RMI)在進(jìn)程之間發(fā)送。
在極端情況下,引起阻塞問題的結(jié)果集頻頻傳送,以至筆者查不到該在什么地方關(guān)閉它。一旦用串行化的結(jié)果集取而代之,就能關(guān)閉實(shí)際的ResultSet,所有問題都立馬消失了。串行化結(jié)果集的任何實(shí)現(xiàn)都需要限制可以創(chuàng)建的行數(shù),因?yàn)?00萬行的串行化結(jié)果集會引起性能問題。筆者開始限制在5000行。
下面的代碼表明了結(jié)果集管理不善,因?yàn)樗鼈鞯搅朔椒ㄍ饷妗,F(xiàn)在很難知道它是不是被關(guān)閉了,因?yàn)榉椒ㄖ挥谐霈F(xiàn)了錯(cuò)誤才關(guān)閉數(shù)據(jù)庫實(shí)例。代碼可以使用,不過,要是結(jié)果集在調(diào)用方法里面沒有關(guān)閉,連接會處于簽出狀態(tài),游標(biāo)仍然是打開的。
public ResultSet getAllSensors()
{
CWDatabase theDB = null;
ResultSet rs = null;
try
{
// 創(chuàng)建數(shù)據(jù)庫實(shí)例和參數(shù)列表實(shí)例
theDB = new CWDatabase();
// 執(zhí)行查詢
rs = theDB.executeQuery("sensor.getAllSensors");
}
catch( Exception exception )
{
CWExceptionReporter.write(this,CWExceptionReporter.FATAL,"Error during query: " + " sensor.getAllSensors ");
theDB.close();
}
return rs;
} // End方法
因?yàn)橛袝r(shí)在開發(fā)周期很晚時(shí)才發(fā)現(xiàn)這類問題,因而無法通過改寫方法來解決,可以通過以下辦法解決問題:傳回串行化的結(jié)果集,通過finally塊關(guān)閉數(shù)據(jù)庫實(shí)例,從而保證一切都正常關(guān)閉。筆者仍認(rèn)為,數(shù)據(jù)庫管理員應(yīng)當(dāng)給引起問題的工程師出難題,不過系統(tǒng)代碼凍結(jié)前一天不是改變大量代碼的時(shí)候。所作的變化用下面的黑體字表明:
public ResultSet getAllSensors()
{
CWDatabase theDB = null;
CWResultSetSerialized rss = null;
try
{
// 創(chuàng)建數(shù)據(jù)庫實(shí)例和參數(shù)列表實(shí)例
theDB = new CWDatabase();
// 執(zhí)行更新
ResultSet rs = theDB.executeQuery("sensor.getAllSensors");
rss = new ResultSetSerialized(rs);
}
catch( Exception exception )
{
CWExceptionReporter.write(this,CWExceptionReporter.FATAL,"Error during query: " + " sensor.getAllSensors ");
}
finally
{
theDB.close();
}
// 返回串行化結(jié)果集
return rss;
} // End方法
性能級別
因性能需求不同,數(shù)據(jù)庫管理員的要求可能大不相同,有的是“只允許速度最快的數(shù)據(jù)庫訪問”,有的是“我不在乎訪問速度,只要可以使用任何特性”。大部分人介于兩者之間。 能夠“關(guān)閉”已知性能低下的JDBC方法大有幫助。譬如說,使用ResultSet.update()比使用不同的Statement.execute()來執(zhí)行同樣的更新慢得多。允許用戶使用rs.last()等方法返回不是“只能向前移動(dòng)的”結(jié)果集也很慢。筆者使用三個(gè)基本的性能級別:
● 級別1:開發(fā)人員不可以訪問連接,也無法發(fā)出動(dòng)態(tài)SQL。所有性能低下的方法都被關(guān)閉,包括結(jié)果集更新和元數(shù)據(jù)訪問。
● 級別2:允許動(dòng)態(tài)查詢,但其他所有性能低下的方法仍然受到限制。筆者的架構(gòu)就使用這種默認(rèn)值。
● 級別3:全面的JDBC訪問,沒有任何限制。
關(guān)閉JDBC特性后,筆者建議發(fā)出異常,這可以解釋特性已被關(guān)閉,需要聯(lián)系數(shù)據(jù)庫管理員。在開發(fā)期間,數(shù)據(jù)庫管理員可以決定是否真正需要該特性,并確定要不要重新添加到框架上,或者更改該特性的性能級別。
public class CWResultSet implements ResultSet
{
private ResultSet m_rs;
private long m_createTime;
private int m_perf_level;
public CWResultSet( ResultSet rs, perf_level)
{
m_rs = rs;
m_createTime = System.currentTimeMillis();
m_perf_level = perf_level;
}
public void updateRow() throws SQLException
{
if(perf_level < CWDatabase.HIGH_PERF_ONLY)
{
CWExceptionReporter.write(this,CWExceptionReporter.ERROR, "Use of method prohibited due to slow performance, “+ " as per the DBA.");
}
m_rs.updateRow();
}
}
為數(shù)據(jù)庫管理員想要開啟或者禁用的每一部分JDBC功能賦予名字,并且利用屬性文件控制這些值相當(dāng)簡單。不過,筆者發(fā)現(xiàn)自己不需要這種靈活性,于是仍采用了基本級別。
SQL性能跟蹤
雖然SQL Anywhere提供了監(jiān)控查詢的功能,但筆者還是提供了語句定時(shí)和報(bào)告機(jī)制。這樣數(shù)據(jù)庫管理員可以對某個(gè)SQL語句及所有語句開啟跟蹤機(jī)制,或者設(shè)定時(shí)間閾值,報(bào)告超過給定閾值的查詢。筆者還考慮了在結(jié)果集層面的定時(shí),那樣就可以確認(rèn)超過閾值的結(jié)果集。這會有所幫助,因?yàn)檎Z句定時(shí)執(zhí)行只提供返回首行的所用時(shí)間,除非語句里面有“按××排序”或者類似子句。跟連接跟蹤的情況很相似,筆者在默認(rèn)情況下關(guān)閉了這項(xiàng)功能,在測試階段加以利用。
public int executeUpdate(String queryName)
{
private long startTime;
< snip >
// 執(zhí)行更新
startQueryTimer();
int numupdate =theDB.executeUpdate queryName,plist);
stopQueryTimer(queryName);
}
如果查詢跟蹤機(jī)制開啟,查詢時(shí)間就會記錄到數(shù)據(jù)庫里面。
(沈建苗 編譯)
(計(jì)算機(jī)世界報(bào) 2006年10月16日 第40期 B31、B32)