轉載自 微信公衆號【碼農翻身】php
網絡訪問html
隨着 Oracle, Sybase, SQL Server ,DB2, Mysql 等人陸陸續續住進數據庫村, 這裏呈現出一片興旺發達的景象, 無數的程序在村裏忙忙碌碌, 讀寫數據庫, 實際上一個村落已經容不下這麼多人了, 數據庫村變成了數據鎮。前端
這一天, 數據庫鎮發生了一件大事: 它連上了網絡!java
外部的花花世界一下所有打開, 不少程序開始離開這個擁擠的城鎮, 住到更加宜居的地方去。mysql
但是他們的工做仍是要讀寫數據庫, 你們都在想辦法能不能經過網絡來訪問數據庫鎮的數據庫。linux
其中移居到Tomcat村的Java 最爲活躍, 這小子先去拜訪了一下Mysql , 相對於Oracle, Sybase 等大佬, Mysql 還很弱小, 也許容易搞定。git
Java 說: 「Mysql 先生, 如今已經網絡時代了, 您也得與時俱進啊, 給咱們開放下網絡接口吧。 」程序員
Mysql 說: 「還網絡時代, 大家這些傢伙愈來愈懶了, 都不肯意到咱們家裏來了! 說說吧, 你想怎麼開放? 」github
Java 說: 「很簡單, 您據說過TCP/IP還有Socket 沒有? 沒有嗎?! 不要緊, 您的操做系統確定知道, 它內置實現了TCP/IP和socket, 您只須要和他商量一下, 須要申請一個ip , 肯定一個端口, 而後您在這個端口監聽, 我每次想訪問數據了, 就會建立一個socket ,向你發起鏈接請求, 你接受了就好了。 」面試
(劉欣注: 參見《張大胖的socket》)
「這麼麻煩啊?」
「其實也簡單, 您的操做系統會幫忙的, 他很是熟悉, 再說只須要作一次就行, 把這個網絡訪問創建起來, 到時候不少程序都會來訪問您, 您會發財的。 」
「不會這麼簡單吧, 假設說, 我是說假設啊, 經過socket咱們創建了鏈接, 經過這個鏈接, 你給我發送什麼東西? 我又給你發什麼東西?」 Mysql很是老練, 直擊命門。
「呃, 這個.... 」
Java 其實內心其實很是明白, 這須要和Mysql定義一個應用層的協議, 就是所謂的你發什麼請求, 我給你什麼響應。
例如:
客戶端程序先給Mysql 打個招呼, Mysql也迴應一下, 俗稱握手。
怎麼作認證、受權, 數據加密, 數據包分組。
用什麼格式發送查詢語句, 用什麼格式來發送結果。
若是結果集很大, 要一會兒全發過來嗎?
怎麼作數據緩衝?
......
等等一系列讓人頭痛的問題。
原本Java是想獨自定義, 這樣本身也許能佔點便宜, 沒想到Mysql 直接提出來了。
「這樣吧 」 Java 說 「咱們先把這個應用層的協議定義下來, 而後您去找操做系統來搞定socket如何? 」
「這還差很少 」 。 Mysql 贊成了。
兩人忙活了一星期, 才把這個應用層協議給定義好。
而後又忙了一星期, 才把Mysql 這裏的socket搞定。
Java 趕忙回到Tomcat村, 作了一個實驗: 經過socket和mysql 創建鏈接, 而後經過socket 發送約定好的應用層協議, 還真不錯, 一次都調通了, 看來準備工做很重要啊。
(劉欣注: 這是個人杜撰, mysql 的網絡訪問早就有了, 並非java 捷足先登搞出來的)
統一接口
搞定了Mysql , Java 很得意, 這是一個很好的起點, 之後和Oracle, SQL Server, Db2等大佬談判也有底氣了。
尤爲是和mysql 商量出的應用層協議, mysql 也大度的公開了, 這樣一來, 無論是什麼語言寫的程序,管你是java, pyhton, ruby , php...... 只要能使用socket, 就能夠遵守mysql 的應用層協議進行訪問, mysql 的顧客呈指數級增加, 財源滾滾。 尤爲是一個叫PHP的傢伙, 簡直和mysql 成了死黨。
Oracle, Db2那幫大佬一看, 馬上就紅了眼, 不到Java 去談判, 也火燒眉毛的定義了一套屬於本身的應用層訪問協議。
使人抓狂的是, 他們的網絡訪問協議和Msyql 的徹底不同 ! 這就意味着以前寫的針對mysql 的程序沒法針對Oracle , Db2通用, 若是想切換數據庫, 每一個程序都得另起爐竈寫一套代碼!
更讓人噁心的是, 每套代碼都得處理很是多的協議細節, 每一個使用Java進行數據庫訪問的程序都在喋喋不休的抱怨: 我就想經過網絡給數據庫發送SQL語句, 怎麼搞的這麼麻煩?
緣由很簡單, 就是直接使用socket編程, 太low 了 , 必須得有一個抽象層屏蔽這些細節!
Java 開始苦苦思索, 作出一個好的抽象不是那麼容易的。
首先得有一個叫鏈接(Connection)的東西, 用來表明和數據庫的鏈接。
想執行SQL怎麼辦? 用一個Statement來 表示吧。 SQL返回的結果也得有個抽象的概念: ResultSet 。
他們之間的關係如圖所示:
從Connection 能夠建立Statement, Statement 執行查詢能夠獲得ResultSet。
ResultSet提供了對數據進行遍歷的方法, 就是rs.next() , rs.getXXXX .... 完美!
對了, 不管是Connection, 仍是Statement, ResultSet , 他們都應該是接口,而不能是具體實現。
具體的實現須要由各個數據庫或者第三方來提供, 毫無疑問, 具體的實現代碼中就須要處理那些煩人的細節了!
Java 把這個東西叫作JDBC, 想着本身定義了一個標準接口, 把包袱都甩給你別人, 他很是得意。
面向接口編程
第一個使用JDBC, 叫作學生信息管理的程序很快發現了問題, 跑來質問Java: 「你這個Connection 接口設計的有問題!」
Java 說: 「不可能, 個人設計多完善啊!」
「看來你這個規範的制定者沒有真正使用啊, 你看看, 我想鏈接Mysql, 把Mysql 提供的jdbc實現(mysql-connector-java-4.1.jar )拿了過來, 創建一個Connection : 」
「這不是挺正常的嗎? 你要鏈接Mysql , 確定要提供ip地址, 端口號,數據庫名啊」 Java 問到。
「問題不在這裏, 昨天我遇到mysql了, 他給了我一個號稱性能更強勁的升級版mysql-connector-java-5.0.jar, 我升級之後, 發現個人代碼編譯都通不過了, 原來mysql 把MysqlConnectionImpl 這個類名改爲了 MysqlConnectionJDBC4Impl , 你看看, 你成天吹噓着要面向接口編程, 不要面向實現編程, 可是你本身設計的東西都作不到啊」
Java以爲背上開始出汗, 那個程序說的沒錯, 設計上出了漏洞, 趕忙彌補吧。
既然不能直接去 new 一個Connection 的實現, 確定要經過一個新的抽象層來作, 這個中間層叫作什麼?
Java 想到了電腦上的驅動程序, 不少硬件無法直接使用, 除非安裝了驅動。 那我也模擬一下再作一個抽象層吧: Driver。
每一個數據庫都須要實現Driver 接口, 經過Driver 能夠得到數據庫鏈接Connection , 可是這個Driver 怎麼才能new 出來呢? 確定不能直接new , Java彷佛陷入了雞生蛋、蛋生雞的無限循環中了。
最後, 仍是Java的反射機制救了他, 不要直接new 了, 每一個數據庫的Driver 都用一個文本字符串來表示, 運行時動態的去裝載, 例如mysql 的Driver 是這樣的:
Oracle是這樣的:
只要這個Driver Class不改動, 其餘具體的實現像Connection, Statement, ResultSet想怎麼改就怎麼改。
接下來的問題是同一個程序可能訪問不一樣的數據庫, 可能有多個不一樣Driver 都被動態裝載進來, 如何來管理?
那就搞一個DriverManager吧, Mysql 的Driver, Oracle的Driver 在類初始化的時候, 必定得註冊到DriverManager中來, 這樣DriverManager才能管理啊:
注意: 關鍵點是static 的代碼塊, 在一個類被裝載後就會執行。
DriverManager 能夠提供一個getConnection的方法, 用於創建數據庫Connection 。
DriverManager會把這些信息傳遞給每一個Driver , 讓每一個Driver去建立Connection 。
慢着! 若是DriverManager 裏既有MysqlDriver, 又有OracleDriver , 這裏到底該鏈接哪個數據庫呢? 難道讓兩個Driver 都嘗試一下? 那樣太費勁了吧, 還得區分開,無法子只好讓那些程序在數據庫鏈接url中來指定吧:
url中指明瞭這是一個什麼數據庫, 每一個Driver 都須要判斷下是否是屬於本身支持的類型, 是的話就開始鏈接, 不是的話直接放棄。
(劉欣注: 每一個Driver接口的實現類都須要實現一個acceptsURL(Sting url)方法, 判斷這個url是否是本身能支持的。 )
唉,真是不容易啊, Java想, 這下整個體系就完備了吧, 爲了得到一個Connection , 綜合起來其實就這麼幾行代碼:
不管是任何數據庫, 只要正確實現了Driver, Connection 等接口, 就能夠輕鬆的歸入到JDBC框架下了。
Java終於能夠高興的宣佈: 「JDBC正式誕生了!」
(完)
鏈接, 鏈接, 老是鏈接!
生活中確定有比數據庫鏈接更有趣的事情。
1
數據庫鏈接
又到了數據庫鏈接的時間!
那些碼農把數據庫參數送過來, Oracle , Db2, Sybase, SQL Server這些JDBC Driver 懶洋洋起來去幹活賺錢。
小東也是其中之一, 天天的工做就是鏈接Mysql 數據庫, 發出SQL查詢, 獲取結果集。
工做穩定, 收入不菲, 只是日復一日,年復一年, 枯燥的工做實在是太使人厭煩了。
有時候小東會和其餘JDBC Driver 聊天, 談起有些不着調的碼農, 建立一個Connection, 發出一個查詢, 處理完ResultSet後 , 馬上就把Connection給關掉了。
「他們簡直不知道咱們創建一個數據鏈接有多麼辛苦, 先經過Socket 創建TCP鏈接, 而後還要有應用層的握手協議, 唉, 不知道要幹多少髒活累活, 這幫碼農用完就關, 真是浪費啊。 」
「還有更可氣的, 有些傢伙使用了PreparedStatement , 我通知數據庫作了預編譯, 制定了查詢計劃, 爲此我還花費了不菲的小費。 可是隻執行了一次查詢, Connection就關掉了, PreparedStatement 也就不可用了, 如今數據庫都懶的給我作預編譯了 !」
「大家這都是好的, 有些極品根本就不關閉Connection, 最後讓這個Connection 進入了不可控狀態。 」
「咱們啊, 都把寶貴的生命都獻給了數據庫鏈接事業...... 」
抱怨歸抱怨, 大部分人都安於現狀,逆來順受了。
2
向Tomcat取經
可是不安分的小東決心改變, 他四處拜訪取經, 可是一無所得。
這一天在Tomcat村遇到了Tomcat 村長, 看到了村長處理Http請求的方式, 忽然間看到了曙光。
村長說: 咱們原本是一個線程處理一個Http請求 , 一個Http請求來到咱們這裏之後, 我並不會新建一個線程來處理, 而是從一個小黑屋叫來一個線程直接幹活, 幹完活之後再回到小黑屋待着。
小東問: 小黑屋是什麼?
(碼農翻身注: 參見文章《我是一個線程》)
村長說: 「學名是線程池, 爲了充分利用資源, 我在啓動時就創建了一批線程, 放到線程池裏, 須要線程時直接去取就能夠了。 」
「那要是線程池裏的線程都被派出去了怎麼辦 ? 」
"要麼新建立線程, 要麼新來的Http請求就要等待了。 實際上,線程也不是無限可用的資源, 也得複用。"
小東心想, 咱們JDBC也能夠這麼搞啊, 把數據庫鏈接給緩存下來, 隨用隨取, 一來正好能夠控制碼農們無限制的鏈接數據庫; 二來能夠減小數據庫鏈接時間; 第三還能夠複用Connection上的PreparedStatement, 不用總是找數據庫預編譯了。
3
數據庫鏈接池
創建數據庫鏈接池不是那麼一路順風的, 小東的第一次嘗試是建立了一個ConnectionPool這個接口:
裏邊有兩個重要的方法, getConnection(), 用於從池中取出一個讓用戶使用;
releaseConnection() 把數據庫鏈接放回池中去。
小東想, 只要我寫一個ConnectionPool的實現類, 裏邊能夠維護一個管理數據庫鏈接的數據結構就好了, 碼農們用起來也很方便, 他們確定會喜歡的。
但是事與願違, 幾乎沒有人用這樣的接口。
小東通過多方打探才明白, 碼農們要麼是用DriverManager來得到Connection, 要麼是使用DataSource來獲得Connection;關閉的時候,只須要調用Connection.close() 就能夠了。
這樣的代碼已經有不少了, 而小東的新方案至關於新的接口, 須要改代碼才能用, 話說回來, 誰願意沒事改代碼玩? 尤爲是正在運行的系統。
再作一次改進吧, 小東 去找Java 這個設計高手求教。
Java 說:「雖然ConnectionPool概念不錯, 可是具體的實現最好隱藏起來, 對碼農來講,仍是經過DataSource 來獲取Connection, 至於這個Connection 是新建的仍是從鏈接池中來的, 碼農不該該關心, 因此應該加一個代理Proxy,把物理的Connection給封裝起來, 而後把這個Proxy返回給碼農。」
「那這個Proxy是否是得和您定義的接口Connection 接口保持一致? 要否則碼農還得面對新東西。」
「是的, 這個Proxy 應該也實現JDBC的Connection 接口, 像這樣: 」
(點擊看大圖)
小東說: 」奧, 我明白了, 當碼農從DataSource中得到Connection的時候, 我返回的實際上是一個ConnectionProxy , 其中封裝了一個從ConnectionPool來的Connection , 而後把各類調用轉發到這個實際的physicalConn的方法去, 關鍵點在close, 並不會真的關閉Connection, 而是放回到ConnectionPool 「
「哈哈, 看來你已經get了, 這就是面向接口編程的好處啊, 你給碼農返回了一個ConnectionProxy, 可是碼農們一無所知, 仍然覺得是在使用Connection , 這是一次成功的‘欺騙’啊」
「可是你定義的Connection 接口中的方法實在是太多了, 足足有50多個, 我這個Proxy類實際上只關注那麼幾個, 例如close方法, 其餘的都是轉發而已,這麼多無聊的轉發代碼是在是太煩人了」
Java說: 「還有一種辦法,能夠用動態代理啊」
小東問:「什麼是動態代理?」
"剛纔咱們提供的那個Proxy能夠稱爲靜態代理, 個人動態代理不用你寫一個類去實現Connection, 徹底能夠在運行期來作, 仍是先來看代碼吧"
(點擊看大圖)
「代碼有點難懂, 你看,這裏沒有聲明一個實際的類來實現Connection 接口 , 而是用動態代理在運行時建立了一個類Proxy.newProxyInstance(....) , 重點部分就是InvocationHandler, 在他的invoke方法中, 咱們判斷下被調用的方法是否是close, 若是是, 就把Connection 放回鏈接池, 若是不是,就調用實際Connection的方法。」 Java 解釋了一通。
小東驚歎到:「代碼雖然難懂, 可是精簡了好多,我對Java 反射不太熟, 回頭我再仔細研究下。」
(碼農翻身注: 不熟悉Java動態代理的也能夠研究下, 這是一項重要的技術)
通過一番折騰, 數據庫鏈接池終於隱藏起來了, 碼農們可使用原有的方式去獲取Connection, 只是不知道背後其實發生了鉅變。
固然也不可能讓碼農徹底意識不到鏈接池, 畢竟他們還得去設置一些參數, 小東定義了一些:
數據庫鏈接池得到了巨大的成功, 幾乎成了每個Java Web項目的標配, 不同的JDBC驅動小東也得到了極高的榮譽, 後面等着他的還會有哪些挑戰呢?
抱怨
JDBC出現之後, 以其對數據庫訪問出色的抽象, 良好的封裝, 特別是把接口和具體分開的作法, 贏得了一片稱讚。
(參見文章《》)
乘着Java和互聯網的東風, JDBC在風口飛了起來, 無數的程序開始使用JDBC進行編程, 訪問數據庫村的數據庫, 在數據庫村,不管是大佬Oracle, 仍是小弟Mysql都賺的盆滿鉢滿。
所謂物極必反, JDBC的代碼寫得多了, 它的弱點就暴露出來了, 不少碼農抱怨道:「JDBC是在是太Low了」。
消息傳到Tomcat村, Java 簡直不相信本身的耳朵: 「什麼? JDBC還很low ? 看來那幫碼農沒有用socket訪問過數據庫吧?! 那才叫low . 」
Tomcat 說 : 「你是個標準的制定者, 寫代碼太少了,太不接地氣了, 你看看這樣的代碼: 」
Java 把代碼拿過來一看, 不禁的倒吸了一口涼氣: 「 代碼怎麼這麼長啊, 彷佛是有那麼一點問題, ‘噪聲’彷佛太多了, 把業務代碼全給淹沒了」
Tomcat說:」看來你也是個明白人啊, 爲了正確的打開和關閉你定義的Connection , Statement, ResultSet 須要花不少功夫, 再加上那些異常處理, 一個50多行的程序, 真正作事的也就那麼10幾行而已, 這些瑣碎代碼太煩人了, 因此你們抱怨很low啊。 」
Java表示贊成: 「不錯, 能夠想象, 若是代碼中有大量這樣的代碼, 碼農會抓狂的, 不過,」 Java忽然想到了些什麼 , 「其實這不是個人問題, 碼農們抱怨錯人了, 我做爲一門語言, 所能提供的就是貼近底層(socket)的抽象, 這樣通用性最強。 至於碼農想消除這些重複代碼, 徹底能夠再封裝, 再抽象, 再分層啊」
Tomcat想一想也是, 在計算機世界裏, 每一個人都有分工, 不能強求別人作不喜歡也不擅長的事情, 看來這件事錯怪Java了。
JDBCTemplate
Java 預料的不錯, 稍微有點追求, 不肯意寫重複代碼的碼農都對JDBC作了封裝, 例如寫個DBUtil的工具類把打開數據庫鏈接, 發出查詢語句都封裝了起來。
碼農的抱怨也漸漸平息了。
有一天, 有個叫JDBCTemplate的人來到了Tomcat村找到了Java , 他自稱是Rod Johnson派來專門用於解決JDBC問題的, 他提供了一個優雅而簡潔的解決方案。
(注: Rod Johnson就是Spring 最初的做者)
JDBCTemplate說: 「尊敬的Java先生, 感謝您發明了JDBC, 讓咱們能夠遠程訪問數據庫村, 您也據說了很多對JDBC的抱怨吧, 個人主人Rod Johnson 也抱怨過, 不過他在大量的編程實踐中總結了不少經驗, 他認爲數據庫訪問無外乎這幾件事情:
指定數據庫鏈接參數
打開數據庫鏈接
聲明SQL語句
預編譯並執行SQL語句
遍歷查詢結果
處理每一次遍歷操做
處理拋出的任何異常
處理事務
關閉數據庫鏈接」
「個人主人認爲」 JDBCTemplate說, 「開發人員只須要完成黑體字工做就能夠了,剩下的事情由我來辦「
「大家主人的總結能力很強, 把一個框架應該作的事情和用戶應該作的事情區分開了 」 Java說
JDBCTemplate 看到Java 態度不錯, 趕忙趁熱打鐵: 「 我給你看個例子:」
Java 和以前那個傳統的JDBC比較了一下, JDBCTemplate的方式的確是把注意力放到了業務層面: 只關注SQL查詢, 以及把ResultSet和 User業務類進行映射, 至於如何打開/關閉Connection, 如何發出查詢,JDBCTemplate在背後都給你悄悄的完成了, 徹底不用碼農去操心。
「你在JDBC上作了不錯的抽象和封裝」 Java 說, 「可是我還不明白JDBCTemplate 怎麼建立出來的」
「這很簡單, 你能夠直接把它new 出來, 固然new 出來的時候須要一個參數, 就是javax.sql. DataSource, 這也是你定義的一個標準接口啊」
"固然, 我主人Rod Johnson推薦結合Spring 來使用, 能夠輕鬆的把我‘注入’到各個你須要的地方去"。
「明白了, 大家主人這是要推銷Spring 啊, 那是什麼東西? 」
「簡單的說,就是一個依賴注入和AOP框架, 功能強大又靈活。 具體的細節還得讓我主人親自來給您介紹了 」
(注: 參見《Spring 的本質系列(1) -- 依賴注入》 返回上一級 回覆數字 0002閱讀文章和《Spring 的本質系列(2) -- AOP》返回上一級 回覆數字 0003閱讀文章)
「好吧, 無論如何, 我看你用起來還不錯, 能夠向你們推薦一下。」
O/R Mapping
JDBCTemplate這樣對JDBC的封裝 , 把數據庫的訪問向前推動了一大步, 可是Tomcat村和數據庫村的不少有識之士都意識到: 本質的問題仍然沒有解決!
這個問題就是面向對象世界和關係數據世界之間存在的巨大鴻溝。
Tomcat村的Java 程序都是面向對象的: 封裝、繼承、多態, 對象被建立起來之後, 互相關聯, 在內存中造成了一張圖。
數據庫村的關係數據庫則是表格: 主鍵,外鍵, 關係運算、範式、事務。 數據被持久化在硬盤上。
ResultSet依然是對一個表的數據的抽象和模擬: rs.next() 獲取下一行, rs.getXXX("XX") 訪問該行某一列。
把關係數據轉化成Java對象的過程, 仍然須要碼農們寫大量代碼來完成。
如今碼農的呼聲愈來愈高, 要把這個過程給自動化了。 他們的要求很清晰: 咱們只想用面向對象的方式來編程, 咱們告訴你Java 類、屬性 和數據庫表、字段之間的關係, 你能不能自動的把對數據庫的增刪改查給實現了 ?
他們還把這個訴求起了一個很洋氣的名稱: O/R Mapping (Object Relational Mapping)
Java 天然不敢怠慢, 趕忙召集Tomcat村和數據庫村開了一次會議, 肯定了這麼幾個原則:
1. 數據庫的表映射爲Java 的類(class)
2. 表中的行記錄映射爲一個個Java 對象
3. 表中的列映射爲Java 對象的屬性。
可是光有這幾個原則是遠遠不夠的, 一旦涉及到實際編程, 細節會撲面而來:
1. Java類的粒度要精細的多, 有時候多個類合在一塊兒才能映射到一張表
例以下面的例子, User類 的name屬性實際上是也是一個類, 但在數據庫User 表中, firstName, middleName, lastName倒是在同一張表中的。
2. Java 的面向對象有繼承, 而數據庫都是關係數據, 根本沒有繼承這回事!
這時候可選的策略就不少了, 好比
(1) 把父類和子類分別映射到各自的Table 中, 數據會出現冗餘
(2) 把父類的公共屬性放到一個Table中, 每一個子類都映射到各自的Table中, 可是隻存放子類本身的屬性。子類和父類的表之間須要關聯。
(3) 乾脆把父類和子類都映射到同一張Table中, 用一個字段(例如Type)來代表這一行記錄是什麼類型。
3. 對象的標識問題
Java中使用a==b 或者 a.equals(b) 來進行對象是否相等的判斷, 而數據庫則是另一套:使用主鍵。
4. 對象的關聯問題
在Java 中, 一個對象關聯到另一個或者一組對象是在是太常見了, 雙向的關聯(也就是A 引用B , B反過來也引用了A )也時常出現, 而在數據庫中定義關聯能用的手段只剩下外鍵和關聯表了。
5. 數據導航
在OOP中, 多個對象組成了一張網, 順着網絡上的路徑, 能夠輕鬆的從一個對象到達另一個對象。 例如: City c = user.getAddress().getCity();
在關係數據庫中非得經過SQL查詢, 表的鏈接等方式來實現不可。
6. 對象的狀態
在OOP中, 對象無非就是建立出來使用, 若是不用了,就須要回收掉, 可是一旦扯上數據庫, 勢必要在編程中引入新的狀態,例如「已經持久化」
......
(注: 原本想講Hibernate, 可是限於篇幅, 實在是沒法展開講細節, 這幾個問題是Hibernate 官網上提到的, 是O/R Maping 的本質問題)
這些細節問題讓Java 頭大, 他暗自思忖: " 仍是別管那些碼農的抱怨, 我仍是守住JDBC這一畝三分地吧, 這些煩人的O/R Mapping 問題仍是讓別人去處理好了。 "
O/R Mapping 的具體實現就這麼被Java 擱置下了。
Hibernate 和 JPA
隨着時間的推移,各大廠商都想利用Java 賺錢, 聯合起來搞了一個叫J2EE的規範, 而後瘋狂的推行本身的應用服務器(例如Weblogic, Websphere等等),還搭配着硬件銷售, 在互聯網泡沫時代賺了很多錢。
J2EE中也有一個叫Entity Bean 的東西, 試圖去搞定O/R Mapping , 但其蹩腳的實現方式被碼農們罵了個狗血噴頭。
轉眼間, 時間到了2001年, Tomcat告訴Java 說: 「據說了嗎? 如今不少碼農都被一個叫Hibernate的東西給迷住了」
"冬眠(Hibernate) ? 讓內存中的數據在數據庫裏冬眠, 這個名字起的頗有意境啊 , 我猜是一個O/R Mapping 工具吧"
"沒錯, 是由一個叫Gavin King的小帥哥開發的, 這個框架很強悍, 它實現了咱們以前討論的各類煩人細節, 你們都趨之若鶩, 已經成爲O/R Mapping 事實上的標準, Entity Bean 已經快被你們拋棄了。 "
「不要緊, Entity Bean 從1.0 開始就是一個扶不起的阿斗, 我已經想通了, 我這裏只是指定標準, 具體的實現讓別人去作。 既然Hibernate 這麼火爆, 咱們就把Gavin King 招安了吧 」
「怎麼招安? 」
「讓小帥哥過來領導着你們搞一個規範吧, 參考一下Hibernate的成功經驗 , 他應該會很樂意的。 」
不久之後, 一個新的Java規範誕生了, 專門用於處理Java 對象的持久化問題, 這個新的規範就是JPA(Java Persistence API), Hibernate 天然也實現了這個規範, 幾乎就是JPA的首選了。
作互聯網研發,最先接觸使用jdbc技術,爲了數據庫鏈接可以複用,會用到c3p0、dbcp等數據庫鏈接池。應該是研發人員最先接觸的數據庫鏈接池,再到httpclient http鏈接池,再到微服務netty鏈接池,redis客戶端鏈接池,以及jdk中線程池技術。
這麼多數據庫、http、netty鏈接池,jdk線程池,本質上都是鏈接池技術,鏈接池技術核心是鏈接或者說建立的資源複用。
鏈接池技術核心:經過減小對於鏈接建立、關閉來提高性能。用於用戶後續使用,好處是後續使用不用在建立鏈接以及線程,由於這些都須要相關不少文件、鏈接資源、操做系統內核資源支持來完成構建,會消耗大量資源,而且建立、關閉會消耗應用程序大量性能。
網絡鏈接自己會消耗大量內核資源,在linux系統下,網絡鏈接建立自己tcp/ip協議棧在內核裏面,鏈接建立關閉會消耗大量文件句柄(linux中萬物皆文件,一種厲害抽象手段)系統資源。當下更可能是應用tcp技術完成網絡傳輸,反覆打開關閉,須要操做系統維護大量tcp協議棧狀態。
鏈接池本質上是構建一個容器,容器來存儲建立好的線程、http鏈接、數據庫鏈接、netty鏈接等。對於使用方至關於黑盒,按照接口進行使用就能夠了。各個鏈接池構建、使用管理詳細過程大概分紅如下三部分。
第一部分:首先初始化鏈接池,根據設置相應參數,鏈接池大小、核心線程數、核心鏈接數等參數,初始化建立數據庫、http、netty鏈接以及jdk線程。
第二部分:鏈接池使用,前邊初始化好的鏈接池、線程池,直接從鏈接池、線程中取出資源便可進行使用,使用完後要記得交還鏈接池、線程池,經過池容器來對資源進行管理。
第三部分:對於鏈接池維護,鏈接池、線程池來維護鏈接、線程狀態,不可用鏈接、線程進行銷燬,正在使用鏈接、線程進行狀態標註,鏈接、線程不夠後而且少於設置最大鏈接、線程數,要進行新鏈接、線程建立。
經過上邊能夠了解到各類鏈接池技術以及線程池原理或者說套路,理解原理才能不被紛繁複雜表象掩蓋。
下面談談構建本身鏈接池,其實理解了鏈接池、線程原理,可使用ArrayList來構建本身鏈接池、線程池。初始化時建立配置鏈接數、線程,存儲在ArrayList容器中,使用時從ArrayList從取出鏈接、線程進行使用,執行完任務後,提交回ArrayList容器。前提條件是單線程,在多線程狀態下要用線程安全容器。
前邊根據原理介紹了一個簡單鏈接池、線程池怎樣構建,實際工業級別線程池還要考慮到鏈接狀態,短鏈接重連,線程池維護管理高效,線程池穩定等多個因素。
須要用到鏈接池而又沒有相關開源產品可用時,java鏈接池可使用common-pool2來構建,好比google開源gRPC技術,自己是高性能跨平臺技術,但目前做爲微服務使用,沒有鏈接池、負載均衡等相應配套,這時能夠根據須要本身基於Java容器構建本身鏈接池。也能夠利用common-pool2構建鏈接池來提高應用性能,以及保持高可用。common-pool2自己不只僅能夠構建鏈接池使用,還能夠用來構建對象池。
鏈接池還有一個反作用就是實現了高可用,在微服務場景下一個鏈接不可用,那麼再從netty鏈接池中取出一個進行使用,避免了鏈接不可用問題。
掌握原理從比較全面掌握各類池技術,避免數據庫鏈接池,再到httpclient http鏈接池,再到微服務netty鏈接池,redis客戶端鏈接池,以及jdk中線程池,對象池各類各樣池技術,使咱們眼花繚亂,花費過多時間,掌握原理機制以不變應萬變。
推廣一下這個方法,其餘技術也是相似,深刻掌握其原理,就能夠明白其餘相似技術類似原理,避免疲於應對各類新技術。但每一種架構設計與實現又與領域有着關係,也不可講原理不顧實際狀況擴展。理論與架構設計、源碼學習相結合纔是最好的,但願有幫助。
轉自:
對於一個簡單的數據庫應用,因爲對於數據庫的訪問不是很頻繁。這時能夠簡單地在須要訪問數據庫時,就新建立一個鏈接,用完後就關閉它,這樣作也不會帶來什麼明顯的性能上的開銷。可是對於一個複雜的數據庫應用,狀況就徹底不一樣了。頻繁的創建、關閉鏈接,會極大的減低系統的性能,由於對於鏈接的使用成了系統性能的瓶頸。
數據庫鏈接池的基本原理是在內部對象池中維護必定數量的數據庫鏈接,並對外暴露數據庫鏈接獲取和返回方法。
外部使用者可經過 getConnection 方法獲取鏈接,使用完畢後再經過 close 方法將鏈接返回,注意此時鏈接並無關閉,而是由鏈接池管理器回收,併爲下一次使用作好準備。
Java 中有一個 DataSource 接口, 數據庫鏈接池就是 DataSource 的一個實現
下面咱們本身實現一個數據庫鏈接池:
首先實現 DataSource, 這裏使用 BlockingQueue 做爲池 (只保留了關鍵代碼)
import javax.sql.DataSource; import java.io.PrintWriter; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.logging.Logger; public class MyDataSource implements DataSource { static { try { Class.forName("com.mysql.jdbc.Driver"); } catch (Exception e) { e.printStackTrace(); } } //這裏有個坑 //MySQL用的是5.5的 //驅動用的是最新的 //鏈接的時候會報The server time zone value '�й���ʱ��' // is unrecognized or represents more than one time zone //解決方法: //1.在鏈接串中加入?serverTimezone=UTC //2.在mysql中設置時區,默認爲SYSTEM //set global time_zone='+8:00' private String url = "jdbc:mysql://localhost:3306/test?serverTimezone=UTC"; private String user = "root"; private String password = "123456"; private BlockingQueue<Connection> pool = new ArrayBlockingQueue<>(3); public MyDataSource() { initPool(); } private void initPool() { try { for (int i = 0; i < 3; i++) { pool.add(new MyConnection( DriverManager.getConnection(url, user, password), this)); } } catch (SQLException e) { e.printStackTrace(); } } /* 從池中獲取鏈接 */ @Override public synchronized Connection getConnection() throws SQLException { try { return pool.take(); } catch (InterruptedException e) { e.printStackTrace(); } throw new RuntimeException("get connection failed!"); } public BlockingQueue<Connection> getPool() { return pool; } public void setPool(BlockingQueue<Connection> pool) { this.pool = pool; } }
實現本身的鏈接, 對原生鏈接進行封裝, 調用 close 方法的時候將鏈接放回到池中
import java.sql.*; import java.util.Map; import java.util.Properties; importjava.util.concurrent.Executor; public class MyConnection implements Connection { //包裝的鏈接private Connection conn; private MyDataSource dataSource; public MyConnection(Connection conn, MyDataSource dataSource) { this.conn = conn; this.dataSource = dataSource; } @Overridepublic Statement createStatement() throws SQLException { return conn.createStatement(); }@Override public PreparedStatement prepareStatement(String sql) throws SQLException { returnconn.prepareStatement(sql); } @Override public boolean getAutoCommit() throws SQLException {return conn.getAutoCommit(); } @Override public void setAutoCommit(boolean autoCommit) throwsSQLException { conn.setAutoCommit(autoCommit); } @Override public void commit() throwsSQLException { conn.commit(); } @Override public void rollback() throws SQLException { conn.rollback(); } @Override public void close() throws SQLException { //解決重複關閉問題 if(!isClosed()) { dataSource.getPool().add(this); } } @Override public boolean isClosed()throws SQLException { return dataSource.getPool().contains(this); } }
main 方法
import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException;import java.sql.Statement; public class Main { public static void main(String[] args) { DataSource source = new MyDataSource(); try { Connection conn = source.getConnection(); Statement st = conn.createStatement(); st.execute("INSERT INTO USER (name,age) values('bob',12)"); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }
在Java Web開發過程當中,會普遍使用到數據源。
咱們基本的使用方式,是經過Spring使用相似以下的配置,來聲明一個數據源:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="${jdbc.driverClassName}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> <property name="maxActive" value="100" /> <property name="maxIdle" value="20" /> <property name="validationQuery" value="SELECT 1 from dual" /> <property name="testOnBorrow" value="true" /> </bean>
在以後應用裏對於數據庫的操做,都基於這個數據源,但這個數據源鏈接池的建立、銷燬、管理,對於用戶都是近乎透明的,甚至數據庫鏈接的獲取,咱們都看不到Connection
對象了。
這種方式是應用自身的數據庫鏈接池,各個應用之間互相獨立。
在相似於Tomcat這樣的應用服務器內部,也有提供數據源的能力,這時的數據源,能夠爲多個應用提供服務。
這一點相似於之前寫過關於Tomcat內部的Connector對於線程池的使用,能夠各個Connector
獨立使用線程池,也能夠共用配置的Executor
。(Tomcat的Connector組件)
那麼,在Tomcat中,怎麼樣配置和使用數據源呢?
TOMCAT_HOME/conf/context.xml
文件,增長相似於下面的內容:<Resource name="jdbc/TestDB" auth="Container" type="javax.sql.DataSource" maxTotal="100" maxIdle="30" maxWaitMillis="10000" username="root" password="pwd" driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://localhost:3306/test"/>
Context initContext = new InitialContext(); Context envContext = (Context)initContext.lookup("java:/comp/env"); DataSource ds = (DataSource)envContext.lookup("jdbc/TestDB"); Connection conn = ds.getConnection();
咱們看,整個過程也並不比使用Spring等框架進行配置複雜,在應用內獲取鏈接也很容易。多個應用均可以經過第3步的方式獲取數據源,這使得同時提供多個應用共享數據源很容易。
這背後的是怎麼實現的呢?
這個容器的鏈接池是怎麼工做的呢,咱們一塊兒來看一看。
在根據context.xml
中配置的Resouce初始化的時候,會調用具體DataSource對應的實現類,Tomcat內部默認使用的BasicDataSource,在類初始化的時候,會執行這樣一行代碼DriverManager.getDrivers()
,其對應的內容以下,主要做用是使用 java.sql.DriverManager
實現的Service Provider機制,全部jar文件包含META-INF/services/java.sql.Driver文件的,會被自動發現、加載和註冊,不須要在須要獲取鏈接的時候,再手動的加載和註冊。
public static java.util.Enumeration<Driver> getDrivers() { java.util.Vector<Driver> result = new java.util.Vector<>(); for(DriverInfo aDriver : registeredDrivers) { if(isDriverAllowed(aDriver.driver, callerClass)) { result.addElement(aDriver.driver); } else { println(" skipping: " + aDriver.getClass().getName()); } } return (result.elements()); }
以後DataSourceFactory會讀取Resouce中指定的數據源的屬性,建立數據源。
在咱們的應用內getConnection
的時候,使用ConnectionFactory
建立Connection, 注意在建立Connection的時候,重點代碼是這個樣子:
public PooledObject<PoolableConnection> makeObject() throws Exception { Connection conn = _connFactory.createConnection(); initializeConnection(conn); PoolableConnection pc = new PoolableConnection(conn,_pool, connJmxName); return new DefaultPooledObject<>(pc);
這裏的_pool
是GenericObjectPool,鏈接的獲取是經過其進行的。
public Connection getConnection() throws SQLException { C conn = _pool.borrowObject(); }
在整個pool中包含幾個隊列,其中比較關鍵的一個定義以下:
private final LinkedBlockingDeque<PooledObject<T>> idleObjects;
咱們再看鏈接的關閉,
public void close() throws SQLException { if (getDelegateInternal() != null) { super.close(); super.setDelegate(null); } }
這裏的關閉,並不會真的調用到Connection的close方法,咱們經過上面的代碼已經看到,Connection返回的時候,實際上是Connection的Wrapper類。在close的時候,真實的會調用到下面的代碼
// Normal close: underlying connection is still open, so we // simply need to return this proxy to the pool try { _pool.returnObject(this); } catch(IllegalStateException e) {}
所謂的return
,是把鏈接放回到上面咱們提到的idleObjects隊列中。整個鏈接是放在一個LIFO的隊列中,因此若是沒有關閉或者超過最大空閒鏈接,就會加到隊列中。而容許外的鏈接纔會真實的銷燬destory
。
int maxIdleSave = getMaxIdle(); if (isClosed() || maxIdleSave > -1 && maxIdleSave <= idleObjects.size()) { try { destroy(p); } catch (Exception e) { swallowException(e); } } else { if (getLifo()) { idleObjects.addFirst(p); // 這裏。 } else { idleObjects.addLast(p); } if (isClosed()) { // Pool closed while object was being added to idle objects. // Make sure the returned object is destroyed rather than left // in the idle object pool (which would effectively be a leak) clear(); } }
總結下:以上是Tomcat內部實現的DataSource部分關鍵代碼。數據源咱們有時候也稱爲鏈接池,所謂池的概念,就是一組能夠不斷重用的資源,在使用完畢後,從新恢復狀態,以備再次使用。
而爲了達到重用的效果,對於客戶端的關閉操做,就不能作真實意義上的物理關閉,而是根據池的狀態,以執行具體的入隊重用,仍是執行物理關閉。不管鏈接池,仍是線程池,池的原理大體都是這樣的。
微信公衆號【黃小斜】大廠程序員,互聯網行業新知,終身學習踐行者。關注後回覆「Java」、「Python」、「C++」、「大數據」、「機器學習」、「算法」、「AI」、「Android」、「前端」、「iOS」、「考研」、「BAT」、「校招」、「筆試」、「面試」、「面經」、「計算機基礎」、「LeetCode」 等關鍵字能夠獲取對應的免費學習資料。