因爲最近疫情的影響,相信最近不少小夥伴都忙於線上辦公或者面試😭,筆者這裏分享一道發生在大廠前端線上編程面試中的一道題目,前端
如何讓 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
library數據結構
在不急不慢的分類好目錄結構以後,總得再弄點代碼給面試官瞧瞧吧o(╥﹏╥)o,不能讓人家空等啊app
固然既然是面試用 NodeJS 第三方模塊解決也不夠好,當時是先屢一下用什麼原生模塊實現比較好,要知足上面這些要求,想到這裏能使用到的原生 Node 內置模塊關鍵有以下兩個:
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 萬數據的,咱們使用readline
和createReadStream
二者配合,將數據按必定條數分別緩存在內存中,這裏代碼正如上面提到,因爲提交的代碼不適合太大(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
封裝,方便在外部配合async
和await
使用:
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
代碼中寫入一下代碼,這裏簡單的使用Set
和filter
方法來求交集:
// 交集方法 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
最後若是文章能帶您一絲幫助或者啓發,請不要吝嗇你的贊和收藏,你的確定是我前進的最大動力😁