当前位置: 首页 > news >正文

数据库存储大量的json文件怎么样高效的读取和分页,利用文件缓存办法不占用内存

利用文件缓存的优势:

  1. 内存友好:使用文件流处理,不将整个大文件加载到内存
  2. 可扩展性:支持超大型JSON文件(GB级别)
  3. 缓存效率:文件缓存可以持久化,服务器重启后仍然有效
  4. 并发安全:每个分页请求生成独立的缓存文件
  5. 灵活性:可以根据需要调整缓存策略和分页大小

首先假设我的数据库里面有大量的json文件,我们现在把导出来,用json格式导出到一个文件内

const fs=require('fs');
const ClientDB=require('../db/db');
const db=new ClientDB('employees','mylog');
const path=require('path');async function testBigJson(){try{const docs=await db.find({});if(docs===false){return;}//获取临时文件名const outputFile=getRandomFileName();//存储临时文件路径const outfile=path.join(__dirname,'..','public',outputFile);//创建文件写入流const writeableStream=fs.createWriteStream(outfile);//我们存储的在数组中,首先写[writeableStream.write('[');let isFile=true;docs.forEach((doc)=>{if(!isFile){writeableStream.write(",\n");}//将我们要导出的字段写成对象,再用json.stringify()写成json格式const formatid=doc.i;const formatusername=doc.username;const formatage=doc.age;const formatcreated=doc.created;const result={id:formatid,username:formatusername,age:formatage,createAt:formatcreated};writeableStream.write(JSON.stringify(result));isFile=false;});writeableStream.write(']');writeableStream.end();//这里是返回临时文件名称,注意不是返回json数据return outputFile;}finally{await db.disconnect();}
}//生成临时的随机json文件
function getRandomFileName(){return `output_${Math.random().toString(36).substring(2,9)}.json`
}testBigJson()
.then(console.log());

第2步使用内存映射(mmap)和缓冲区操作,真正高效的文件读取方案

const fs=require('fs');
const {promisify}=require('util');
const open=promisify(fs.open);
const read=promisify(fs.read);
const close=promisify(fs.close);
const stat=promisify(fs.stat);class HighJsonPaginator{constructor(filePath){//json文件名路径this.filePath=filePath;this.fd=null;this.fileSize=0;this.buffer=Buffer.alloc(64*1024);  //64KB缓存区 }//初始化文件,先获取文件句柄,再获取文件属性,取得文件大小async init(){this.fd=await open(this.filePath,'r');const stats=await stat(this.filePath); //返回文件所有属性this.fileSize=stats.size;}async close(){if(this.fd){await close(this.fd);}}//快速查找json数组的起始和结束位置async findArrayBounds(){const satrtBuffer=Buffer.alloc(1024);const endBuffer=Buffer.alloc(1024);//读取文件开头寻找'['//startBuffer指定的缓冲区,存放后面读取的内容//第1个0为写入缓冲区的位置,先写入缓存再读取内容,1024为读取文件字节的长度//最后的0为读取文件的开始位置await read(this.fd,satrtBuffer,0,1024,0);const startIndex=satrtBuffer.indexOf('[');//读取文件末尾寻找']'await read(this.fd,endBuffer,0,1024,this.fileSize-1024);const endIndex=endBuffer.indexOf(']');return{dataStart:startIndex+1, //跳过'['dataEnd:this.fileSize - (1024-endIndex) //找到']'的位置};};//高效分页读取  使用二进制搜索async paginateEfficiently(page=1,pageSize=10){const bounds=await this.findArrayBounds();const totalBytes=bounds.dataEnd - bounds.dataStart;//创建索引(如果不存在)const index=await this.createOrLoadIndex(bounds);const startItem=(page-1)*pageSize;const endItem=startItem + pageSize;//这里表示小于分页需要的数量,这里是10个,数据就全返回if(startItem >=index.positions.length){return {data:[],pagination:{page,pageSize,total:index.positions.length}};}const items=[];for(let i=startItem;i<Math.min(endItem,index.positions.length);i++){//提取单个json数据,即{}这样一对数据const position=index.positions[i];//然后用readJsonItem()读取转换为对象const item=await this.readJsonItem(position.start,position.end); //这个start/end位置在下面createOrLoadIndex()的循环中//将单个对象加入数组if(item) items.push(item);}return{data:items,pagination:{page,pageSize,total:index.positions.length,totalPages:Math.ceil(index.positions.length / pageSize)}}}//创建或加载索引文件async createOrLoadIndex(bounds){const indexFile=this.filePath +'.index';try{//尝试读取现有索引const indexData=await fs.promises.readFile(indexFile,'utf8');return JSON.parse(indexData);}catch{//创建新索引const index={positions:[]};let position=bounds.dataStart;let depth=0; //尝试let itemStart=position; //项目开始位置while(position < bounds.dataEnd){const bytesToRead=Math.min(this.buffer.length,bounds.dataEnd-position); //选择最小那个值 const {bytesRead}=await read(this.fd,this.buffer,0,bytesToRead,position);for(let i=0;i<bytesRead;i++){const char=String.fromCharCode(this.buffer[i]);if(char==='{'){if(depth===0) itemStart=position+i;depth++;}else if(char==='}'){depth--;if(depth===0){index.positions.push({start:itemStart,end:position+i+1});}}else if(char==='[' || char===']'){//跳过数组括号continue;}}//这里相当于移动指针,比如btyesRead实际读取到字节数为50字节//处理完这50字节后,我们需要的移动指针,//position 从0->50,下一次read(this.fd,this.buffer,0,bytesToRead,position);里面的position就从50开始了position+=bytesRead; }//保存索引await fs.promises.writeFile(indexFile,JSON.stringify(index));return index;}}//读取单个Json项目async readJsonItem(start,end){const length =end-start;const buffer = Buffer.alloc(length);await read(this.fd,buffer,0,length,start);try{return JSON.parse(buffer.toString('utf-8'));}catch(error){console.error('解析json失败:',error);return null;}}
}module.exports =HighJsonPaginator;

下面就是测试,比用readline=require(‘readline’)按行读取json文件快10-100倍,读取1000个文档就在30毫秒左右,看你是什么硬盘,读100个以下几乎就是几毫秒

const HighJsonPaginator=require('./highJsonPaginator');async function benchmark() {const testFile = '../public/output_vzqfri8.json';console.time('高效二进制读取');const efficientPaginator = new HighJsonPaginator(testFile);await efficientPaginator.init();const result=await efficientPaginator.paginateEfficiently(1, 30);await efficientPaginator.close();console.timeEnd('高效二进制读取');console.log(result);
}benchmark();

到这里就结束了,如果你前端要调用读取分页可以直接用第2步的HighJsonPaginator{}类,
如果你想要再中间写一个缓存也是可以的,下面的方案只是作为参考

首先创建一个文件映射,遮避真实json文件名
我这里是直接写进fileMap中,你在用的时候可以把这个文件导入第1个代码中,将返回的临时文件中用set设置进这个fileMap中

// config/files-map.js
const fileMap = new Map();// 将真实文件名映射为随机ID
fileMap.set('output_vzqfri8.json', {id: 'a1b2c3d4e5',name: 'large-data.json',path: './data/output_vzqfri8.json',accessible: true
});fileMap.set('sensitive-data.json', {id: 'f6g7h8i9j0', name: 'sensitive-data.json',path: './data/sensitive-data.json',accessible: false // 限制访问
});// 通过ID获取文件信息
function getFileById(fileId) {for (const [key, value] of fileMap.entries()) {if (value.id === fileId) {return value;}}return null;
}// 通过名称获取文件信息
function getFileByName(filename) {return fileMap.get(filename);
}module.exports = { fileMap, getFileById, getFileByName };

第2步前端的调用,这里只写了个大概

const { getFileById } = require('../config/files-map');router.get('/api/optimized-data/:fileId', async (req, res) => {try {const { fileId } = req.params;const { page = '1', pageSize = '20' } = req.query;// 通过ID获取文件信息const fileInfo = getFileById(fileId);if (!fileInfo) {return res.status(404).json({ error: '文件不存在或无权访问' });}if (!fileInfo.accessible) {return res.status(403).json({ error: '无权访问该文件' });}const pageNum = Math.max(1, parseInt(page));const pageSizeNum = Math.min(Math.max(1, parseInt(pageSize)), 200);const cacheKey = `data:${fileId}:${pageNum}:${pageSizeNum}`;// 使用示例:/api/optimized-data/a1b2c3d4e5?page=2&pageSize=50// 用户看不到真实的文件名 large-data.json// ... 其余处理逻辑const result = await processFile(fileInfo.path, pageNum, pageSizeNum);res.json(result);} catch (error) {console.error('文件处理错误:', error);res.status(500).json({ error: '内部服务器错误' });}
});

或者用固定文件名,或jwt令牌也可以的

// 使用固定的文件标识,不暴露真实文件名
const allowedFiles = {'dataset-1': './data/large-data.json','report-2024': './data/sensitive-report.json','user-stats': './data/user-statistics.json'
};router.get('/api/optimized-data/:fileKey', async (req, res) => {try {const { fileKey } = req.params;if (!allowedFiles[fileKey]) {return res.status(404).json({ error: '文件不存在' });}const filePath = allowedFiles[fileKey];// 使用示例:/api/optimized-data/dataset-1?page=2&pageSize=50// 用户看到的是 dataset-1,而不是 large-data.json// ... 处理逻辑} catch (error) {// 错误处理}
});

下面是文件缓存管理

const crypto = require('crypto');
const fs = require('fs').promises;
const path = require('path');class OptimizedFileCache {constructor(cacheDir = './cache') {this.cacheDir = cacheDir;this.indexCache = new Map(); // 内存缓存索引}async getCache(key, generator) {const cachePath = this.getCachePath(key);try {// 检查缓存是否存在且新鲜const cachedData = await this.readCache(cachePath);if (cachedData) {return cachedData;}} catch (error) {// 缓存不存在或损坏}// 生成新数据并缓存const data = await generator();await this.writeCache(cachePath, data);return data;}getCachePath(key) {const hash = crypto.createHash('sha256').update(key).digest('hex');return path.join(this.cacheDir, `${hash}.cache`);}async readCache(filePath) {try {const [data, stats] = await Promise.all([fs.readFile(filePath, 'utf8'),fs.stat(filePath)]);// 检查缓存是否过期(1小时)if (Date.now() - stats.mtimeMs > 3600000) {return null;}return JSON.parse(data);} catch {return null;}}async writeCache(filePath, data) {await fs.mkdir(path.dirname(filePath), { recursive: true });await fs.writeFile(filePath, JSON.stringify(data));}
}

这里是使用文件预读和缓冲区池

class BufferPool {constructor(poolSize = 10, bufferSize = 64 * 1024) {this.pool = [];this.bufferSize = bufferSize;for (let i = 0; i < poolSize; i++) {this.pool.push(Buffer.alloc(bufferSize));}}acquire() {return this.pool.pop() || Buffer.alloc(this.bufferSize);}release(buffer) {if (this.pool.length < 20) { // 限制池大小this.pool.push(buffer);}}
}// 使用预读优化
async function preReadOptimization(fd, position, length, bufferPool) {const buffer = bufferPool.acquire();const { bytesRead } = await read(fd, buffer, 0, Math.min(length, buffer.length), position);const result = buffer.slice(0, bytesRead);bufferPool.release(buffer);return result;
}
http://www.xdnf.cn/news/1465093.html

相关文章:

  • 数据结构:排序
  • 【Day21】146.LRU缓存 (Least Recently Used)
  • 详细解读Docker
  • STC携手VEX发起全球首个碳资产RWA生态,泰国峰会即将引爆绿色金融
  • 飞算JavaAI炫技赛:电商系统开发全流程实战解析
  • 卫星在轨光压计算详解
  • openharmony之AV_CodeC音视频编解码模块详解(二)
  • (未完待续...)如何编写一个用于构建python web项目镜像的dockerfile文件
  • Kubernetes实战系列(4)
  • v4l2设置图像分辨率失败的问题
  • react+umi项目如何添加electron的功能
  • PyTorch 中.backward() 详解使用
  • 前后端国密加密传输用户密码流程
  • Unity 解决天空盒中间出现一条线
  • flink 伪代码
  • 高效管理网络段和端口集合的工具之ipset
  • Bug排查日记:高效记录与解决之道
  • 高通AR1平台Recovery架构分析与自动恢复出厂设置实现
  • 从 elecworks 到云端协同:SOLIDWORKS Electrical 发展历史 + 核心功能 + 采购指南
  • Linux 磁盘扩容及分区相关操作实践
  • 从Java全栈到云原生:一场技术深度对话
  • Golang语言设计理念
  • 【GEOS-Chem伴随模型第一期】GEOS-Chem Adjoint 模型详解
  • 常见Bash脚本漏洞分析与防御
  • 【Flutter】RefreshIndicator 无法下拉刷新问题
  • 【存储选型终极指南】RustFS vs MinIO:5大维度深度对决,95%技术团队的选择秘密!
  • LeetCode 131 分割回文串
  • 【LeetCode热题100道笔记】删除链表的倒数第 N 个结点
  • Kafka核心原理与常见面试问题解析
  • 《AI 问答系统:从开发到落地,关键技术与实践案例全解析》