轉載自:http://soft.chinabyte.com/database/258/12609258.shtmlhtml
衆所周知,java在處理數據量比較大的時候,加載到內存必然會致使內存溢出,而在一些數據處理中咱們不得不去處理海量數據,在作數據處理中,咱們常見的手段是分解,壓縮,並行,臨時文件等方法;java
例如,咱們要將數據庫(不管是什麼數據庫)的數據導出到一個文件,通常是Excel或文本格式的CSV;對於Excel來說,對於POI和JXL的接口,你不少時候沒有辦法去控制內存何時向磁盤寫入,很噁心,並且這些API在內存構造的對象大小將比數據原有的大小要大不少倍數,因此你不得不去拆分Excel,還好,POI開始意識到這個問題,在3.8.4的版本後,開始提供cache的行數,提供了SXSSFWorkbook的接口,能夠設置在內存中的行數,不過惋惜的是,他當你超過這個行數,每添加一行,它就將相對行數前面的一行寫入磁盤(如你設置2000行的話,當你寫第20001行的時候,他會將第一行寫入磁盤),其實這個時候他些的臨時文件,以致於不消耗內存,不過這樣你會發現,刷磁盤的頻率會很是高,咱們的確不想這樣,由於咱們想讓他達到一個範圍一次性將數據刷如磁盤,好比一次刷1M之類的作法,惋惜如今尚未這種API,很痛苦,我本身作過測試,經過寫小的Excel比使用目前提供刷磁盤的API來寫大文件,效率要高一些,並且這樣若是訪問的人稍微多一些磁盤IO可能會扛不住,由於IO資源是很是有限的,因此仍是拆文件纔是上策;而當咱們寫CSV,也就是文本類型的文件,咱們不少時候是能夠本身控制的,不過你不要用CSV本身提供的API,也是不太可控的,CSV自己就是文本文件,你按照文本格式寫入便可被CSV識別出來;如何寫入呢?下面來講說。。。node
在處理數據層面,如從數據庫中讀取數據,生成本地文件,寫代碼爲了方便,咱們未必要1M怎麼來處理,這個交給底層的驅動程序去拆分,對於咱們的程序來說咱們認爲它是連續寫便可;咱們好比想將一個1000W數據的數據庫表,導出到文件;此時,你要麼進行分頁,oracle固然用三層包裝便可,mysql用limit,不過度頁每次都會新的查詢,並且隨着翻頁,會愈來愈慢,其實咱們想拿到一個句柄,而後向下遊動,編譯一部分數據(如10000行)將寫文件一次(寫文件細節很少說了,這個是最基本的),須要注意的時候每次buffer的數據,在用outputstream寫入的時候,最好flush一下,將緩衝區清空下;接下來,執行一個沒有where條件的SQL,會不會將內存撐爆?是的,這個問題咱們值得去思考下,經過API發現能夠對SQL進行一些操做,例如,經過:PreparedStatement statement = connection.prepareStatement(sql),這是默認獲得的預編譯,還能夠經過設置:mysql
PreparedStatement statement = connection.prepareStatement(sql,ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY);nginx
來設置遊標的方式,以致於遊標不是將數據直接cache到本地內存,而後經過設置statement.setFetchSize(200);設置遊標每次遍歷的大小;OK,這個其實我用過,oracle用了和沒用沒區別,由於oracle的jdbc API默認就是不會將數據cache到java的內存中的,而mysql裏頭設置根本無效,我上面說了一堆廢話,呵呵,我只是想說,java提供的標準API也未必有效,不少時候要看廠商的實現機制,還有這個設置是不少網上說有效的,可是這純屬抄襲;對於oracle上面說了不用關心,他自己就不是cache到內存,因此java內存不會致使什麼問題,若是是mysql,首先必須使用5以上的版本,而後在鏈接參數上加上useCursorFetch=true這個參數,至於遊標大小能夠經過鏈接參數上加上:defaultFetchSize=1000來設置,例如:算法
jdbc:mysql://xxx.xxx.xxx.xxx:3306/abc?zeroDateTimeconvertToNull&useCursorFetch=true&defaultFetchSize=1000< /span>sql
上次被這個問題糾結了好久(mysql的數據老致使程序內存膨脹,並行2個直接系統就宕了),還去看了不少源碼才發現奇蹟居然在這裏,最後通過mysql文檔的確認,而後進行測試,並行多個,並且數據量都是500W以上的,都不會致使內存膨脹,GC一切正常,這個問題終於完結了。數據庫
咱們再聊聊其餘的,數據拆分和合並,當數據文件多的時候咱們想合併,當文件太大想要拆分,合併和拆分的過程也會遇到相似的問題,還好,這個在咱們可控制的範圍內,若是文件中的數據最終是能夠組織的,那麼在拆分和合並的時候,此時就不要按照數據邏輯行數來作了,由於行數最終你須要解釋數據自己來斷定,可是隻是作拆分是沒有必要的,你須要的是作二進制處理,在這個二進制處理過程,你要注意了,和平時read文件不要使用同樣的方式,平時大多對一個文件讀取只是用一次read操做,若是對於大文件內存確定直接掛掉了,不用多說,你此時因該每次讀取一個可控範圍的數據,read方法提供了重載的offset和length的範圍,這個在循環過程當中本身能夠計算出來,寫入大文件和上面同樣,不要讀取到必定程序就要經過寫入流flush到磁盤;其實對於小數據量的處理在現代的NIO技術的中也有用到,例如多個終端同時請求一個大文件下載,例如視頻下載吧,在常規的狀況下,若是用java的容器來處理,通常會發生兩種狀況:緩存
其一爲內存溢出,由於每一個請求都要加載一個文件大小的內存甚至於更多,由於java包裝的時候會產生不少其餘的內存開銷,若是使用二進制會產生得少一些,並且在通過輸入輸出流的過程當中還會經歷幾回內存拷貝,固然若是有你相似nginx之類的中間件,那麼你能夠經過send_file模式發送出去,可是若是你要用程序來處理的時候,內存除非你足夠大,可是java內存再大也會有GC的時候,若是你內存真的很大,GC的時候死定了,固然這個地方也能夠考慮本身經過直接內存的調用和釋放來實現,不過要求剩餘的物理內存也足夠大才行,那麼足夠大是多大呢?這個很差說,要看文件自己的大小和訪問的頻率;服務器
其二爲假如內存足夠大,無限制大,那麼此時的限制就是線程,傳統的IO模型是線程是一個請求一個線程,這個線程從主線程從線程池中分配後,就開始工做,通過你的Context包裝、Filter、攔截器、業務代碼各個層次和業務邏輯、訪問數據庫、訪問文件、渲染結果等等,其實整個過程線程都是被掛住的,因此這部分資源很是有限,並且若是是大文件操做是屬於IO密集型的操做,大量的CPU時間是空餘的,方法最直接固然是增長線程數來控制,固然內存足夠大也有足夠的空間來申請線程池,不過通常來說一個進程的線程池通常會受到限制也不建議太多的,而在有限的系統資源下,要提升性能,咱們開始有了new IO技術,也就是NIO技術,新版的裏面又有了AIO技術,NIO只能算是異步IO,可是在中間讀寫過程仍然是阻塞的(也就是在真正的讀寫過程,可是不會去關心中途的響應),還未作到真正的異步IO,在監聽connect的時候他是不須要不少線程參與的,有單獨的線程去處理,鏈接也又傳統的socket變成了selector,對於不須要進行數據處理的是無需分配線程處理的;而AIO經過了一種所謂的回調註冊來完成,固然還須要OS的支持,當會掉的時候會去分配線程,目前還不是很成熟,性能最多和NIO吃平,不過隨着技術發展,AIO必然會超越NIO,目前谷歌V8虛擬機引擎所驅動的node.js就是相似的模式,有關這種技術不是本文的說明重點;
將上面二者結合起來就是要解決大文件,還要並行度,最土的方法是將文件每次請求的大小下降到必定程度,如8K(這個大小是通過測試後網絡傳輸較爲適宜的大小,本地讀取文件並不須要這麼小),若是再作深刻一些,能夠作必定程度的cache,將多個請求的同樣的文件,cache在內存或分佈式緩存中,你不用將整個文件cache在內存中,將近期使用的cache幾秒左右便可,或你能夠採用一些熱點的算法來配合;相似迅雷下載的斷點傳送中(不過迅雷的網絡協議不太同樣),它在處理下載數據的時候未必是連續的,只要最終能合併便可,在服務器端能夠反過來,誰正好須要這塊的數據,就給它就能夠;才用NIO後,能夠支持很大的鏈接和併發,本地經過NIO作socket鏈接測試,100個終端同時請求一個線程的服務器,正常的WEB應用是第一個文件沒有發送完成,第二個請求要麼等待,要麼超時,要麼直接拒絕得不到鏈接,改爲NIO後此時100個請求都能鏈接上服務器端,服務端只須要1個線程來處理數據就能夠,將不少數據傳遞給這些鏈接請求資源,每次讀取一部分數據傳遞出去,不過能夠計算的是,在整體長鏈接傳輸過程當中整體效率並不會提高,只是相對相應和所開銷的內存獲得量化控制,這就是技術的魅力,也許不要太多的算法,不過你得懂他。
相似的數據處理還有不少,有些時候還會將就效率問題,好比在HBase的文件拆分和合並過程當中,要不影響線上業務是比較難的事情,不少問題值得咱們去研究場景,由於不一樣的場景有不一樣的方法去解決,可是大同小異,明白思想和方法,明白內存和體系架構,明白你所面臨的是瀋陽的場景,只是細節上改變能夠帶來驚人的效果。