掃二維碼與項(xiàng)目經(jīng)理溝通
我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
今天在修改前端頁面的時(shí)候,發(fā)現(xiàn)程序中有一個(gè)頁面的加載速度很慢,差不多需要5秒,這其實(shí)是難以接受的,我也不知道為什么上線這么長時(shí)間了,沒人提過這個(gè)事兒。

我記得有一個(gè)詞兒,叫秒開率。
秒開率是指能夠在1秒內(nèi)完成頁面的加載。
查詢的時(shí)候,會(huì)訪問后臺(tái)數(shù)據(jù)庫,查詢前20條數(shù)據(jù),按道理來說,這應(yīng)該很快才對(duì)。
追蹤代碼,看看啥問題,最后發(fā)現(xiàn)問題有三:
大字段批量查詢、批量文件落地、讀取大文件并進(jìn)行網(wǎng)絡(luò)傳輸,不慢才怪,這一頓騷操作,5秒能加載完畢,已經(jīng)燒高香了。
經(jīng)過調(diào)查發(fā)現(xiàn),這個(gè)PDF模板只有在點(diǎn)擊運(yùn)費(fèi)模板按鈕時(shí)才會(huì)使用。
打開代碼一看,居然是通過FileReader讀取的,我了個(gè)乖乖~
這有什么問題嗎?都是從百度拷貝過來的,百度還會(huì)有錯(cuò)嗎?而且也測試了,沒問題啊。
嗯,對(duì),是沒問題,是可以實(shí)現(xiàn)需求,可是,為什么用這個(gè)?不知道。更別說效率問題了~
優(yōu)化4:通過緩沖流讀取文件。
Java I/O (Input/Output) 是對(duì)傳統(tǒng) I/O 操作的封裝,它是以流的形式來操作數(shù)據(jù)的。
在上一篇 《增加索引 + 異步 + 不落地后,從 12h 優(yōu)化到 15 min》中,提到了4種優(yōu)化方式,數(shù)據(jù)庫優(yōu)化、復(fù)用優(yōu)化、并行優(yōu)化、算法優(yōu)化。
其中Buffered緩沖流就屬于復(fù)用優(yōu)化的一種,這個(gè)頁面的查詢完全可以通過復(fù)用優(yōu)化優(yōu)化一下。
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+"";
}通過循環(huán)模擬了150000個(gè)文件進(jìn)行測試,F(xiàn)ileReader耗時(shí)8136毫秒,BufferedReader耗時(shí)6718毫秒,差不多相差1秒半的時(shí)間,差距還是相當(dāng)大的,俗話說得好,水滴石穿。
同樣是read方法,只不過是包了一層,有啥不同呢?
BufferedReader 是一個(gè)緩沖字符輸入流,可以對(duì) FileRead 進(jìn)行包裝,提供了一個(gè)緩存數(shù)組,將數(shù)據(jù)按照一定規(guī)則讀取到緩存區(qū)中,輸入流每次讀取文件數(shù)據(jù)時(shí)都需要將數(shù)據(jù)進(jìn)行字符編碼,而 BufferedReader 的出現(xiàn),降低了輸入流訪問數(shù)據(jù)源的次數(shù),將一定大小的數(shù)據(jù)一次讀取到緩存區(qū)并進(jìn)行字符編碼,從而提高 IO 的效率。
如果沒有緩沖,每次調(diào)用 read() 或 readLine() 都可能導(dǎo)致從文件中讀取字節(jié),轉(zhuǎn)換為字符,然后返回,這可能非常低效。
就像取快遞一樣,在取快遞的時(shí)候,肯定是想一次性的取完,避免再來一趟。
對(duì) FileRead 進(jìn)行包裝變成了BufferedReader緩沖字符輸入流,其實(shí),Java IO流就是最典型的裝飾器模式,裝飾器模式通過組合替代繼承的方式在不改變?cè)碱惖那闆r下添加增強(qiáng)功能,主要解決繼承關(guān)系過于復(fù)雜的問題,之前整理過一篇裝飾器模式,這里就不論述了。
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將緩沖字符數(shù)組設(shè)置的這么小,才8192個(gè)字節(jié)?
這是一個(gè)比較折中的方案,如果緩沖區(qū)太大的話,就會(huì)增加單次讀寫的時(shí)間,同樣內(nèi)存的大小也是有限制的,不可能都讓你來干這個(gè)一件事。
很多小伙伴也肯定用過它的read(char[] cbuf),它內(nèi)部維護(hù)了一個(gè)char數(shù)組,每次寫/讀數(shù)據(jù)時(shí),操作的是數(shù)組,這樣可以減少IO次數(shù)。
傳統(tǒng) IO 執(zhí)行的話需要 4 次上下文切換(用戶態(tài) -> 內(nèi)核態(tài) -> 用戶態(tài) -> 內(nèi)核態(tài) -> 用戶態(tài))和 4 次拷貝。
NIO中比較常用的是FileChannel,主要用來對(duì)本地文件進(jìn)行 IO 操作。
傳統(tǒng)的文件I/O操作可能會(huì)變得很慢,這時(shí)候mmap就閃亮登場了。
mmap(Memory-mapped files)是一種在內(nèi)存中創(chuàng)建映射文件的機(jī)制,它可以使我們像訪問內(nèi)存一樣訪問文件,從而避免頻繁的文件I/O操作。
使用mmap的方式是在內(nèi)存中創(chuàng)建一個(gè)虛擬地址,然后將文件映射到這個(gè)虛擬地址上,這個(gè)映射的過程是由操作系統(tǒng)完成的。
實(shí)現(xiàn)映射后,進(jìn)程就可以采用指針的方式讀寫操作這一段內(nèi)存,系統(tǒng)會(huì)自動(dòng)回寫到對(duì)應(yīng)的文件磁盤上,這樣就完成了對(duì)文件的讀取操作,而不用調(diào)用 read、write 等系統(tǒng)函數(shù)。
內(nèi)核空間對(duì)這段區(qū)域的修改也會(huì)直接反映用戶空間,從而可以實(shí)現(xiàn)不同進(jìn)程間的文件共享。
在 Java 中,mmap 技術(shù)主要使用了 Java NIO (New IO)庫中的 FileChannel 類,它提供了一種將文件映射到內(nèi)存的方法,稱為 MappedByteBuffer。MappedByteBuffer 是 ByteBuffer 的一個(gè)子類,它擴(kuò)展了 ByteBuffer 的功能,可以直接將文件映射到內(nèi)存中。
根據(jù)文件地址創(chuàng)建了一層緩存當(dāng)作索引,放在虛擬內(nèi)存中,使用時(shí)會(huì)根據(jù)的地址,直接找到磁盤中文件的位置,把數(shù)據(jù)分段load到系統(tǒng)內(nèi)存(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();
// 創(chuàng)建一個(gè)與文件大小相同的字節(jié)數(shù)組
ByteBuffer buffer = ByteBuffer.allocate((int) size);
// 將通道上的所有數(shù)據(jù)都讀入到buffer中
while (channel.read(buffer) != -1) {}
// 切換為只讀模式
buffer.flip();
// 從buffer中獲取數(shù)據(jù)并處理
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
ret = new String(data);
} catch (IOException e) {
System.out.println("readFileByMmap異常:" + e);
}
return ret;
}
mmap 是一種內(nèi)存映射技術(shù),mmap 相比于傳統(tǒng)的 緩沖流 來說,其實(shí)就是少了 1 次 CPU 拷貝,變成了數(shù)據(jù)共享。
雖然減少了一次拷貝,但是上下文切換的次數(shù)還是沒變。
因?yàn)榇嬖谝淮蜟PU拷貝,因此mmap并不是嚴(yán)格意義上的零拷貝。
RocketMQ 中就是使用的 mmap 來提升磁盤文件的讀寫性能。
零拷貝將上下文切換和拷貝的次數(shù)壓縮到了極致。
在內(nèi)核的支持下,零拷貝少了一個(gè)步驟,那就是內(nèi)核緩存向用戶空間的拷貝,這樣既節(jié)省了內(nèi)存,也節(jié)省了 CPU 的調(diào)度時(shí)間,讓效率更高。
直接將用戶緩沖區(qū)干掉,而且沒有CPU拷貝,故得名零拷貝。
分享文章:使用懶加載+零拷貝后,程序的秒開率提升至99.99%
文章鏈接:http://uogjgqi.cn/article/cccpicc.html

我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流