您好,登錄后才能下訂單哦!
不知道大家之前對(duì)類似什么是MySQL JDBC StreamResult通信原理的文章有無(wú)了解,今天我在這里給大家再簡(jiǎn)單的講講。感興趣的話就一起來(lái)看看正文部分吧,相信看完什么是MySQL JDBC StreamResult通信原理你一定會(huì)有所收獲的。
【先回顧一下簡(jiǎn)單的通信】:
JDBC與數(shù)據(jù)庫(kù)之間的通信是通過(guò)Socket完成的,因此我們可以把數(shù)據(jù)庫(kù)當(dāng)成一個(gè)SocketServer的提供方,因此當(dāng)SocketServer返回?cái)?shù)據(jù)的時(shí)候(類似于SQL結(jié)果集的返回)其流程是:服務(wù)端程序數(shù)據(jù)(數(shù)據(jù)庫(kù)) -> 內(nèi)核Socket Buffer -> 網(wǎng)絡(luò) -> 客戶端Socket Buffer -> 客戶端程序(JDBC所在的JVM內(nèi)存)
到目前為止,IT行業(yè)中大家所看到的JDBC無(wú)論是:MySQL JDBC、SQL Server JDBC、PG JDBC、Oracle JDBC。甚至于是NoSQL的Client:Redis Client、MongoDB Client、Memcached,數(shù)據(jù)的返回基本也是這樣一個(gè)邏輯。
【使用MySQL JDBC默認(rèn)直接讀取數(shù)據(jù)為什么會(huì)掛?】
(1)MySQL Server方在發(fā)起的SQL結(jié)果集會(huì)全部通過(guò)OutputStream向外輸出數(shù)據(jù),也就是向本地的Kennel對(duì)應(yīng)的socket buffer中寫(xiě)入數(shù)據(jù),這是一次內(nèi)存拷貝(內(nèi)存拷貝這個(gè)不是本文的重點(diǎn))。
(2)此時(shí)Kennel的Buffer有數(shù)據(jù)的時(shí)候就會(huì)把數(shù)據(jù)通過(guò)TCP鏈路(JDBC主動(dòng)發(fā)起的Socket鏈路),回傳數(shù)據(jù),此時(shí)數(shù)據(jù)會(huì)回傳到JDBC所在機(jī)器上,會(huì)先進(jìn)入Kennel區(qū)域,同樣進(jìn)入到一個(gè)Buffer區(qū)。
(3)JDBC在發(fā)起SQL操作后,Java代碼是在inputStream.read()操作上阻塞,當(dāng)緩沖區(qū)有數(shù)據(jù)的時(shí)候,就會(huì)被喚醒,然后將緩沖區(qū)的數(shù)據(jù)讀取到Java內(nèi)存中,這是JDBC端的一次內(nèi)存拷貝。
(4)接下來(lái)MySQL JDBC會(huì)不斷讀取緩沖區(qū)數(shù)據(jù)到Java內(nèi)存中,MySQL Server會(huì)不斷發(fā)送數(shù)據(jù)。注意在數(shù)據(jù)沒(méi)有完全組裝完之前,客戶端發(fā)起的SQL操作不會(huì)響應(yīng),也就是給你的感覺(jué)MySQL服務(wù)端還沒(méi)響應(yīng),其實(shí)數(shù)據(jù)已經(jīng)到本地,JDBC還沒(méi)對(duì)調(diào)用execute方法的地方返回結(jié)果集的第一條數(shù)據(jù),而是不斷從緩沖器讀取數(shù)據(jù)。
(5)關(guān)鍵是這個(gè)傻帽就像一把這個(gè)數(shù)據(jù)讀取完,根本不管家里放不放的下,就會(huì)將整個(gè)表的內(nèi)容讀取到Java內(nèi)存中,先是FULL GC,接下來(lái)就是內(nèi)存溢出。
【JDBC參數(shù)上設(shè)置useCursorFetch=true可以解決】
這個(gè)方案配合FetchSize設(shè)置,確實(shí)可以解決問(wèn)題,這個(gè)方案其實(shí)就是告訴MySQL服務(wù)端我要多少數(shù)據(jù),每次要多少數(shù)據(jù),通信過(guò)程有點(diǎn)像這樣:
這樣做就像我們生活中的那樣,我需要什么就去超市買什么,需要多少就去買多少。不過(guò)這種交互不像現(xiàn)在網(wǎng)購(gòu),坐在家里就可以把東西送到家里來(lái),它一定要走路(網(wǎng)絡(luò)鏈路),也就是需要網(wǎng)絡(luò)的時(shí)間開(kāi)銷,假如數(shù)據(jù)有1億數(shù)據(jù),將FetchSize設(shè)置成1000的話,會(huì)進(jìn)行10萬(wàn)次來(lái)回通信;如果網(wǎng)絡(luò)延遲同機(jī)房0.02ms,那么10萬(wàn)次通信會(huì)增加2秒的時(shí)間,不算大。那么如果跨機(jī)房2ms的延遲時(shí)間會(huì)多出來(lái)200秒(也就是3分20秒),如果國(guó)內(nèi)跨城市10~40ms延遲,那么時(shí)間將會(huì)1000~4000秒,如果是跨國(guó)200~300ms呢?時(shí)間會(huì)多出十多個(gè)小時(shí)出來(lái)。
在這里的計(jì)算中,我們還沒(méi)有包含系統(tǒng)調(diào)用次數(shù)增加了很多,線程等待和喚醒的上下文次數(shù)變多,網(wǎng)絡(luò)包重傳的情況對(duì)整體性能的影響,因此這種方案看似合理,但是性能確不怎么樣。
另外,由于MySQL方不知道客戶端什么時(shí)候?qū)?shù)據(jù)消費(fèi)完,而自身的對(duì)應(yīng)表可能會(huì)有DML寫(xiě)入操作,此時(shí)MySQL需要建立一個(gè)臨時(shí)表空間來(lái)存放需要拿走的數(shù)據(jù)。因此對(duì)于當(dāng)你啟用useCursorFetch讀取大表的時(shí)候會(huì)看到MySQL上的幾個(gè)現(xiàn)象:
(1)IOPS飆升,因?yàn)榇嬖诖罅康腎O讀取,如果是普通硬盤(pán),此時(shí)可能會(huì)引起業(yè)務(wù)寫(xiě)入的抖動(dòng)
(2)磁盤(pán)空間飆升,這塊臨時(shí)空間可能比原表更大,如果這個(gè)表在整個(gè)庫(kù)內(nèi)部占用相當(dāng)大的比重有可能會(huì)導(dǎo)致數(shù)據(jù)庫(kù)磁盤(pán)寫(xiě)滿,空間會(huì)在結(jié)果集讀取完成后或者客戶端發(fā)起Result.close()時(shí)由MySQL去回收。
(3)CPU和內(nèi)存會(huì)有一定比例的上升,根據(jù)CPU的能力決定。
(4)客戶端JDBC發(fā)起SQL后,長(zhǎng)時(shí)間等待SQL響應(yīng)數(shù)據(jù),這段時(shí)間就是服務(wù)端在準(zhǔn)備數(shù)據(jù),這個(gè)等待與原始的JDBC不設(shè)置任何參數(shù)的方式也表現(xiàn)出等待,在內(nèi)部原理上是不一樣的,前者是一直在讀取網(wǎng)絡(luò)緩沖區(qū)的數(shù)據(jù),沒(méi)有響應(yīng)給業(yè)務(wù),現(xiàn)在是MySQL數(shù)據(jù)庫(kù)在準(zhǔn)備臨時(shí)數(shù)據(jù)空間,沒(méi)有響應(yīng)給JDBC。
【Stream讀取數(shù)據(jù)】
我們知道第1種方式會(huì)導(dǎo)致Java掛掉,第2種方式效率低而且對(duì)MySQL數(shù)據(jù)庫(kù)的影響較大,客戶端響應(yīng)也較慢,僅僅能夠解決問(wèn)題而已,那么現(xiàn)在來(lái)看下Stream讀取方式。
前面提到當(dāng)你使用statement.setFetchSize(Integer.MIN_VALUE)或com.mysql.jdbc.StatementImpl.enableStreamingResults()就可以開(kāi)啟Stream讀取結(jié)果集的方式,在發(fā)起execute之前FetchSize不能再手工設(shè)置,且確保游標(biāo)是FORWARD_ONLY的。
這種方式很神奇,似乎內(nèi)存也不掛了,響應(yīng)也變快了,對(duì)MySQL的影響也變小了,至少IOPS不會(huì)那么大了,磁盤(pán)占用也沒(méi)有了。以前僅僅看到JDBC中走了單獨(dú)的代碼,認(rèn)為這是MySQL和JDBC之間的另一種通信協(xié)議,殊不知,它竟然是“客戶端行為”,沒(méi)錯(cuò),你沒(méi)看錯(cuò),它就是客戶端行為。
它在發(fā)起enableStreamingResults()的時(shí)候,幾乎不會(huì)做任何與服務(wù)端的交互工作,也就是服務(wù)端會(huì)按照方式1回傳數(shù)據(jù),那么服務(wù)端使勁向緩沖區(qū)懟數(shù)據(jù),客戶端是如何扛得住壓力的呢?
在JDBC當(dāng)中,當(dāng)你開(kāi)啟Stream結(jié)果集處理的時(shí)候,它并不是一把將所有數(shù)據(jù)讀取到Java內(nèi)存中的,也就是圖1中并不是一次性將數(shù)據(jù)讀取到Java緩沖區(qū)的,而是每次讀取一個(gè)package(這個(gè)package可以理解成Java中的一個(gè)byte[]數(shù)組),一次最多讀取這么多,然后會(huì)看是否繼續(xù)向下讀取保證數(shù)據(jù)的完整性。業(yè)務(wù)代碼是按照字節(jié)解析成行也業(yè)務(wù)方使用的。
服務(wù)端剛開(kāi)始使勁向緩沖區(qū)懟數(shù)據(jù),這些數(shù)據(jù)也會(huì)懟滿客戶端的內(nèi)核緩沖區(qū),當(dāng)兩邊的緩沖區(qū)都被懟滿的時(shí)候,服務(wù)端的1個(gè)Buffer嘗試通過(guò)TCP傳遞數(shù)據(jù)給接收方時(shí),此時(shí)由于消費(fèi)方的緩沖區(qū)也是滿的,因此發(fā)送方的線程會(huì)阻塞住,等待對(duì)方消費(fèi),對(duì)方消費(fèi)一部分,就可以推送一部分?jǐn)?shù)據(jù)過(guò)去。連起來(lái)看就是JDBC的Stream數(shù)據(jù)未來(lái)得及消費(fèi)之前,緩沖區(qū)數(shù)據(jù)如果是滿的,那么MySQL發(fā)送數(shù)據(jù)的線程就阻塞住了,這樣確保了一個(gè)平衡(關(guān)于這一點(diǎn),大家可以使用Java的Socket來(lái)嘗試下是否是這樣的)。
對(duì)于JDBC客戶端,數(shù)據(jù)獲取的時(shí)候每次都在本地的內(nèi)核緩沖區(qū)當(dāng)中,就在小區(qū)的快遞包裹箱拿回家一個(gè)距離,那么自然比起每次去超市的RT要小得多了,而且這個(gè)過(guò)程是準(zhǔn)備好的數(shù)據(jù),所以沒(méi)有IO阻塞的過(guò)程(除非MySQL服務(wù)端傳遞的數(shù)據(jù)還不如消費(fèi)端處理數(shù)據(jù)來(lái)得快,那一般也只有消費(fèi)端不做任何業(yè)務(wù),拿到數(shù)據(jù)直接放棄的測(cè)試代碼,才會(huì)發(fā)生這樣的事情),這個(gè)時(shí)候不論:跨機(jī)房、跨地區(qū)、跨國(guó)家,只要服務(wù)端開(kāi)始響應(yīng)就會(huì)源源不斷地傳遞數(shù)據(jù)過(guò)來(lái),而這個(gè)動(dòng)作即使是第1種方式也是必然需要經(jīng)歷的過(guò)程。
相對(duì)于第1種方式,JDBC使用的時(shí)候會(huì)不導(dǎo)致內(nèi)存溢出,即使讀取大表不內(nèi)存溢出也會(huì)很長(zhǎng)時(shí)間才會(huì)響應(yīng);不過(guò)這種方式相對(duì)方式1來(lái)講對(duì)數(shù)據(jù)庫(kù)影響相對(duì)較大,在傳遞的數(shù)據(jù)的過(guò)程中,相應(yīng)的數(shù)據(jù)行會(huì)被上鎖(防止被修改),使用InnoDB會(huì)分段加鎖處理,使用MyISAM會(huì)加全表鎖,可能導(dǎo)致業(yè)務(wù)阻塞。
【理論上可以更進(jìn)一步,只要你愿意】
理論上這種方式是比較好的了,但是就完美主義來(lái)講,我們可以繼續(xù)探討一下,對(duì)于懶人來(lái)講,我們連到小區(qū)樓下快遞包裹箱去拿一下的動(dòng)力也是沒(méi)有的,我們心里想的就是要是誰(shuí)給我拿到家里來(lái)送到我嘴巴里,連嘴巴都給我掰開(kāi)多好。
在技術(shù)上理論上確實(shí)可以做到這樣,因?yàn)镴DBC從內(nèi)核拷貝內(nèi)存到Java當(dāng)中是需要花時(shí)間的,要是有另一個(gè)人把這個(gè)事情做了,我在家里干別的事情的時(shí)候它就給我送到家里來(lái)了,我要用的時(shí)候就直接從家里來(lái),這個(gè)時(shí)間豈不是省掉了。每錯(cuò),對(duì)于你來(lái)講確實(shí)省掉了,不過(guò)問(wèn)題就是誰(shuí)來(lái)送?
在程序中一定需要加一個(gè)線程來(lái)干這個(gè)事情,把內(nèi)核的數(shù)據(jù)拷貝到應(yīng)用內(nèi)存,甚至于解析成行數(shù)據(jù),應(yīng)用程序直接使用,但這一定完美嗎?其實(shí)這個(gè)中間就有個(gè)協(xié)調(diào)問(wèn)題了,例如家里要炒菜,缺一包調(diào)料,原本可以自己到樓下買,但是非要讓別人送家里,這個(gè)時(shí)候其它的菜都下鍋了,就剩一包調(diào)料,那么你沒(méi)別的辦法,只能等這包調(diào)料送到家里來(lái)以后才能進(jìn)行炒菜的下一道工序。所以,在理想情況下,它可以節(jié)約很多次內(nèi)存拷貝時(shí)間,會(huì)增加一些協(xié)調(diào)鎖的開(kāi)銷。
那么可以不可以直接從內(nèi)核緩沖區(qū)讀取數(shù)據(jù)呢?
理論上也是可以的,在解釋這個(gè)問(wèn)題之前,我們先了解下除了這一次內(nèi)存拷貝還有那些:
JDBC按照二進(jìn)制將內(nèi)核緩沖區(qū)的數(shù)據(jù)讀取后,也會(huì)進(jìn)一步解析成具體的結(jié)構(gòu)化數(shù)據(jù),由于此時(shí)要給業(yè)務(wù)方返回ResultSet的具體行的結(jié)構(gòu)化數(shù)據(jù),也就是生成RowData的數(shù)據(jù)一定會(huì)有一次拷貝,而且JDBC返回某些對(duì)象類型數(shù)據(jù)的時(shí)候(例如byte []數(shù)組),在某些場(chǎng)景的實(shí)現(xiàn),它不希望你通過(guò)結(jié)果集修改返回結(jié)果中的byte []的內(nèi)容(byte[1] = 0xFF)去修改ResultSet本身內(nèi)容,可能還會(huì)再做1次內(nèi)存拷貝,業(yè)務(wù)代碼使用過(guò)程中還會(huì)存在拼字符串,網(wǎng)絡(luò)輸出等,又是一堆的內(nèi)存拷貝,這些在業(yè)務(wù)層面是無(wú)法避免的,相對(duì)這點(diǎn)點(diǎn)拷貝來(lái)講,簡(jiǎn)直微不足道,所以我們也沒(méi)去干這事情,以為從整體上看幾乎微不足道,除非你的程序瓶頸在這里。
因此從整體上看內(nèi)存拷貝是無(wú)法避免的,多的這一次無(wú)非是系統(tǒng)級(jí)的調(diào)用,開(kāi)銷會(huì)更大一點(diǎn),從技術(shù)上來(lái)講,我們是可以做到直接從內(nèi)核態(tài)直接讀取數(shù)據(jù)的;但這個(gè)時(shí)候就需要按照字節(jié)將Buffer從的數(shù)據(jù)拿走才能讓遠(yuǎn)程更多的數(shù)據(jù)傳遞過(guò)來(lái),沒(méi)有第三個(gè)位置存放Buffer了,否則又回到了內(nèi)核到應(yīng)用的內(nèi)存拷貝上來(lái)了。
相對(duì)來(lái)講,服務(wù)端倒是可以優(yōu)化直接將數(shù)據(jù)通過(guò)直接IO的方式傳遞(不過(guò)這種方式數(shù)據(jù)的協(xié)議就和數(shù)據(jù)的存儲(chǔ)格式一致了,顯然只是理論上的), 要真正做到自定義的協(xié)議,又要通過(guò)內(nèi)核態(tài)數(shù)據(jù)直接發(fā)送,需要通過(guò)修改OS級(jí)別的文件系統(tǒng)協(xié)議,來(lái)達(dá)到轉(zhuǎn)換的目的。
看完什么是MySQL JDBC StreamResult通信原理這篇文章,大家覺(jué)得怎么樣?如果想要了解更多相關(guān),可以繼續(xù)關(guān)注我們的行業(yè)資訊板塊。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。