当前位置: 首页 > 后端技术 > Node.js

大厂面试题分享:如何在只有50M内存的环境下让6000万数据包和300万数据包相交

时间:2023-04-03 12:21:30 Node.js

由于最近疫情的影响,相信很多小伙伴最近都忙于线上办公或者面试?,笔者在这里分享一个大厂前端在线编程面试中发生的一个问题。如何在只有50M内存的环境下对6000万数据包和300万数据包进行交集。两个庞大的数据,而这两个数据包的数据结构如下,仔细观察里面的数据,我们不难发现,里面有QQ号,地址,年龄,如题要求,需要求交集,所以我们可以暂时忽略地址和年龄,使用QQ号作为唯一标识:QQ:40645253地址:xxx年龄:xxxQQ:49844525地址:xxx年龄:xxxQQ:51053984地址:xxx年龄:xxxQQ:15692967地址:xxx年龄:xxxQQ:39211026地址:xxxAge:xxx//下面省略了6000万条数据...整理完上面的数据包结构,我们就得看看50M内存是个什么样子了。由于是线上面试,所以我们只能在本地进行短时间的测试,这样的数据量在本地会占用多少空间呢?由于限于前端面试,笔者选择了NodeJS来创建这些庞大的数据。当时网上写的时候没有评论。这里为了方便小伙伴理解,写这篇文章的时候有意识地加上了?constfs=require("fs");constpath=require('path');constwriter=fs.createWriteStream(path.resolve(__dirname,'data-60M.txt'),{highWaterMark:1});constwriteSixtyMillionTimes=(writer)=>{constwrite=()=>{letdata=Buffer.from(`${parseInt(Math.random()*60000000)}\n`)letok=true;做{我--;if(i===0){//最后一个接下来写writer.write(data);}else{//检查是否可以继续写入。//不要传入回调,因为写入还没有完成。ok=writer.write(数据);}}while(i>0&&ok);if(i>0){//提前中止。//当'drain'事件触发时继续写入。writer.once('drain',write);}}//初始化6000万条数据leti=600000;write();}writeSixtyMillionTimes(writer)先在本地新建一个data-60M.txt文件,然后新建一个data-60M.js写入并执行上面的代码,这里我主要使用递归,因为当时为了快速写入文件测试大小,当时模拟的QQ号是使用${parseInt(Math.random()*60000000)}\n随机数生成的,在实际测试性能中,会有一个QQ号重复的几率很小,但影响不大。实际的影响是,当你在生成6000万数据的时候,电脑在疯狂运转,运气不好的时候,直接卡死,然后远程面试就直接断线了。在此,笔者还是建议远程面试时,手机随时待命。电脑GG的时候可以联系面试官解救。经过与计算机性能的多轮斗争以及与面试官尴尬的重新联系之后,我终于找到了实际文件。尺寸规则如下。这里为了方便小伙伴调试源码,我也把代码上传到了Github。链接放在最后,只放了6000条数据。实际300MB实在是上传不来。小伙伴们可以下载源码自测,祈祷电脑能活下来o(╥﹏╥)o:数据量内存占用6000条数据(Git版)>=30KB60万条数据(实际版)>=300MB300条数据(Git版)>=15KB300万条数据(实际版本)>=15MB到了这个地方,笔者逐渐看出这个问题的坑可能就在那里,测试文件中6000万条数据的大小约为300MB,而这300万条数据的大小大约是15MB,在50MB的内存限制下,我们可以把300万条大约15MB的数据完全放入内存,剩下的35MB空间是不允许我们完全放入6000万条的大约300MB的数据,所以我们需要把数据切割成10块,每块大约30MB,然后读出来和内存中的300万条数据进行比较,找到交集。50MB的情况太苛刻了,是不是手机还是老人?不敢问o(╥﹏╥)o在思考上面这一系列逻辑的时候,为了不浪费面试官宝贵的时间,我创建了如下文件和文件夹,同时思考,让自己整理代码,自己思考有了时间和回旋余地,以下是我当时的临时目录结构。databasedata-3M.txt-模拟300万个数据包data-60M.txt-模拟6000万个数据包librarydata-3M.js-处理300万个数据包的逻辑data-60M.js-处理6000万个数据包的逻辑intersect.js-处理数据包交集create-60M.js-生成大数据文件result.txt最终数据包交集结果index.js主要逻辑文件归类到目录结构不着急,得弄点代码展示一下面试官o(╥﹏╥)o,总不能让人白等吧。当然,既然是NodeJS第三方模块的面试,还不够好。那时,是时候检查使用什么本机模块了。更好的是,为了满足上述需求,这里可以使用的原生Node的关键内置模块如下:fs-文件系统readline-逐行读取fs.createReadStream(path[,options])方法,其中options您可以包括起始值和结束值以从文件中读取一个字节范围而不是整个文件。start和end都包括在内,从0开始计数。这种方式方便我们分段读取6000万条数据。当时赶紧写了个例子验证一下,从一个100字节大小的文件中读取最后10个字节:fs.createReadStream('data-60M.txt',{start:90,end:99});经过短暂的试验,我发现这个方案是可行的,我稍微冷静了下来。至少这种替代方案至少可以留出一些操作空间,但当时讨论了以下解决方案:fs.createReadStream()提供了highWaterMark选项,它允许我们读取大小等于highWaterMark选项的块的流,而highWaterMark的默认值为:64*1024(即64KB),我们可以根据需要进行调整,当内部可读缓冲区的总大小达到highWaterMark设置的阈值时,流会暂时停止从底层读取数据资源直到当前buffered的数据被消耗完后,我们可以触发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将数据组合成一定数量的item分别缓存在内存中。这里的代码如上所述。由于提交的代码不适合太大(Git上传太慢),所以数据量减少到6000,如果分成10份,每份缓存需要读取600条左右,读取后每一条数据,调用intersect函数求交集,保存在硬盘的result.txt文件中,然后释放内存://writetheresultconstwriteResult=(element)=>{appendFile('./result.txt',`${element}\n`,(err)=>{err?()=>console.log('写入成功'):()=>console.log('写入失败');})}这里最重要的是定义一个空容器lineCount来存放每条数据,通过if(lineCount===6000){}语句判断内存超出限制空间然后释放内存:const{createReadStream,appendFile}=require('fs');constreadline=require('readline');constintersect=require('./intersect');module.exports=(smallData)=>{returnnewPromise((resolve)=>{constrl=readline.createInterface({//6000个数据流input:createReadStream('./database/data60M.txt',{//节流阀highWaterMark:50}),//处理分隔符crlfDelay:Infinity});//缓存数量letlineCount=0;//缓存容器letrawData=[];//逐行读取rl。on('line',(line)=>{rawData.push(line);lineCount++;//限制每次读取6000条数据,读取十次if(lineCount===6000){//释放内存//...});rl.on('close',()=>{resolve('end');})})}在释放内存之前需要使用rl.pause()暂停流,然后执行两步逻辑:写将交集结果写入硬盘,然后使用rl.resume()重新启动流程:if(lineCount===6000){//暂停流程rl.pause();//获取交集letintersectResult=intersect(rawData,smallData);//遍历交集并写入结果intersectResult.forEach(element=>{writeResult(element)});//释放缓存rawData=null;相交结果=空;原始数据=[];//重置读取次数lineCount=0;//restartthestreamrl.resume();}上面的过程其实很耗电时间又慢,这里的建议是代码运行时间长了不要忽视面试官o(╥﹏╥)o处理完上面6000万条数据,只剩下300万份了。是300万,所以所有的数据都可以放到内存中。我们在data-3M.js中编写如下代码,用Promise封装,方便外部配合async和await使用:constfs=require('fs');constreadline=require('readline');module.exports=()=>{returnnewPromise((resolve)=>{constrl=readline.createInterface({input:fs.createReadStream('./database/data-3M.txt'),crlfDelay:Infinity});让check=[];rl.on('line',(line)=>{check.push(line);});rl.on('close',()=>{resolve(check)})})}然后就是交集,把代码写在intersect.js代码里,这里简单的使用Set和filter方法求交集://intersectionmethodmodule.exports=(a,b)=>{returna.filter(x=>newSet(b).has(x));}将以上两个处理关键数据分别导入到index.js逻辑中,然后执行逻辑,就可以乖乖等待面试官的审核和指导:constdata3M=require('./library/data-3M');constdata60M=require('./library/data-60M');(async()=>{letsmallData=awaitdata3M();letresult=awaitdata60M(小数据);控制台日志(结果);})();这里附上源码地址,方便小伙伴测试:https://github.com/Wscats/intersect同样使用如下命令运行测试,运行成功后的结果会显示在result中.txt:gitclonehttps://github.com/Wscats/intersect#运行npmstart#查看结果npmrundev#生成新的大数据npmrunbuild最后,如果文章能给你一点帮助或者启发,请不要吝啬你的点赞和收藏,你的肯定就是我前进的最大动力?笔记本原文https://github.com/Wscats/intersect阅读更多往期优质文章,可以移步我的GitHub查看