在使用Java語言進行和數據庫有關的的應用開發中,通常都使用JDBC來進行和數據庫的交互,其中有一個關鍵的概念就是Connection(鏈接),它在Java中是一個類,表明了一個通道。經過它,使用數據的應用就能夠從數據庫訪問數據了。 html
對於一個簡單的數據庫應用,因爲對於數據庫的訪問不是很頻繁。這時能夠簡單地在須要訪問數據庫時,就新建立一個鏈接,用完後就關閉它,這樣作也不會帶來什 麼明顯的性能上的開銷。可是對於一個複雜的數據庫應用,狀況就徹底不一樣了。頻繁的創建、關閉鏈接,會極大的減低系統的性能,由於對於鏈接的使用成了系統性 能的瓶頸。 java
本文給出的方法能夠有效的解決這個問題。在本方法中提出了一個合理、有效的鏈接管理策略,避免了對於鏈接的隨意、無規則的使用。該策略的核心思想是:鏈接 複用。經過創建一個數據庫鏈接池以及一套鏈接使用管理策略,使得一個數據庫鏈接能夠獲得高效、安全的複用,避免了數據庫鏈接頻繁創建、關閉的開銷。另外, 因爲對JDBC中的原始鏈接進行了封裝,從而方便了數據庫應用對於鏈接的使用(特別是對於事務處理),提升了開發效率,也正是由於這個封裝層的存在,隔離 了應用的自己的處理邏輯和具體數據庫訪問邏輯,使應用自己的複用成爲可能。 數據庫
回頁首 設計模式
我參與的項目是開發一個網管系統,不可避免的要和數據庫打交道。剛開始時,因爲對於數據庫的訪問不是很頻繁,對於數據庫鏈接的使用就是簡單的須要時就創建,用完就關閉的策略,這很符合XP(eXtreme Programming)的口號:"Do the Simplest Thing that Could Possibly Work"。確實,開始時工做的很好。隨着項目的進展,對於數據庫的訪問開始變的頻繁,問題就暴露出來了,原先的經過簡單地獲取和關閉數據庫鏈接的方法將很大的影響系統的性能,這種影響是因爲數據庫資源管理器進程頻繁的建立和摧毀那些鏈接對象而引發的。 安全
此時,就有必要對數據庫訪問方法進行重構(refactoring),由於咱們確實須要進行改進,來提升系統的性能。 多線程
回頁首 併發
能夠看出,問題的根源就是因爲對於鏈接資源的低效管理形成的。對於共享資源,有一個很著名的設計模式:資源池。該模式正是爲了解決資源頻繁分配、釋放所造 成的問題的。把該模式應用到數據庫鏈接管理領域,就是創建一個數據庫鏈接池,提供一套高效的鏈接分配、使用策略,最終目標是實現鏈接的高效、安全的複用。 框架
第一步,就是要創建一個靜態的鏈接池,所謂靜態是指,池中的鏈接是在系統初始化時就分配好的,而且不可以隨意關閉的。Java中給咱們提供不少容器類能夠 方便的用來構建鏈接池,如:Vector、Stack等。在系統初始化時,根據配置建立鏈接並放置在鏈接池中,之後所使用的鏈接都是從該鏈接池中獲取的, 這樣就能夠避免鏈接隨意創建、關閉形成的開銷(固然,咱們沒有辦法避免Java的Garbage Collection帶來的開銷)。 性能
有了這個鏈接池,下面咱們就能夠提供一套自定義的分配、釋放策略。 spa
當客戶請求數據庫鏈接時,首先看鏈接池中是否有空閒鏈接,這裏的空閒是指,目前沒有分配出去的鏈接。若是存在空閒鏈接則把鏈接分配給客戶,並做相應處理, 具體處理策略,在關鍵議題中會詳述,主要的處理策略就是標記該鏈接爲已分配。若鏈接池中沒有空閒鏈接,就在已經分配出去的鏈接中,尋找一個合適的鏈接給客 戶(選擇策略會在關鍵議題中詳述),此時該鏈接在多個客戶間複用。
當客戶釋放數據庫鏈接時,能夠根據該鏈接是否被複用,進行不一樣的處理。若是鏈接沒有使用者,就放入到鏈接池中,而不是被關閉。
能夠看出正是這套策略保證了數據庫鏈接的有效複用。
數據庫鏈接池中到底要放置多少個鏈接,鏈接耗盡後該如何處理呢?這時一個配置策略。通常的配置策略是,開始時,根據具體的應用需求,給出一個初始的鏈接池中鏈接的數目以及一個鏈接池能夠擴張到的最大鏈接數目。本方案就是按照這種策略實現的。
本節將對上述解決方案中的關鍵細節進行詳述,正是這些關鍵的策略保證了數據庫鏈接複用的高效和安全。
3.2節中的分配、釋放策略對於有效複用鏈接很是重要,咱們採用的方法也是採用了一個頗有名的設計模式:Reference Counting(引用記數)。該模式在複用資源方面用的很是普遍,咱們把該方法運用到對於鏈接的分配釋放上。每個數據庫鏈接,保留一個引用記數,用來 記錄該鏈接的使用者的個數。具體的實現上,咱們採用了兩極鏈接池,空閒池和使用池。空閒池中存放目前尚未分配出去被使用的鏈接,一旦一個鏈接被分配出 去,那麼就會放入到使用池中,而且增長引用記數。
這樣作有一個很大的好處,使得咱們能夠高效的使用鏈接,由於一旦空閒池中的鏈接被所有分配出去,咱們就能夠根據相應的策略從使用池中挑選出一個已經正在使 用的鏈接用來複用,而不是隨意拿出一個鏈接去複用。策略能夠根據須要去選擇,咱們採用的策略比較簡單:複用引用記數最小的鏈接。Java的面向對象特性, 使得咱們能夠靈活的選擇不一樣的策略(提供一個不一樣策略共用的抽象接口,各個具體的策略都實現這個接口,這樣對於策略的處理邏輯就和策略的實現邏輯分離)。
前面談到的都是關於使用數據庫鏈接進行普通的數據庫訪問。對於事務處理,狀況就變得比較複雜。由於事務自己要求原子性的保證,此時就要求對於數據庫的操做 符合"All-All-Nothing"原則,即要麼所有完成,要麼什麼都不作。若是簡單的採用上述的鏈接複用的策略,就會發生問題,由於沒有辦法控制屬 於同一個事務的多個數據庫操做方法的動做,可能這些數據庫操做是在多個鏈接上進行的,而且這些鏈接可能被其餘非事務方法複用。
Connection自己具備提供了對於事務的支持,能夠經過設置Connection的AutoCommit屬性爲false,顯式的調用commit 或者rollback方法來實現。可是要安全、高效的進行Connection進行復用,就必須提供相應的事務支持機制。咱們採用的方法是:採用顯式的事 務支撐方法,每個事務獨佔一個鏈接。這種方法能夠大大下降對於事務處理的複雜性(若是事務不獨佔一條鏈接,那麼要保證事務的原子性而且又不妨礙複用該連 接的其餘和該事務無關的操做,基本上不可能,除非Connection類是你開發的),而且又不會妨礙鏈接的複用,由於隸屬於該事務的全部數據庫操做都是 經過這一個鏈接完成的,而且事務方法又複用了其餘一些數據庫方法。
在咱們的鏈接管理服務提供了顯式的事務開始、結束(commit或者rollback)聲明,以及一個事務註冊表,用於登記事務發起者和事務使用的鏈接的 對應關係,經過該表,使用事務的部分和咱們的鏈接管理部分就隔離開,由於該表是在運行時根據實際的調用狀況,動態生成的。事務使用的鏈接在該事務運行中不 能被複用。
當使用者須要使用事務方法時,首先調用鏈接管理服務提供的beginTrans方法,該方法主要處理流程以下(僞碼描述):
public void beginTrans( ) { … conn = getIdleConnectionFromPoll( ); userId = getUserId( ); registerTrans(userId, conn); … }
在咱們的實現中,用戶標識是經過使用者所在的線程來標識的。後面的全部對於數據庫的訪問都是經過查找該註冊表,使用已經分配的鏈接來完成的。當事務結束時,從註冊表中刪除相應表項。
對於嵌套的事務如何處理呢?咱們採用的方法仍爲引用記數,不過這裏的引用記數是指的"嵌套層次",具體的細節,再也不贅述。
從上面的論述能夠看出,普通的數據庫方法和事務方法對於鏈接的使用(分配、釋放)是不一樣的,爲了便於使用,對外提供一致的操做接口,咱們對鏈接進行了封 裝:即普通鏈接和事務鏈接。在此,咱們利用了Java中的強大的面向對象特性:多態。普通鏈接和事務鏈接均實現了一個DbConnection接口,對於 接口中定義的方法,分別根據本身的特色做了不一樣的實現,這樣在對於鏈接的處理上就很是的一致了。
爲了是咱們的鏈接管理服務有更大的通用性,就必需要考慮到多線程環境,即併發問題。在一個多線程的環境下,咱們必需要保證鏈接管理自身數據的一致性和鏈接 內部數據是一致性,還好Java提供對這方面的很好的支持(synchronized關鍵字),這樣咱們就很容易使鏈接管理成爲線程安全的。
本文給出了一個基本的鏈接管理框架,在其中使用了一些普遍使用的設計模式(資源池,引用記數等),使得高效、安全的複用數據庫鏈接成爲可能。固然,還有一 些問題沒有考慮到,好比:沒有實現對不一樣種類的數據庫的聯合管理;沒有提供定時檢測機制,查詢鏈接的狀態等。另外在鏈接管理的使用包裝上比起一些商用的系 統還顯粗糙,可是底層的基理是一致的,因此經過本文相信對於這些商用的產品中的相關功能會有更好的理解。