明輝手游網(wǎng)中心:是一個免費(fèi)提供流行視頻軟件教程、在線學(xué)習(xí)分享的學(xué)習(xí)平臺!

淺談MySQL JDBC StreamResult通信原理

[摘要]本篇文章給大家?guī)淼膬?nèi)容是關(guān)于淺談MySQL JDBC StreamResult通信原理,有一定的參考價值,有需要的朋友可以參考一下,希望對你有所幫助。使用MySQL JDBC讀取過較大數(shù)據(jù)量的人應(yīng)...
本篇文章給大家?guī)淼膬?nèi)容是關(guān)于淺談MySQL JDBC StreamResult通信原理,有一定的參考價值,有需要的朋友可以參考一下,希望對你有所幫助。

使用MySQL JDBC讀取過較大數(shù)據(jù)量的人應(yīng)該清楚(例如超過1GB),在讀取的時候內(nèi)存很可能會Java堆內(nèi)存溢出,而我們的解決方案是statement.setFetchSize(Integer.MIN_VALUE)并確保游標(biāo)是只讀向前滾動的即可(為游標(biāo)的默認(rèn)值),也可以強(qiáng)制類型轉(zhuǎn)換為com.mysql.jdbc.StatementImpl,然后調(diào)用其內(nèi)部方法:enableStreamingResults()這樣讀取數(shù)據(jù)內(nèi)存就不會掛掉了,這兩者達(dá)到的效果是一致的。當(dāng)然也可以使用useCursorFetch,但是這種方式測試結(jié)果性能要比StreamResult慢很多,為什么?在本文會闡述其大致的原理。

我在前面的部分文章和書籍中都有介紹過其MySQL JDBC在這一塊內(nèi)部處理的代碼分成三個不同的類來完成的,不過我一直沒有去深究過數(shù)據(jù)庫和JDBC之間到底是如何通信的過程。有一段時間我一直認(rèn)為這都屬于服務(wù)端行為或者是客戶端與服務(wù)端配合的行為,然后并不其然,今天我們來講一下這個行為是怎么回事。

【先回顧一下簡單的通信】:

JDBC與數(shù)據(jù)庫之間的通信是通過Socket完成的,因此我們可以把數(shù)據(jù)庫當(dāng)成一個SocketServer的提供方,因此當(dāng)SocketServer返回數(shù)據(jù)的時候(類似于SQL結(jié)果集的返回)其流程是:服務(wù)端程序數(shù)據(jù)(數(shù)據(jù)庫) -> 內(nèi)核Socket Buffer -> 網(wǎng)絡(luò) -> 客戶端Socket Buffer -> 客戶端程序(JDBC所在的JVM內(nèi)存)

到目前為止,IT行業(yè)中大家所看到的JDBC無論是:MySQL JDBC、SQL Server JDBC、PG JDBC、Oracle JDBC。甚至于是NoSQL的Client:Redis Client、MongoDB Client、Memcached,數(shù)據(jù)的返回基本也是這樣一個邏輯。

2509688-08682973734cd3ee.png

【使用MySQL JDBC默認(rèn)直接讀取數(shù)據(jù)為什么會掛?】

(1)MySQL Server方在發(fā)起的SQL結(jié)果集會全部通過OutputStream向外輸出數(shù)據(jù),也就是向本地的Kennel對應(yīng)的socket buffer中寫入數(shù)據(jù),這是一次內(nèi)存拷貝(內(nèi)存拷貝這個不是本文的重點(diǎn))。

(2)此時Kennel的Buffer有數(shù)據(jù)的時候就會把數(shù)據(jù)通過TCP鏈路(JDBC主動發(fā)起的Socket鏈路),回傳數(shù)據(jù),此時數(shù)據(jù)會回傳到JDBC所在機(jī)器上,會先進(jìn)入Kennel區(qū)域,同樣進(jìn)入到一個Buffer區(qū)。

(3)JDBC在發(fā)起SQL操作后,Java代碼是在inputStream.read()操作上阻塞,當(dāng)緩沖區(qū)有數(shù)據(jù)的時候,就會被喚醒,然后將緩沖區(qū)的數(shù)據(jù)讀取到Java內(nèi)存中,這是JDBC端的一次內(nèi)存拷貝。

