在基于JDBC的數(shù)據(jù)庫應(yīng)用開發(fā)中,數(shù)據(jù)庫連接的管理是一個難點(diǎn),因?yàn)樗菦Q定該應(yīng)用性能的一個重要因素。本文在對數(shù)據(jù)庫連接進(jìn)行透徹分析的基礎(chǔ)上,提出并實(shí)現(xiàn)了一個高效的連接管理策略,使得開發(fā)高性能的數(shù)據(jù)庫應(yīng)用變得相對容易。特別是,對于連接管理中的兩個難點(diǎn):事務(wù)和多線程問題進(jìn)行了深入的剖析,并給出了一個基于設(shè)計(jì)模式的解決方案
介紹
在使用Java語言進(jìn)行和數(shù)據(jù)庫有關(guān)的的應(yīng)用開發(fā)中,一般都使用JDBC來進(jìn)行和數(shù)據(jù)庫的交互,其中有一個關(guān)鍵的概念就是Connection(連接),它在Java中是一個類,代表了一個通道。通過它,使用數(shù)據(jù)的應(yīng)用就可以從數(shù)據(jù)庫訪問數(shù)據(jù)了。
對于一個簡單的數(shù)據(jù)庫應(yīng)用,由于對于數(shù)據(jù)庫的訪問不是很頻繁。這時可以簡單地在需要訪問數(shù)據(jù)庫時,就新創(chuàng)建一個連接,用完后就關(guān)閉它,這樣做也不會帶來什么明顯的性能上的開銷。但是對于一個復(fù)雜的數(shù)據(jù)庫應(yīng)用,情況就完全不同了。頻繁的建立、關(guān)閉連接,會極大的減低系統(tǒng)的性能,因?yàn)閷τ谶B接的使用成了系統(tǒng)性能的瓶頸。
本文給出的方法可以有效的解決這個問題。在本方法中提出了一個合理、有效的連接管理策略,避免了對于連接的隨意、無規(guī)則的使用。該策略的核心思想是:連接復(fù)用。通過建立一個數(shù)據(jù)庫連接池以及一套連接使用管理策略,使得一個數(shù)據(jù)庫連接可以得到高效、安全的復(fù)用,避免了數(shù)據(jù)庫連接頻繁建立、關(guān)閉的開銷。另外,由于對JDBC中的原始連接進(jìn)行了封裝,從而方便了數(shù)據(jù)庫應(yīng)用對于連接的使用(特別是對于事務(wù)處理),提高了開發(fā)效率,也正是因?yàn)檫@個封裝層的存在,隔離了應(yīng)用的本身的處理邏輯和具體數(shù)據(jù)庫訪問邏輯,使應(yīng)用本身的復(fù)用成為可能。
問題產(chǎn)生
我參與的項(xiàng)目是開發(fā)一個網(wǎng)管系統(tǒng),不可避免的要和數(shù)據(jù)庫打交道。剛開始時,由于對于數(shù)據(jù)庫的訪問不是很頻繁,對于數(shù)據(jù)庫連接的使用就是簡單的需要時就建立,用完就關(guān)閉的策略,這很符合XP(eXtreme Programming)的口號:"Do the Simplest
Thing that Could Possibly Work"。確實(shí),開始時工作的很好。隨著項(xiàng)目的進(jìn)展,對于數(shù)據(jù)庫的訪問開始變的頻繁,問題就暴露出來了,原先的通過簡單地獲取和關(guān)閉數(shù)據(jù)庫連接的方法將很大的影響系統(tǒng)的性能,這種影響是由于數(shù)據(jù)庫資源管理器進(jìn)程頻繁的創(chuàng)建和摧毀那些連接對象而引起的。
此時,就有必要對數(shù)據(jù)庫訪問方法進(jìn)行重構(gòu)(refactoring),因?yàn)槲覀兇_實(shí)需要進(jìn)行改進(jìn),來提高系統(tǒng)的性能。
解決方案
可以看出,問題的根源就是由于對于連接資源的低效管理造成的。對于共享資源,有一個很著名的設(shè)計(jì)模式:資源池。該模式正是為了解決資源頻繁分配、釋放所造成的問題的。把該模式應(yīng)用到數(shù)據(jù)庫連接管理領(lǐng)域,就是建立一個數(shù)據(jù)庫連接池,提供一套高效的連接分配、使用策略,最終目標(biāo)是實(shí)現(xiàn)連接的高效、安全的復(fù)用。
3.1、建立連接池
第一步,就是要建立一個靜態(tài)的連接池,所謂靜態(tài)是指,池中的連接是在系統(tǒng)初始化時就分配好的,并且不能夠隨意關(guān)閉的。Java中給我們提供很多容器類可以方便的用來構(gòu)建連接池,如:Vector、Stack等。在系統(tǒng)初始化時,根據(jù)配置創(chuàng)建連接并放置在連接池中,以后所使用的連接都是從該連接池中獲取的,這樣就可以避免連接隨意建立、關(guān)閉造成的開銷(當(dāng)然,我們沒有辦法避免Java的Garbage Collection帶來的開銷)。
3.2、分配、釋放策略
有了這個連接池,下面我們就可以提供一套自定義的分配、釋放策略。
當(dāng)客戶請求數(shù)據(jù)庫連接時,首先看連接池中是否有空閑連接,這里的空閑是指,目前沒有分配出去的連接。如果存在空閑連接則把連接分配給客戶,并作相應(yīng)處理,具體處理策略,在關(guān)鍵議題中會詳述,主要的處理策略就是標(biāo)記該連接為已分配。若連接池中沒有空閑連接,就在已經(jīng)分配出去的連接中,尋找一個合適的連接給客戶(選擇策略會在關(guān)鍵議題中詳述),此時該連接在多個客戶間復(fù)用。
當(dāng)客戶釋放數(shù)據(jù)庫連接時,可以根據(jù)該連接是否被復(fù)用,進(jìn)行不同的處理。如果連接沒有使用者,就放入到連接池中,而不是被關(guān)閉。
可以看出正是這套策略保證了數(shù)據(jù)庫連接的有效復(fù)用。
3.3、配置策略
數(shù)據(jù)庫連接池中到底要放置多少個連接,連接耗盡后該如何處理呢?這時一個配置策略。一般的配置策略是,開始時,根據(jù)具體的應(yīng)用需求,給出一個初始的連接池中連接的數(shù)目以及一個連接池可以擴(kuò)張到的最大連接數(shù)目。本方案就是按照這種策略實(shí)現(xiàn)的。
關(guān)鍵議題
本節(jié)將對上述解決方案中的關(guān)鍵細(xì)節(jié)進(jìn)行詳述,正是這些關(guān)鍵的策略保證了數(shù)據(jù)庫連接復(fù)用的高效和安全。
4.1、引用記數(shù)
3.2節(jié)中的分配、釋放策略對于有效復(fù)用連接非常重要,我們采用的方法也是采用了一個很有名的設(shè)計(jì)模式:Reference Counting(引用記數(shù))。該模式在復(fù)用資源方面用的非常廣泛,我們把該方法運(yùn)用到對于連接的分配釋放上。每一個數(shù)據(jù)庫連接,保留一個引用記數(shù),用來記錄該連接的使用者的個數(shù)。具體的實(shí)現(xiàn)上,我們采用了兩極連接池,空閑池和使用池??臻e池中存放目前還沒有分配出去被使用的連接,一旦一個連接被分配出去,那么就會放入到使用池中,并且增加引用記數(shù)。
這樣做有一個很大的好處,使得我們可以高效的使用連接,因?yàn)橐坏┛臻e池中的連接被全部分配出去,我們就可以根據(jù)相應(yīng)的策略從使用池中挑選出一個已經(jīng)正在使用的連接用來復(fù)用,而不是隨意拿出一個連接去復(fù)用。策略可以根據(jù)需要去選擇,我們采用的策略比較簡單:復(fù)用引用記數(shù)最小的連接。Java的面向?qū)ο筇匦?,使得我們可以靈活的選擇不同的策略(提供一個不同策略共用的抽象接口,各個具體的策略都實(shí)現(xiàn)這個接口,這樣對于策略的處理邏輯就和策略的實(shí)現(xiàn)邏輯分離)。
4.2、事務(wù)處理
前面談到的都是關(guān)于使用數(shù)據(jù)庫連接進(jìn)行普通的數(shù)據(jù)庫訪問。對于事務(wù)處理,情況就變得比較復(fù)雜。因?yàn)槭聞?wù)本身要求原子性的保證,此時就要求對于數(shù)據(jù)庫的操作符合"All-All-Nothing"原則,即要么全部完成,要么什么都不做。如果簡單的采用上述的連接復(fù)用的策略,就會發(fā)生問題,因?yàn)闆]有辦法控制屬于同一個事務(wù)的多個數(shù)據(jù)庫操作方法的動作,可能這些數(shù)據(jù)庫操作是在多個連接上進(jìn)行的,并且這些連接可能被其他非事務(wù)方法復(fù)用。
Connection本身具有提供了對于事務(wù)的支持,可以通過設(shè)置Connection的AutoCommit屬性為false,顯式的調(diào)用commit或者rollback方法來實(shí)現(xiàn)。但是要安全、高效的進(jìn)行Connection進(jìn)行復(fù)用,就必須提供相應(yīng)的事務(wù)支持機(jī)制。我們采用的方法是:采用顯式的事務(wù)支撐方法,每一個事務(wù)獨(dú)占一個連接。這種方法可以大大降低對于事務(wù)處理的復(fù)雜性(如果事務(wù)不獨(dú)占一條連接,那么要保證事務(wù)的原子性并且又不妨礙復(fù)用該連接的其他和該事務(wù)無關(guān)的操作,基本上不可能,除非Connection類是你開發(fā)的),并且又不會妨礙連接的復(fù)用,因?yàn)殡`屬于該事務(wù)的所有數(shù)據(jù)庫操作都是通過這一個連接完成的,并且事務(wù)方法又復(fù)用了其他一些數(shù)據(jù)庫方法。
在我們的連接管理服務(wù)提供了顯式的事務(wù)開始、結(jié)束(commit或者rollback)聲明,以及一個事務(wù)注冊表,用于登記事務(wù)發(fā)起者和事務(wù)使用的連接的對應(yīng)關(guān)系,通過該表,使用事務(wù)的部分和我們的連接管理部分就隔離開,因?yàn)樵摫硎窃谶\(yùn)行時根據(jù)實(shí)際的調(diào)用情況,動態(tài)生成的。事務(wù)使用的連接在該事務(wù)運(yùn)行中不能被復(fù)用。
當(dāng)使用者需要使用事務(wù)方法時,首先調(diào)用連接管理服務(wù)提供的beginTrans方法,該方法主要處理流程如下(偽碼描述):
public void beginTrans( ) {
…
conn = getIdleConnectionFromPoll( );
userId = getUserId( );
registerTrans(userId, conn);
…
}
|
在我們的實(shí)現(xiàn)中,用戶標(biāo)識是通過使用者所在的線程來標(biāo)識的。后面的所有對于數(shù)據(jù)庫的訪問都是通過查找該注冊表,使用已經(jīng)分配的連接來完成的。當(dāng)事務(wù)結(jié)束時,從注冊表中刪除相應(yīng)表項(xiàng)。
對于嵌套的事務(wù)如何處理呢?我們采用的方法仍為引用記數(shù),不過這里的引用記數(shù)是指的"嵌套層次",具體的細(xì)節(jié),不再贅述。
4.3、封裝
從上面的論述可以看出,普通的數(shù)據(jù)庫方法和事務(wù)方法對于連接的使用(分配、釋放)是不同的,為了便于使用,對外提供一致的操作接口,我們對連接進(jìn)行了封裝:即普通連接和事務(wù)連接。在此,我們利用了Java中的強(qiáng)大的面向?qū)ο筇匦裕憾鄳B(tài)。普通連接和事務(wù)連接均實(shí)現(xiàn)了一個DbConnection接口,對于接口中定義的方法,分別根據(jù)自己的特點(diǎn)作了不同的實(shí)現(xiàn),這樣在對于連接的處理上就非常的一致了。
4.4、并發(fā)問題
為了是我們的連接管理服務(wù)有更大的通用性,就必須要考慮到多線程環(huán)境,即并發(fā)問題。在一個多線程的環(huán)境下,我們必須要保證連接管理自身數(shù)據(jù)的一致性和連接內(nèi)部數(shù)據(jù)是一致性,還好Java提供對這方面的很好的支持(synchronized關(guān)鍵字),這樣我們就很容易使連接管理成為線程安全的。
結(jié)論
本文給出了一個基本的連接管理框架,在其中使用了一些廣泛使用的設(shè)計(jì)模式(資源池,引用記數(shù)等),使得高效、安全的復(fù)用數(shù)據(jù)庫連接成為可能。當(dāng)然,還有一些問題沒有考慮到,比如:沒有實(shí)現(xiàn)對不同種類的數(shù)據(jù)庫的聯(lián)合管理;沒有提供定時檢測機(jī)制,查詢連接的狀態(tài)等。另外在連接管理的使用包裝上比起一些商用的系統(tǒng)還顯粗糙,但是底層的基理是一致的,所以通過本文相信對于這些商用的產(chǎn)品中的相關(guān)功能會有更好的理解。
參考資料
《Thinking in Java》Bruce Eckel
《Real-Time Design Patterns》 Bruce Powel Dougladd