大廠前端面試分享:如何讓6000萬數據包和300萬數據包在僅50M內存環境中求交集

因爲最近疫情的影響,相信最近不少小夥伴都忙於線上辦公或者面試😭,筆者這裏分享一道發生在大廠前端線上編程面試中的一道題目,前端

如何讓 6000 萬數據包和 300 萬數據包在僅 50M 內存環境中求交集,請簡單說出您解決這問題的思路

咱們假設如今有兩份龐大的數據,而這兩份數據包的數據結構均以下,仔細觀察裏面的數據咱們不難發現,裏面有 QQ 號,地址和年齡,如題目的要求咱們須要是求交集,因此咱們暫時能夠忽略地址和年齡,以 QQ 號做爲惟一標識:git

QQ:40645253 地址:xxx 年齡:xxx
QQ:49844525 地址:xxx 年齡:xxx
QQ:51053984 地址:xxx 年齡:xxx
QQ:15692967 地址:xxx 年齡:xxx
QQ:39211026 地址:xxx 年齡:xxx
// 如下省略 6000 萬條數據
...

梳理了上面的數據包結構以後,咱們就得看看 50M 內存是什麼狀況了,因爲面試在線上進行,只能短期在本地測試下上面這個數據量在本地會佔有有多大空間,那因爲限因而場前端面試,因此筆者選用了 NodeJS 去製造這些龐大的數據了,當時線上編寫的時候是沒註釋的,這裏爲了方便小夥伴理解,在寫這篇文章的時候我自覺加上了😁github

const fs = require("fs");
const path = require('path');
const writer = fs.createWriteStream(path.resolve(__dirname, 'data-60M.txt'), { highWaterMark: 1 });

const writeSixtyMillionTimes = (writer) => {
    const write = () => {
        let data = Buffer.from(`${parseInt(Math.random() * 60000000)}\n`)
        let ok = true;
        do {
            i--;
            if (i === 0) {
                // 最後一次寫入。
                writer.write(data);
            } else {
                // 檢查是否能夠繼續寫入。 
                // 不要傳入回調,由於寫入尚未結束。
                ok = writer.write(data);
            }
        } while (i > 0 && ok);
        if (i > 0) {
            // 被提早停止。
            // 當觸發 'drain' 事件時繼續寫入。
            writer.once('drain', write);
        }
    }
    // 初始化6000萬數據
    let i = 600000;
    write();
}

writeSixtyMillionTimes(writer)

首先在本地新建一份 data-60M.txt 文件,而後新建一份 data-60M.js 把上面代碼寫入並執行,這裏我最主要是使用了一個遞歸,因爲當時爲了快速寫入文件測試大小,當時模擬的 QQ 號,是使用 ${parseInt(Math.random() * 60000000)}\n 隨機數生成的,在實際測試表現中會有極小概率重複 QQ 號,但影響不大,實際影響最大的是當你在生成 6000 萬數據的時候,電腦瘋狂運做,運氣很差的時候直接卡死,而後遠程面試直接斷線。面試

這裏筆者仍是建議小夥伴在遠程面試中手機隨時待命,當電腦 GG 的時候還能聯繫面試官搶救一下,通過與電腦性能的多輪鬥爭,和麪試官尷尬重連的狀況下,終於發現實際文件大小規律以下,這裏爲了方便小夥伴調試源代碼,我也把代碼傳到 Github 上,連接放在最後,只放了 6000 條數據,實際 300MB 實在是傳不上去了,小夥伴們能夠下載源代碼自行測試,祈禱電腦能熬過去o(╥﹏╥)o:npm

