今天在修改前端頁面的時候,發現程序中有一個頁面的加載速度很慢,差不多需要5秒,這其實是難以接受的,我也不知道為什么上線這么長時間了,沒人提過這個事兒。
我記得有一個詞兒,叫秒開率。
秒開率是指能夠在1秒內完成頁面的加載。
查詢的時候,會訪問后臺數據庫,查詢前20條數據,按道理來說,這應該很快才對。
追蹤代碼,看看啥問題,最后發現問題有三:
大字段批量查詢、批量文件落地、讀取大文件并進行網絡傳輸,不慢才怪,這一頓騷操作,5秒能加載完畢,已經燒高香了。
經過調查發現,這個PDF模板只有在點擊運費模板按鈕時才會使用。
打開代碼一看,居然是通過FileReader讀取的,我了個乖乖~
這有什么問題嗎?都是從百度拷貝過來的,百度還會有錯嗎?而且也測試了,沒問題啊。
嗯,對,是沒問題,是可以實現需求,可是,為什么用這個?不知道。更別說效率問題了~
優化4:通過緩沖流讀取文件。
Java I/O (Input/Output) 是對傳統 I/O 操作的封裝,它是以流的形式來操作數據的。
在上一篇 《增加索引 + 異步 + 不落地后,從 12h 優化到 15 min》中,提到了4種優化方式,數據庫優化、復用優化、并行優化、算法優化。
其中Buffered緩沖流就屬于復用優化的一種,這個頁面的查詢完全可以通過復用優化優化一下。
FileReader連readLine()方法都沒有,我也是醉了~
private static int readFileByReader(String filePath) { int result = 0; try (Reader reader = new FileReader(filePath)) { int value; while ((value = reader.read()) != -1) { result += value; } } catch (Exception e) { System.out.println("readFileByReader異常:" + e); } return result;}
private static String readFileByBuffer(String filePath) { StringBuilder builder = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) { String data = null; while ((data = reader.readLine())!= null){ builder.append(data); } }catch (Exception e) { System.out.println("readFileByReader異常:" + e); } return builder+"";}
通過循環模擬了150000個文件進行測試,FileReader耗時8136毫秒,BufferedReader耗時6718毫秒,差不多相差1秒半的時間,差距還是相當大的,俗話說得好,水滴石穿。
同樣是read方法,只不過是包了一層,有啥不同呢?
BufferedReader 是一個緩沖字符輸入流,可以對 FileRead 進行包裝,提供了一個緩存數組,將數據按照一定規則讀取到緩存區中,輸入流每次讀取文件數據時都需要將數據進行字符編碼,而 BufferedReader 的出現,降低了輸入流訪問數據源的次數,將一定大小的數據一次讀取到緩存區并進行字符編碼,從而提高 IO 的效率。
如果沒有緩沖,每次調用 read() 或 readLine() 都可能導致從文件中讀取字節,轉換為字符,然后返回,這可能非常低效。
就像取快遞一樣,在取快遞的時候,肯定是想一次性的取完,避免再來一趟。
對 FileRead 進行包裝變成了BufferedReader緩沖字符輸入流,其實,Java IO流就是最典型的裝飾器模式,裝飾器模式通過組合替代繼承的方式在不改變原始類的情況下添加增強功能,主要解決繼承關系過于復雜的問題,之前整理過一篇裝飾器模式,這里就不論述了。
public int read(char cbuf[], int off, int len) throws IOException { return in.read(cbuf, off, len);}
private void fill() throws IOException { int dst; if (markedChar <= UNMARKED) { /* No mark */ dst = 0; } else { /* Marked */ int delta = nextChar - markedChar; if (delta >= readAheadLimit) { /* Gone past read-ahead limit: Invalidate mark */ markedChar = INVALIDATED; readAheadLimit = 0; dst = 0; } else { if (readAheadLimit <= cb.length) { /* Shuffle in the current buffer */ System.arraycopy(cb, markedChar, cb, 0, delta); markedChar = 0; dst = delta; } else { /* Reallocate buffer to accommodate read-ahead limit */ char ncb[] = new char[readAheadLimit]; System.arraycopy(cb, markedChar, ncb, 0, delta); cb = ncb; markedChar = 0; dst = delta; } nextChar = nChars = delta; } } int n; do { n = in.read(cb, dst, cb.length - dst); } while (n == 0); if (n > 0) { nChars = dst + n; nextChar = dst; }}
核心方法fill():
既然緩沖這么好用,為啥jdk將緩沖字符數組設置的這么小,才8192個字節?
這是一個比較折中的方案,如果緩沖區太大的話,就會增加單次讀寫的時間,同樣內存的大小也是有限制的,不可能都讓你來干這個一件事。
很多小伙伴也肯定用過它的read(char[] cbuf),它內部維護了一個char數組,每次寫/讀數據時,操作的是數組,這樣可以減少IO次數。
傳統 IO 執行的話需要 4 次上下文切換(用戶態 -> 內核態 -> 用戶態 -> 內核態 -> 用戶態)和 4 次拷貝。
NIO中比較常用的是FileChannel,主要用來對本地文件進行 IO 操作。
傳統的文件I/O操作可能會變得很慢,這時候mmap就閃亮登場了。
mmap(Memory-mapped files)是一種在內存中創建映射文件的機制,它可以使我們像訪問內存一樣訪問文件,從而避免頻繁的文件I/O操作。
使用mmap的方式是在內存中創建一個虛擬地址,然后將文件映射到這個虛擬地址上,這個映射的過程是由操作系統完成的。
實現映射后,進程就可以采用指針的方式讀寫操作這一段內存,系統會自動回寫到對應的文件磁盤上,這樣就完成了對文件的讀取操作,而不用調用 read、write 等系統函數。
內核空間對這段區域的修改也會直接反映用戶空間,從而可以實現不同進程間的文件共享。
在 Java 中,mmap 技術主要使用了 Java NIO (New IO)庫中的 FileChannel 類,它提供了一種將文件映射到內存的方法,稱為 MappedByteBuffer。MappedByteBuffer 是 ByteBuffer 的一個子類,它擴展了 ByteBuffer 的功能,可以直接將文件映射到內存中。
根據文件地址創建了一層緩存當作索引,放在虛擬內存中,使用時會根據的地址,直接找到磁盤中文件的位置,把數據分段load到系統內存(pagecache)中。
public static String readFileByMmap(String filePath) { File file = new File(filePath); String ret = ""; StringBuilder builder = new StringBuilder(); try (FileChannel channel = new RandomAccessFile(file, "r").getChannel()) { long size = channel.size(); // 創建一個與文件大小相同的字節數組 ByteBuffer buffer = ByteBuffer.allocate((int) size); // 將通道上的所有數據都讀入到buffer中 while (channel.read(buffer) != -1) {} // 切換為只讀模式 buffer.flip(); // 從buffer中獲取數據并處理 byte[] data = new byte[buffer.remaining()]; buffer.get(data); ret = new String(data); } catch (IOException e) { System.out.println("readFileByMmap異常:" + e); } return ret;}
mmap 是一種內存映射技術,mmap 相比于傳統的 緩沖流 來說,其實就是少了 1 次 CPU 拷貝,變成了數據共享。
雖然減少了一次拷貝,但是上下文切換的次數還是沒變。
因為存在一次CPU拷貝,因此mmap并不是嚴格意義上的零拷貝。
RocketMQ 中就是使用的 mmap 來提升磁盤文件的讀寫性能。
零拷貝將上下文切換和拷貝的次數壓縮到了極致。
在內核的支持下,零拷貝少了一個步驟,那就是內核緩存向用戶空間的拷貝,這樣既節省了內存,也節省了 CPU 的調度時間,讓效率更高。
直接將用戶緩沖區干掉,而且沒有CPU拷貝,故得名零拷貝。
本文鏈接:http://www.www897cc.com/showinfo-26-60995-0.html使用懶加載 + 零拷貝后,程序的秒開率提升至99.99%
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com