├── result.txt ├── .gitignore ├── .DS_Store ├── database ├── .DS_Store └── data-3M.txt ├── library ├── intersect.js ├── data-3M.js ├── create-60M.js └── data-60M.js ├── index.js ├── package.json └── README.md /result.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | data60.txt.back -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/intersect/HEAD/.DS_Store -------------------------------------------------------------------------------- /database/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/intersect/HEAD/database/.DS_Store -------------------------------------------------------------------------------- /library/intersect.js: -------------------------------------------------------------------------------- 1 | // 交集方法 2 | module.exports = (a, b) => { 3 | return a.filter(x => new Set(b).has(x)); 4 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const data3M = require('./library/data-3M'); 2 | const data60M = require('./library/data-60M'); 3 | (async () => { 4 | let smallData = await data3M(); 5 | let result = await data60M(smallData); 6 | console.log(result); 7 | })(); -------------------------------------------------------------------------------- /database/data-3M.txt: -------------------------------------------------------------------------------- 1 | 30560852 2 | 37090337 3 | 44045859 4 | 5872712 5 | 5448510 6 | 57472209 7 | 4845684 8 | 22409487 9 | 32975804 10 | 44805079 11 | 30662173 12 | 33165574 13 | 305234 14 | 24691276 15 | 34896452 16 | 21688532 17 | 28387233 18 | 36807140 19 | 52943515 20 | 39405740 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "intersect", 3 | "version": "1.0.0", 4 | "description": "6000万数据包和300万数据包在50M内存使用环境中求交集", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index", 8 | "build": "node ./library/create-60M", 9 | "dev": "cat result.txt" 10 | }, 11 | "author": "", 12 | "license": "ISC" 13 | } 14 | -------------------------------------------------------------------------------- /library/data-3M.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const readline = require('readline'); 3 | module.exports = () => { 4 | return new Promise((resolve) => { 5 | const rl = readline.createInterface({ 6 | input: fs.createReadStream('./database/data-3M.txt'), 7 | crlfDelay: Infinity 8 | }); 9 | let check = []; 10 | rl.on('line', (line) => { 11 | check.push(line); 12 | }); 13 | rl.on('close', () => { 14 | resolve(check) 15 | }) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /library/create-60M.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require('path'); 3 | const writer = fs.createWriteStream(path.resolve(__dirname, '../database/data-60M.txt'), { highWaterMark: 1 }); 4 | 5 | const writeSixtyMillionTimes = (writer) => { 6 | const write = () => { 7 | let data = Buffer.from(`${parseInt(Math.random() * 60000000)}\n`) 8 | let ok = true; 9 | do { 10 | i--; 11 | if (i === 0) { 12 | // 最后一次写入。 13 | writer.write(data); 14 | } else { 15 | // 检查是否可以继续写入。 16 | // 不要传入回调,因为写入还没有结束。 17 | ok = writer.write(data); 18 | } 19 | } while (i > 0 && ok); 20 | if (i > 0) { 21 | // 被提前中止。 22 | // 当触发 'drain' 事件时继续写入。 23 | writer.once('drain', write); 24 | } 25 | } 26 | // 初始化6000万数据 27 | let i = 600000; 28 | write(); 29 | } 30 | 31 | writeSixtyMillionTimes(writer) -------------------------------------------------------------------------------- /library/data-60M.js: -------------------------------------------------------------------------------- 1 | const { createReadStream, appendFile } = require('fs'); 2 | const readline = require('readline'); 3 | const intersect = require('./intersect'); 4 | // 写入结果 5 | const writeResult = (element) => { 6 | appendFile('./result.txt', `${element}\n`, (err) => { 7 | err ? () => console.log('写入成功') : () => console.log('写入失败'); 8 | }) 9 | } 10 | module.exports = (smallData) => { 11 | return new Promise((resolve) => { 12 | const rl = readline.createInterface({ 13 | // 6000条数据流 14 | input: createReadStream('./database/data-60M.txt', { 15 | // 节流阀 16 | highWaterMark: 50 17 | }), 18 | // 处理分隔符 19 | crlfDelay: Infinity 20 | }); 21 | // 缓存次数 22 | let lineCount = 0; 23 | // 缓存容器 24 | let rawData = []; 25 | // 逐行读取 26 | rl.on('line', (line) => { 27 | rawData.push(line); 28 | console.log(line, rawData.length); 29 | lineCount++; 30 | // 限制每一次读取600条数据,分十次读取 31 | if (lineCount === 600) { 32 | console.log('读取次数为:', lineCount); 33 | console.log('取出结果为:', rawData); 34 | // 暂停流 35 | rl.pause(); 36 | // 获取交集 37 | let intersectResult = intersect(rawData, smallData); 38 | // 遍历交集并写入结果 39 | intersectResult.forEach(element => { 40 | writeResult(element) 41 | }); 42 | setTimeout(() => { 43 | // 释放缓存 44 | rawData = []; 45 | // 重置读取次数 46 | lineCount = 0; 47 | // 重启流 48 | rl.resume(); 49 | }, 0) 50 | } 51 | }); 52 | rl.on('close', () => { 53 | resolve('结束'); 54 | }) 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 下载 && 运行 2 | 3 | 下载源代码: 4 | ```bash 5 | git clone https://github.com/Wscats/intersect 6 | ``` 7 | 8 | 使用以下命令运行测试,运行成功后结果会在`result.txt`中展现结果: 9 | ```bash 10 | # 运行 11 | npm start 12 | # 查看结果 13 | npm run dev 14 | # 生成新的大数据 15 | npm run build 16 | ``` 17 | 18 | # 目录结构 19 | 20 | - database 21 | - data-3M.txt - 模拟的3百万数据包 22 | - data-60M.txt - 模拟的6千万数据包 23 | - library 24 | - data-3M.js - 处理3百万数据包的逻辑 25 | - data-60M.js - 处理6千万数据包的逻辑 26 | - intersect.js - 处理数据包的交集 27 | - create-60M.js - 生成大数据的文件 28 | - result.txt 最终数据包的交集结果 29 | - index.js 主逻辑文件 30 | 31 | 理想数据包的数据结构如下: 32 | ``` 33 | QQ:40645253 地址:xxx 年龄:xxx 34 | QQ:49844525 地址:xxx 年龄:xxx 35 | QQ:51053984 地址:xxx 年龄:xxx 36 | QQ:15692967 地址:xxx 年龄:xxx 37 | QQ:39211026 地址:xxx 年龄:xxx 38 | ... 39 | ``` 40 | 理想数据包的内存占用如下: 41 | 42 | |数据量|内存占用| 43 | |-|-| 44 | |6000条数据|>=30KB| 45 | |6000万条数据|>=300.000KB>=300MB| 46 | |300条数据|>=15KB| 47 | |300万条数据|>=150.000KB>=15MB| 48 | 49 | 在50MB的内存限制下,我们可以把300万条约15MB的数据完全放入内存中,剩余大概35MB空间是不允许我们完全放入6000万条约300MB的数据,所以我们需要把数据切割成10块左右,大概每块控制在30MB,然后分别读取出来跟内存中的300万条数据进行比对并求出交集。 50 | 51 | 在 Node 中要满足上面的要求,我们分别需要用到两个关键的内置模块: 52 | 53 | - fs - 文件系统 54 | - readline - 逐行读取 55 | 56 | `fs.createReadStream(path[, options])`方法中,其中 options 可以包括 start 和 end 值,以从文件中读取一定范围的字节而不是整个文件。 start 和 end 都包含在内并从 0 开始计数,这种是方法方便我们分段读取6000万条数据。 57 | 58 | 示例,从一个大小为 100 个字节的文件中读取最后 10 个字节: 59 | ```js 60 | fs.createReadStream('data3M.txt', { start: 90, end: 99 }); 61 | ``` 62 | 63 | 除此之外还可以使用,`fs.createReadStream()` 提供 highWaterMark 选项,它允许我们将以大小等于 highWaterMark 选项的块读取流,highWaterMark 的默认值为: 64 * 1024(即64KB),我们可以根据需要进行调整,当内部的可读缓冲的总大小达到 highWaterMark 设置的阈值时,流会暂时停止从底层资源读取数据,直到当前缓冲的数据被消费,我们就可以触发`readline.pause()`暂停流,处理完之后继续触发`readline.resume()`恢复流,然后不断重复以上步骤,将6000万数据分别处理完。 64 | 65 | readline 模块提供了一个接口,用于一次一行地读取可读流中的数据。 它可以使用以下方式访问,并且我们的数据包,每条数据之间是使用`\n、\r 或 \r\n`隔开,所以这样方便我们使用`readline.on('line', (input) => {})`来接受每一行数据包的字符串。 66 | 67 | # data-60M.js 68 | 69 | 该文件用于专门处理6000万数据,我们使用`readline`和`createReadStream`两者配合,将数据按一定条数分别缓存在内存中,由于提交的代码不适合太大(Git传上去太慢),所以把数据量减少到6000条,那么分成10份的话,每份缓存就需要读600条左右,读完每份数据之后调用`intersect`函数求交集,并存入硬盘`result.txt`文件中,然后释放内存: 70 | 71 | ```js 72 | // 写入结果 73 | const writeResult = (element) => { 74 | appendFile('./result.txt', `${element}\n`, (err) => { 75 | err ? () => console.log('写入成功') : () => console.log('写入失败'); 76 | }) 77 | } 78 | ``` 79 | 这里最关键是要定义一个空的容器`lineCount`来存放每段数据,并且使用`if (lineCount === 600) {}`语句判断内存超过限制的空间后做释放内存的处理: 80 | ```js 81 | const { createReadStream, appendFile } = require('fs'); 82 | const readline = require('readline'); 83 | const intersect = require('./intersect'); 84 | 85 | module.exports = (smallData) => { 86 | return new Promise((resolve) => { 87 | const rl = readline.createInterface({ 88 | // 6000条数据流 89 | input: createReadStream('./database/data60M.txt', { 90 | // 节流阀 91 | highWaterMark: 50 92 | }), 93 | // 处理分隔符 94 | crlfDelay: Infinity 95 | }); 96 | // 缓存次数 97 | let lineCount = 0; 98 | // 缓存容器 99 | let rawData = []; 100 | // 逐行读取 101 | rl.on('line', (line) => { 102 | rawData.push(line); 103 | lineCount++; 104 | // 限制每一次读取600条数据,分十次读取 105 | if (lineCount === 600) { 106 | // 释放内存 107 | // ... 108 | } 109 | ); 110 | rl.on('close', () => { 111 | resolve('结束'); 112 | }) 113 | }) 114 | } 115 | ``` 116 | 释放内存后前需要使用`rl.pause()`暂停流,然后做两步逻辑: 117 | 118 | - 求交集结果 119 | - 写入每段交集结果到硬盘 120 | 121 | 然后需要使用`rl.resume()`重启流: 122 | ```js 123 | if (lineCount === 600) { 124 | // 暂停流 125 | rl.pause(); 126 | // 获取交集 127 | let intersectResult = intersect(rawData, smallData); 128 | // 遍历交集并写入结果 129 | intersectResult.forEach(element => { 130 | writeResult(element) 131 | }); 132 | // 释放缓存 133 | rawData = null; 134 | intersectResult = null; 135 | rawData = []; 136 | // 重置读取次数 137 | lineCount = 0; 138 | // 重启流 139 | rl.resume(); 140 | } 141 | ``` 142 | 143 | # data-3M.js 144 | 145 | 这里的数据由于是3百万,所以可以把全部数据放入内存,这里用Promise封装,方便在外部配合`async`和`await`使用: 146 | ```js 147 | const fs = require('fs'); 148 | const readline = require('readline'); 149 | module.exports = () => { 150 | return new Promise((resolve) => { 151 | const rl = readline.createInterface({ 152 | input: fs.createReadStream('./database/data-3M.txt'), 153 | crlfDelay: Infinity 154 | }); 155 | let check = []; 156 | rl.on('line', (line) => { 157 | check.push(line); 158 | }); 159 | rl.on('close', () => { 160 | resolve(check) 161 | }) 162 | }) 163 | } 164 | ``` 165 | 166 | # intersect.js 167 | 168 | 这里简单的使用`Set`和`filter`方法来求交集: 169 | ```js 170 | // 交集方法 171 | module.exports = (a, b) => { 172 | return a.filter(x => new Set(b).has(x)); 173 | } 174 | ``` 175 | 176 | # index.js 177 | 178 | 这里分别把上面两份处理关键数据的逻辑引入,然后执行逻辑: 179 | ```js 180 | const data3M = require('./library/data-3M'); 181 | const data60M = require('./library/data-60M'); 182 | (async () => { 183 | let smallData = await data3M(); 184 | let result = await data60M(smallData); 185 | console.log(result); 186 | })(); 187 | ``` 188 | 189 | # create-60M.js 190 | 191 | 生成全新的大数据,用于测试: 192 | ```js 193 | const fs = require("fs"); 194 | const path = require('path'); 195 | const writer = fs.createWriteStream(path.resolve(__dirname, '../database/data-60M.txt'), { highWaterMark: 1 }); 196 | 197 | const writeSixtyMillionTimes = (writer) => { 198 | const write = () => { 199 | let data = Buffer.from(`${parseInt(Math.random() * 60000000)}\n`) 200 | let ok = true; 201 | do { 202 | i--; 203 | if (i === 0) { 204 | // 最后一次写入。 205 | writer.write(data); 206 | } else { 207 | // 检查是否可以继续写入。 208 | // 不要传入回调,因为写入还没有结束。 209 | ok = writer.write(data); 210 | } 211 | } while (i > 0 && ok); 212 | if (i > 0) { 213 | // 被提前中止。 214 | // 当触发 'drain' 事件时继续写入。 215 | writer.once('drain', write); 216 | } 217 | } 218 | // 初始化6000万数据 219 | let i = 600000; 220 | write(); 221 | } 222 | 223 | writeSixtyMillionTimes(writer) 224 | ``` 225 | 226 | # 后记 227 | 228 | 15 个月后再回顾,发现跟 VSCode 的这个思路很相似,具体如下 229 | 230 | - [Text Buffer Reimplementation](https://code.visualstudio.com/blogs/2018/03/23/text-buffer-reimplementation) 231 | --------------------------------------------------------------------------------