數據量 內存佔用
6000條數據(Git版本) >=30KB
6000萬條數據(實際版本) >=300MB
300條數據(Git版本 >=15KB
300萬條數據(實際版本) >=15MB

到了這個地方,筆者漸漸地看出這個問題的坑點可能在那裏了,6000 萬條數據在測試文件中大小大概在 300MB 左右,而 300 萬條數據大小大概在 15 MB,在 50MB 的內存限制下,咱們能夠把 300 萬條約 15MB 的數據徹底放入內存中,剩餘大概 35MB 空間是不容許咱們徹底放入 6000 萬條約 300MB 的數據,因此咱們須要把數據切割成10塊左右,大概每塊控制在 30MB ,而後分別讀取出來跟內存中的 300 萬條數據進行比對並求出交集,50MB 狀況也太極端苛刻了,難道是手機而且仍是老人機嗎,我也不敢問啊o(╥﹏╥)o編程

在思考上面這一連串的邏輯時候,爲了避免耽誤面試官寶貴的時間,邊想邊隨手創建好下面幾份文件和文件夾,好梳理代碼,給本身思考時間和迴旋的餘地,如下是我當時臨時的目錄結構。緩存

  • databasebash

    • data-3M.txt - 模擬的3百萬數據包
    • data-60M.txt - 模擬的6千萬數據包
  • library數據結構

    • data-3M.js - 處理3百萬數據包的邏輯
    • data-60M.js - 處理6千萬數據包的邏輯
    • intersect.js - 處理數據包的交集
    • create-60M.js - 生成大數據的文件
  • result.txt 最終數據包的交集結果
  • index.js 主邏輯文件

在不急不慢的分類好目錄結構以後,總得再弄點代碼給面試官瞧瞧吧o(╥﹏╥)o,不能讓人家空等啊app

固然既然是面試用 NodeJS 第三方模塊解決也不夠好,當時是先屢一下用什麼原生模塊實現比較好,要知足上面這些要求,想到這裏能使用到的原生 Node 內置模塊關鍵有以下兩個:

  • fs - 文件系統
  • readline - 逐行讀取

fs.createReadStream(path[, options])方法中,其中 options 能夠包括 start 和 end 值,以從文件中讀取必定範圍的字節而不是整個文件。 start 和 end 都包含在內並從 0 開始計數,這種是方法方便咱們分段讀取 6000 萬條數據。當時快速寫了一個示例去驗證,從一個大小爲 100 個字節的文件中讀取最後 10 個字節:

fs.createReadStream('data-60M.txt', { start: 90, end: 99 });

通過短暫的測試,發現這個方案可行,心理淡定了一點。

起碼這個備用方案起碼能夠給本身留點操做空間,但當時討論到的是下面這種方案:

fs.createReadStream() 提供 highWaterMark 選項,它容許咱們將以大小等於 highWaterMark 選項的塊讀取流,highWaterMark 的默認值爲: 64 * 1024(即64KB),咱們能夠根據須要進行調整,當內部的可讀緩衝的總大小達到 highWaterMark 設置的閾值時,流會暫時中止從底層資源讀取數據,直到當前緩衝的數據被消費,咱們就能夠觸發readline.pause()暫停流,處理完以後繼續觸發readline.resume()恢復流,而後不斷重複以上步驟,將 6000 萬數據分別處理完。

readline 模塊提供了一個接口,用於一次一行地讀取可讀流中的數據。 它可使用如下方式訪問,而且咱們的數據包,每條數據之間是使用\n、\r 或 \r\n隔開,因此這樣方便咱們使用readline.on('line', (input) => {})來接受每一行數據包的字符串。

這裏自我感受有些丟分項,是當時忘記了 fs.createReadStream 裏面一些配置項,在現場臨時翻閱 NodeJS 的官方 API 文檔,這裏很是感謝當時面試官的理解(^▽^)

下面,咱們就要寫最關鍵的代碼了,就是如何處理那 6000 萬條數據,打開剛纔新建好的data-60M.js文件,該文件就是用於專門處理 6000 萬數據的,咱們使用readlinecreateReadStream二者配合,將數據按必定條數分別緩存在內存中,這裏代碼正如上面提到,因爲提交的代碼不適合太大(Git傳上去太慢),因此把數據量減小到 6000 條,那麼分紅 10 份的話,每份緩存就須要讀 600 條左右,讀完每份數據以後調用intersect函數求交集,並存入硬盤result.txt文件中,而後釋放內存:

// 寫入結果
const writeResult = (element) => {
    appendFile('./result.txt', `${element}\n`, (err) => {
        err ? () => console.log('寫入成功') : () => console.log('寫入失敗');
    })
}

這裏最關鍵是要定義一個空的容器lineCount來存放每段數據,而且使用if (lineCount === 6000) {}語句判斷內存超過限制的空間後作釋放內存的處理:

const { createReadStream, appendFile } = require('fs');
const readline = require('readline');
const intersect = require('./intersect');

module.exports = (smallData) => {
    return new Promise((resolve) => {
        const rl = readline.createInterface({
            // 6000條數據流
            input: createReadStream('./database/data60M.txt', {
                // 節流閥
                highWaterMark: 50
            }),
            // 處理分隔符
            crlfDelay: Infinity
        });
        // 緩存次數
        let lineCount = 0;
        // 緩存容器
        let rawData = [];
        // 逐行讀取
        rl.on('line', (line) => {
            rawData.push(line);
            lineCount++;
            // 限制每一次讀取6000條數據,分十次讀取
            if (lineCount === 6000) {
                // 釋放內存
                // ...
            }
        );
        rl.on('close', () => {
            resolve('結束');
        })
    })
}

釋放內存後前須要使用rl.pause()暫停流,而後作兩步邏輯:

  • 求交集結果
  • 寫入每段交集結果到硬盤

而後須要使用rl.resume()重啓流:

if (lineCount === 6000) {
    // 暫停流
    rl.pause();
    // 獲取交集
    let intersectResult = intersect(rawData, smallData);
    // 遍歷交集並寫入結果
    intersectResult.forEach(element => {
        writeResult(element)
    });
    // 釋放緩存
    rawData = null;
    intersectResult = null;
    rawData = [];
    // 重置讀取次數
    lineCount = 0;
    // 重啓流
    rl.resume();
}

上面這個過程實際很是耗時和緩慢,這裏建議就是在代碼長時間運行的時候不要冷落了面試官o(╥﹏╥)o

在處理完上面 6 千萬數據以後,就剩下 3百萬份的了,這裏的數據因爲是3百萬,因此能夠把所有數據放入內存,咱們在data-3M.js寫入如下代碼,這裏用Promise封裝,方便在外部配合asyncawait使用:

const fs = require('fs');
const readline = require('readline');
module.exports = () => {
    return new Promise((resolve) => {
        const rl = readline.createInterface({
            input: fs.createReadStream('./database/data-3M.txt'),
            crlfDelay: Infinity
        });
        let check = [];
        rl.on('line', (line) => {
            check.push(line);
        });
        rl.on('close', () => {
            resolve(check)
        })
    })
}

而後就是交集了,在intersect.js代碼中寫入一下代碼,這裏簡單的使用Setfilter方法來求交集:

// 交集方法
module.exports = (a, b) => {
    return a.filter(x => new Set(b).has(x));
}

分別把上面兩份處理關鍵數據在index.js邏輯引入,而後執行邏輯,就能夠乖乖地等待面試官的檢閱和指導了:

const data3M = require('./library/data-3M');
const data60M = require('./library/data-60M');
(async () => {
    let smallData = await data3M();
    let result = await data60M(smallData);
    console.log(result);
})();

這裏附上源代碼地址方便小夥伴測試:https://github.com/Wscats/intersect

也使用如下命令運行測試,運行成功後結果會在result.txt中展示結果:

git clone https://github.com/Wscats/intersect
# 運行
npm start
# 查看結果
npm run dev
# 生成新的大數據
npm run build

最後若是文章能帶您一絲幫助或者啓發,請不要吝嗇你的贊和收藏,你的確定是我前進的最大動力😁

相關文章
相關標籤/搜索