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