(4)接下來MySQL JDBC會不斷讀取緩沖區(qū)數(shù)據(jù)到Java內(nèi)存中,MySQL Server會不斷發(fā)送數(shù)據(jù)。注意在數(shù)據(jù)沒有完全組裝完之前,客戶端發(fā)起的SQL操作不會響應(yīng),也就是給你的感覺MySQL服務(wù)端還沒響應(yīng),其實數(shù)據(jù)已經(jīng)到本地,JDBC還沒對調(diào)用execute方法的地方返回結(jié)果集的第一條數(shù)據(jù),而是不斷從緩沖器讀取數(shù)據(jù)。

(5)關(guān)鍵是這個傻帽就像一把這個數(shù)據(jù)讀取完,根本不管家里放不放的下,就會將整個表的內(nèi)容讀取到Java內(nèi)存中,先是FULL GC,接下來就是內(nèi)存溢出。

【JDBC參數(shù)上設(shè)置useCursorFetch=true可以解決】

這個方案配合FetchSize設(shè)置,確實可以解決問題,這個方案其實就是告訴MySQL服務(wù)端我要多少數(shù)據(jù),每次要多少數(shù)據(jù),通信過程有點(diǎn)像這樣:

2509688-f1a76be8ea4da3f9.png

這樣做就像我們生活中的那樣,我需要什么就去超市買什么,需要多少就去買多少。不過這種交互不像現(xiàn)在網(wǎng)購,坐在家里就可以把東西送到家里來,它一定要走路(網(wǎng)絡(luò)鏈路),也就是需要網(wǎng)絡(luò)的時間開銷,假如數(shù)據(jù)有1億數(shù)據(jù),將FetchSize設(shè)置成1000的話,會進(jìn)行10萬次來回通信;如果網(wǎng)絡(luò)延遲同機(jī)房0.02ms,那么10萬次通信會增加2秒的時間,不算大。那么如果跨機(jī)房2ms的延遲時間會多出來200秒(也就是3分20秒),如果國內(nèi)跨城市10~40ms延遲,那么時間將會1000~4000秒,如果是跨國200~300ms呢?時間會多出十多個小時出來。

在這里的計算中,我們還沒有包含系統(tǒng)調(diào)用次數(shù)增加了很多,線程等待和喚醒的上下文次數(shù)變多,網(wǎng)絡(luò)包重傳的情況對整體性能的影響,因此這種方案看似合理,但是性能確不怎么樣。

另外,由于MySQL方不知道客戶端什么時候?qū)?shù)據(jù)消費(fèi)完,而自身的對應(yīng)表可能會有DML寫入操作,此時MySQL需要建立一個臨時表空間來存放需要拿走的數(shù)據(jù)。因此對于當(dāng)你啟用useCursorFetch讀取大表的時候會看到MySQL上的幾個現(xiàn)象:

(1)IOPS飆升,因為存在大量的IO讀取,如果是普通硬盤,此時可能會引起業(yè)務(wù)寫入的抖動

(2)磁盤空間飆升,這塊臨時空間可能比原表更大,如果這個表在整個庫內(nèi)部占用相當(dāng)大的比重有可能會導(dǎo)致數(shù)據(jù)庫磁盤寫滿,空間會在結(jié)果集讀取完成后或者客戶端發(fā)起Result.close()時由MySQL去回收。

(3)CPU和內(nèi)存會有一定比例的上升,根據(jù)CPU的能力決定。

(4)客戶端JDBC發(fā)起SQL后,長時間等待SQL響應(yīng)數(shù)據(jù),這段時間就是服務(wù)端在準(zhǔn)備數(shù)據(jù),這個等待與原始的JDBC不設(shè)置任何參數(shù)的方式也表現(xiàn)出等待,在內(nèi)部原理上是不一樣的,前者是一直在讀取網(wǎng)絡(luò)緩沖區(qū)的數(shù)據(jù),沒有響應(yīng)給業(yè)務(wù),現(xiàn)在是MySQL數(shù)據(jù)庫在準(zhǔn)備臨時數(shù)據(jù)空間,沒有響應(yīng)給JDBC。

【Stream讀取數(shù)據(jù)】

我們知道第1種方式會導(dǎo)致Java掛掉,第2種方式效率低而且對MySQL數(shù)據(jù)庫的影響較大,客戶端響應(yīng)也較慢,僅僅能夠解決問題而已,那么現(xiàn)在來看下Stream讀取方式。

