沒想到你是這樣的JDBC
本文將介紹 MySQL Client 與 Server 的通信原理,以及 Java JDBC 的工作原理等。什么是JDBC 的 Type4,什么又是 Type 3?
一、 MySQL Client & Server
我們在進行數(shù)據(jù)庫的操作時,總是通過 GUI 數(shù)據(jù)管理工具,或者命令行連接到 MySQL 的 Server 上,然后進行一系列數(shù)據(jù)庫的創(chuàng)建、表與表內(nèi)數(shù)據(jù)的操作等。
這個時候,這一系列 GUI管理工具,或者命令行,都是一個 MySQL 的 Client, 然后將 Client 的一系列操作命令,發(fā)送給 Server。 這里在發(fā)送時,Client 的命令都是根據(jù) MySQL 規(guī)范,生成的一個個packet進行發(fā)送。
更直觀的理解, MySQL 的 Client 和 Server 相當(dāng)于是 Socket 通信中的一個 Client 與 Server, 彼此按照約定的協(xié)議格式進行通信。
二、 JDBC 是什么?
什么是 JDBC 呢? 你一定會脫口而出,不就是通過它連庫嘛。 這么理解只是其中的一小部分,「灑灑水的啦」。
JDBC 全稱:The Java Database Connectivity,要從兩個方面來理解。
- API
- Driver
API , 首先是一個標(biāo)準,并不針對特定的數(shù)據(jù)庫,做為一個高層抽象,提供Java 語言與眾多數(shù)據(jù)庫之間的連通。 通過JDBC API,我們不再需要根據(jù)不同的數(shù)據(jù)庫使用不同的操作方式,而是以一種標(biāo)準的操作,實現(xiàn)『Write Once, Run anywhere』。
既然 API 是個標(biāo)準,就需要有相對應(yīng)的實現(xiàn), 這里的 Driver 就是各個數(shù)據(jù)庫廠商根據(jù)標(biāo)準進行的針對實現(xiàn)。這也是為什么在應(yīng)用開發(fā)時,連MySQL 使用 MySQL 的 connector,連接 Oracle 使用 Oracle 的驅(qū)動的原因。
畢竟如何和自己廠家的數(shù)據(jù)庫交互,只有各個廠商自己清楚,所以根據(jù)標(biāo)準,各個廠商開發(fā)自己的 Connector。
下圖來自官方文檔,來描述 JDBC 的作用以及請求中所處的位置。
圖的左側(cè),也稱為Type4, 是通過Driver 直接連接數(shù)據(jù)庫 Server。這種也是最常用的,通過Driver ,將JDBC 的請求轉(zhuǎn)成數(shù)據(jù)庫服務(wù)器可以識別的協(xié)議格式。
圖的右側(cè), 稱為Type 3 是通過Driver,將JDBC 的請求轉(zhuǎn)成 中間件的協(xié)議格式。
以MySQL為例,看到這里我們發(fā)現(xiàn),其實 JDBC 的操作,本質(zhì)上相當(dāng)于是一個 MySQL 的 Client,通過 Driver,把應(yīng)用里的查詢、刪除等操作「翻譯」成了 MySQL Server 可識別的協(xié)議格式,再傳遞過去執(zhí)行。
所以,整個JDBC 做的事情可以歸結(jié)為以下三件:
- 創(chuàng)建數(shù)據(jù)庫連接
- 發(fā)送 SQL statement
- 處理請求結(jié)果
JDBC 總結(jié)起來的兩個部分,數(shù)據(jù)庫服務(wù)提供方,開發(fā)XXXDriver, 應(yīng)用開發(fā)者使用Driver 連接數(shù)據(jù)庫,進行數(shù)據(jù)庫操作。
這樣應(yīng)用開發(fā)者就不需要關(guān)心底層與數(shù)據(jù)庫交互時的協(xié)議實現(xiàn),如何進行請求連接,交互等,可以更專心到自己的業(yè)務(wù)。 否則,每個開發(fā)者都需要處理一次和數(shù)據(jù)交互的協(xié)議,繁瑣而且不易,重復(fù)勞動。
三、MySQL connector-J 部分源碼
有了上述的「理論」知識后,我們來看點干的。 MySQL 的驅(qū)動包是開源的,我們可以很方便的進行下載了解實現(xiàn)。
最傳統(tǒng)的 JDBC 使用,一般都是通過以下這種方式:
- Connection c = DriverManager.getConnection(url, user,pwd);
- Statement stmt = c.createStatment
- stmt.executeQuery 拿結(jié)果
getConnection的時候一般都需要提供一個URL,這個URL也都是固定寫法,比如mysql的是 jdbc:mysql://,這一部分是按照規(guī)范,同時在Driver的代碼里,通過解析URL獲取要連接到的主機,端口,以及其他的連接參數(shù)。
- public Properties parseURL(String url, Properties defaults) throws java.sql.SQLException {
- Properties urlProps = (defaults != null) ? new Properties(defaults) : new Properties();
- if (url == null) {
- return null;
- }
- if (!StringUtils.startsWithIgnoreCase(url, URL_PREFIX) && !StringUtils.startsWithIgnoreCase(url, MXJ_URL_PREFIX)
- && !StringUtils.startsWithIgnoreCase(url, LOADBALANCE_URL_PREFIX) && !StringUtils.startsWithIgnoreCase(url, REPLICATION_URL_PREFIX)) {
- return null;
- }
- int beginningOfSlashes = url.indexOf("//");
- if (StringUtils.startsWithIgnoreCase(url, MXJ_URL_PREFIX)) {
- urlProps.setProperty("socketFactory", "com.mysql.management.driverlaunched.ServerLauncherSocketFactory");
- }
看這一部分源碼可以發(fā)現(xiàn),除了我們常用的url配置,還可以在其中進行l(wèi)oadbalance的配置等等。長了見識。
- DriverManager.getConnection(xx,xx,xx) 這個方法最終會調(diào)用 Service Provider 已經(jīng)加載的 Driver中可用的driver,調(diào)用driver的getConnection方法,對應(yīng)到Mysql的源碼,就是下方這個,重點是`com.mysql.jdbc.ConnectionImpl.getInstance`
- public java.sql.Connection connect(String url, Properties info) {
- if (url == null) {
- throw SQLError.createSQLException(Messages.getString("NonRegisteringDriver.1"), SQLError.SQL_STATE_UNABLE_TO_CONNECT_TO_DATASOURCE, null);
- }
- if (StringUtils.startsWithIgnoreCase(url, LOADBALANCE_URL_PREFIX)) {
- return connectLoadBalanced(url, info);
- } else if (StringUtils.startsWithIgnoreCase(url, REPLICATION_URL_PREFIX)) {
- return connectReplicationConnection(url, info);
- }
- Properties props = null;
- if ((props = parseURL(url, info)) == null) {
- return null;
- }
- if (!"1".equals(props.getProperty(NUM_HOSTS_PROPERTY_KEY))) {
- return connectFailover(url, info);
- }
- try {
- Connection newConn = com.mysql.jdbc.ConnectionImpl.getInstance(host(props), port(props), props, database(props), url);
- return newConn;
- }
再來看 getInstance具體做了啥?
- protected static Connection getInstance(String hostToConnectTo, int portToConnectTo, Properties info, String databaseToConnectTo, String url)
- throws SQLException {
- if (!Util.isJdbc4()) {
- return new ConnectionImpl(hostToConnectTo, portToConnectTo, info, databaseToConnectTo, url);
- }
- return (Connection) Util.handleNewInstance(JDBC_4_CONNECTION_CTOR,
- new Object[] { hostToConnectTo, Integer.valueOf(portToConnectTo), info, databaseToConnectTo, url }, null);
- }
- this.io = new MysqlIO(newHost, newPort, mergedProps, getSocketFactoryClassName(), getProxy(), getSocketTimeout(),
- this.largeRowSizeThreshold.getValueAsInt());
- this.io.doHandshake(this.user, this.password, this.database);
我們看,先通過MysqlIO創(chuàng)建了一個IO連接,然后進行握手
- // save last exception to propagate to caller if connection fails
- SocketException lastException = null;
- // Need to loop through all possible addresses. Name lookup may return multiple addresses including IPv4 and IPv6 addresses. Some versions of
- // MySQL don't listen on the IPv6 address so we try all addresses.
- for (int i = 0; i < possibleAddresses.length; i++) {
- try {
- this.rawSocket = createSocket(props); // 這里創(chuàng)建了一個空的Socket對象
- configureSocket(this.rawSocket, props); //將一些超時之類的屬性設(shè)置到socket中
- InetSocketAddress sockAddr = new InetSocketAddress(possibleAddresses[i], this.port); //獲取host對應(yīng)的ip地址等,再加上端口,組成一個Address
- // bind to the local port if not using the ephemeral port
- if (localSockAddr != null) {
- this.rawSocket.bind(localSockAddr);
- }
- this.rawSocket.connect(sockAddr, getRealTimeout(connectTimeout)); //實際連接到服務(wù)器
連接Mysql的url中,可以分成好幾類,例如可以連接到mysql進行l(wèi)oadbalanner, jdbc:mysql:loadbalancer//xxx 還有進行replicated
我們在使用JDBC連接時,一定會常使用PreparedStatement, 這個稱為預(yù)編譯sql,其中可以設(shè)置一些占位符
那這些占位符是啥時候填充進去的呢?
查看Mysql Connector 的源碼,我們發(fā)現(xiàn),實際前面的createPreparedStatment,setXX之類的時候,
只是設(shè)置到對應(yīng)的變量里記錄了下來,
在執(zhí)行executeQuery的時候,會再從前面記錄下來的變理中提取出來,做為值填充到原來的sql占位中去
整個sql做為一個packet發(fā)送過去。
這個時候也就更容易理解為啥預(yù)編譯不容易被SQL 注入,而拼接SQL容易。 因為預(yù)編譯在替換占位符時,即使你的值里有類似于 「--」 這一類的危險內(nèi)容,或者 1==1, 都是做為一個column的value 來使用,而拼接SQL,則會放到完整的語句中,在執(zhí)行時被全部解析,導(dǎo)致問題。
以下就是 MySQL Connector 在執(zhí)行 sql 時的調(diào)用棧。
- java.lang.Thread.State: RUNNABLE
- at com.mysql.jdbc.MysqlIO.send(MysqlIO.java:3633)
- at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2460)
- at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2625)
- at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2551)
- - locked <0x5a3> (a com.mysql.jdbc.JDBC4Connection)
- at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1861)
- at com.mysql.jdbc.PreparedStatement.executeQuery(PreparedStatement.java:1962)
整個背后其實原理也和我們前面說的一樣,比較簡單,是通過一個TCP Socket 方式,在獲取到OutputStream,接裝好的SQL,
在執(zhí)行的時候,是寫到這個Output里,發(fā)送到 Mysql的服務(wù)器。
返回值是怎么獲取的呢? 是將返回的Buffer轉(zhuǎn)換成ResultSet
- ResultSetInternalMethods rs = readAllResults(callingStatement, maxRows, resultSetType, resultSetConcurrency, streamResults, catalog, resultPacket,
- false, -1L, cachedMetadata);
此外,在實際的業(yè)務(wù)開發(fā)中,對于在代碼中拿到的一個Connection,可能會遇到網(wǎng)絡(luò)抖動,數(shù)據(jù)庫服務(wù)異常等情況。有連接問題之前,我們可以先檢測連接是否可用,來避免繼續(xù)使用有問題的Connection,導(dǎo)致問題一直存在。
檢測一個連接是否可用,可以通過執(zhí)行一條最簡單的 `select 1` 來判斷是否有異常,當(dāng)然,在JDBC的標(biāo)準里,也包含一個檢查連接是否可用的方法 isValid
實現(xiàn)原理,對于MySQL 的Connctor-J客戶端,是通過向Server發(fā)送一條ping的命令,來檢測連接的狀態(tài)。
總結(jié)一下,我們通過幾個部分來介紹了 MySQL Client 與 Server 的交互原理,以及JDBC 是什么,是通過什么方式來和 Server 進行交互的。
順道再分享下最近遇到的一個和數(shù)據(jù)庫連接有關(guān)的小插曲。在處理一個問題,增加數(shù)據(jù)庫連接檢查之后,功能正確就上線了。上線不久,接到另一個服務(wù)提供方報警,說我們發(fā)送了其不能處理的數(shù)據(jù)庫指令。 黑人問號臉。我只是通過獲取數(shù)據(jù)庫狀態(tài)的一個getAttribute的方式來檢查下連接啊。 據(jù)說他們收到的是show xxx status之類的指令。 那為啥不能識別呢?
仔細問了一下,是由于他們提供的特殊 Proxy 服務(wù),只實現(xiàn)了MySQL 的部分指令解析,所以對應(yīng)show xxx 不支持,而我們項目里默認以為全部的client 都支持全集指令,導(dǎo)致問題。之后改了一個檢查方式解決了報警問題。
所以,在開發(fā)時,也需要再考慮下接入的服務(wù),是否會按照規(guī)范,把全部內(nèi)容實現(xiàn)了。
【本文為51CTO專欄作者“侯樹成”的原創(chuàng)稿件,轉(zhuǎn)載請通過作者微信公眾號『Tomcat那些事兒』獲取授權(quán)】