前面提到當(dāng)你使用statement.setFetchSize(Integer.MIN_VALUE)或com.mysql.jdbc.StatementImpl.enableStreamingResults()就可以開啟Stream讀取結(jié)果集的方式,在發(fā)起execute之前FetchSize不能再手工設(shè)置,且確保游標(biāo)是FORWARD_ONLY的。

這種方式很神奇,似乎內(nèi)存也不掛了,響應(yīng)也變快了,對MySQL的影響也變小了,至少IOPS不會那么大了,磁盤占用也沒有了。以前僅僅看到JDBC中走了單獨(dú)的代碼,認(rèn)為這是MySQL和JDBC之間的另一種通信協(xié)議,殊不知,它竟然是“客戶端行為”,沒錯,你沒看錯,它就是客戶端行為。

它在發(fā)起enableStreamingResults()的時候,幾乎不會做任何與服務(wù)端的交互工作,也就是服務(wù)端會按照方式1回傳數(shù)據(jù),那么服務(wù)端使勁向緩沖區(qū)懟數(shù)據(jù),客戶端是如何扛得住壓力的呢?

在JDBC當(dāng)中,當(dāng)你開啟Stream結(jié)果集處理的時候,它并不是一把將所有數(shù)據(jù)讀取到Java內(nèi)存中的,也就是圖1中并不是一次性將數(shù)據(jù)讀取到Java緩沖區(qū)的,而是每次讀取一個package(這個package可以理解成Java中的一個byte[]數(shù)組),一次最多讀取這么多,然后會看是否繼續(xù)向下讀取保證數(shù)據(jù)的完整性。業(yè)務(wù)代碼是按照字節(jié)解析成行也業(yè)務(wù)方使用的。

服務(wù)端剛開始使勁向緩沖區(qū)懟數(shù)據(jù),這些數(shù)據(jù)也會懟滿客戶端的內(nèi)核緩沖區(qū),當(dāng)兩邊的緩沖區(qū)都被懟滿的時候,服務(wù)端的1個Buffer嘗試通過TCP傳遞數(shù)據(jù)給接收方時,此時由于消費(fèi)方的緩沖區(qū)也是滿的,因此發(fā)送方的線程會阻塞住,等待對方消費(fèi),對方消費(fèi)一部分,就可以推送一部分?jǐn)?shù)據(jù)過去。連起來看就是JDBC的Stream數(shù)據(jù)未來得及消費(fèi)之前,緩沖區(qū)數(shù)據(jù)如果是滿的,那么MySQL發(fā)送數(shù)據(jù)的線程就阻塞住了,這樣確保了一個平衡(關(guān)于這一點(diǎn),大家可以使用Java的Socket來嘗試下是否是這樣的)。

對于JDBC客戶端,數(shù)據(jù)獲取的時候每次都在本地的內(nèi)核緩沖區(qū)當(dāng)中,就在小區(qū)的快遞包裹箱拿回家一個距離,那么自然比起每次去超市的RT要小得多了,而且這個過程是準(zhǔn)備好的數(shù)據(jù),所以沒有IO阻塞的過程(除非MySQL服務(wù)端傳遞的數(shù)據(jù)還不如消費(fèi)端處理數(shù)據(jù)來得快,那一般也只有消費(fèi)端不做任何業(yè)務(wù),拿到數(shù)據(jù)直接放棄的測試代碼,才會發(fā)生這樣的事情),這個時候不論:跨機(jī)房、跨地區(qū)、跨國家,只要服務(wù)端開始響應(yīng)就會源源不斷地傳遞數(shù)據(jù)過來,而這個動作即使是第1種方式也是必然需要經(jīng)歷的過程。

相對于第1種方式,JDBC使用的時候會不導(dǎo)致內(nèi)存溢出,即使讀取大表不內(nèi)存溢出也會很長時間才會響應(yīng);不過這種方式相對方式1來講對數(shù)據(jù)庫影響相對較大,在傳遞的數(shù)據(jù)的過程中,相應(yīng)的數(shù)據(jù)行會被上鎖(防止被修改),使用InnoDB會分段加鎖處理,使用MyISAM會加全表鎖,可能導(dǎo)致業(yè)務(wù)阻塞。

【理論上可以更進(jìn)一步,只要你愿意】

理論上這種方式是比較好的了,但是就完美主義來講,我們可以繼續(xù)探討一下,對于懶人來講,我們連到小區(qū)樓下快遞包裹箱去拿一下的動力也是沒有的,我們心里想的就是要是誰給我拿到家里來送到我嘴巴里,連嘴巴都給我掰開多好。

在技術(shù)上理論上確實可以做到這樣,因為JDBC從內(nèi)核拷貝內(nèi)存到Java當(dāng)中是需要花時間的,要是有另一個人把這個事情做了,我在家里干別的事情的時候它就給我送到家里來了,我要用的時候就直接從家里來,這個時間豈不是省掉了。每錯,對于你來講確實省掉了,不過問題就是誰來送?

在程序中一定需要加一個線程來干這個事情,把內(nèi)核的數(shù)據(jù)拷貝到應(yīng)用內(nèi)存,甚至于解析成行數(shù)據(jù),應(yīng)用程序直接使用,但這一定完美嗎?其實這個中間就有個協(xié)調(diào)問題了,例如家里要炒菜,缺一包調(diào)料,原本可以自己到樓下買,但是非要讓別人送家里,這個時候其它的菜都下鍋了,就剩一包調(diào)料,那么你沒別的辦法,只能等這包調(diào)料送到家里來以后才能進(jìn)行炒菜的下一道工序。所以,在理想情況下,它可以節(jié)約很多次內(nèi)存拷貝時間,會增加一些協(xié)調(diào)鎖的開銷。

那么可以不可以直接從內(nèi)核緩沖區(qū)讀取數(shù)據(jù)呢?

理論上也是可以的,在解釋這個問題之前,我們先了解下除了這一次內(nèi)存拷貝還有那些:

JDBC按照二進(jìn)制將內(nèi)核緩沖區(qū)的數(shù)據(jù)讀取后,也會進(jìn)一步解析成具體的結(jié)構(gòu)化數(shù)據(jù),由于此時要給業(yè)務(wù)方返回ResultSet的具體行的結(jié)構(gòu)化數(shù)據(jù),也就是生成RowData的數(shù)據(jù)一定會有一次拷貝,而且JDBC返回某些對象類型數(shù)據(jù)的時候(例如byte []數(shù)組),在某些場景的實現(xiàn),它不希望你通過結(jié)果集修改返回結(jié)果中的byte []的內(nèi)容(byte[1] = 0xFF)去修改ResultSet本身內(nèi)容,可能還會再做1次內(nèi)存拷貝,業(yè)務(wù)代碼使用過程中還會存在拼字符串,網(wǎng)絡(luò)輸出等,又是一堆的內(nèi)存拷貝,這些在業(yè)務(wù)層面是無法避免的,相對這點(diǎn)點(diǎn)拷貝來講,簡直微不足道,所以我們也沒去干這事情,以為從整體上看幾乎微不足道,除非你的程序瓶頸在這里。

因此從整體上看內(nèi)存拷貝是無法避免的,多的這一次無非是系統(tǒng)級的調(diào)用,開銷會更大一點(diǎn),從技術(shù)上來講,我們是可以做到直接從內(nèi)核態(tài)直接讀取數(shù)據(jù)的;但這個時候就需要按照字節(jié)將Buffer從的數(shù)據(jù)拿走才能讓遠(yuǎn)程更多的數(shù)據(jù)傳遞過來,沒有第三個位置存放Buffer了,否則又回到了內(nèi)核到應(yīng)用的內(nèi)存拷貝上來了。

相對來講,服務(wù)端倒是可以優(yōu)化直接將數(shù)據(jù)通過直接IO的方式傳遞(不過這種方式數(shù)據(jù)的協(xié)議就和數(shù)據(jù)的存儲格式一致了,顯然只是理論上的), 要真正做到自定義的協(xié)議,又要通過內(nèi)核態(tài)數(shù)據(jù)直接發(fā)送,需要通過修改OS級別的文件系統(tǒng)協(xié)議,來達(dá)到轉(zhuǎn)換的目的。

以上就是淺談MySQL JDBC StreamResult通信原理的詳細(xì)內(nèi)容,更多請關(guān)注php中文網(wǎng)其它相關(guān)文章!


學(xué)習(xí)教程快速掌握從入門到精通的SQL知識。