如何处理Node.js读取MySQL海量数据时的内存不足异常?
问题描述
每日需从MySQL数据库读取6000万条记录,基于计算更新日期列,但使用Node.js和Sequelize读取数据时抛出heap out of memory异常。原实现代码如下:
db.sequelize.models.transaction.schema("dpunit").findAll({ order: [ ['txndate', 'asc'] ] }).then(txnList => { // txnList 包含6000万条记录 let valueUpdated = []; for (let tranz of txnList) { tranz.expdate = new Date() // 更新值 valueUpdated.push(tranz.dataValues) } db.sequelize.models.transaction.schema("dpunit").bulkCreate(valueUpdated, { updateOnDuplicate: ["expdate"] }).then(data => { res.send({ data: data.Length }); }).catch(err => { res.send({ error: err}); }); });
最优解决方案
1. 优先使用纯SQL直接更新(性能最优)
如果更新逻辑(比如设置expdate为当前日期)可通过SQL表达式实现,完全不需要把数据拉到应用层处理,这是效率最高、内存占用最低的方案,直接在数据库端完成所有操作。
示例代码:
// 直接执行UPDATE语句,无需读取任何数据到内存 db.sequelize.models.transaction.schema("dpunit") .update( { expdate: db.sequelize.fn('NOW') }, // 用MySQL的NOW()函数获取当前日期,可根据实际计算逻辑调整 { where: {} } // 如需过滤记录,在此添加条件(如特定txndate范围) ) .then(updatedCount => { res.send({ data: updatedCount }); }) .catch(err => { res.send({ error: err }); });
优势:彻底避免内存溢出,数据库端处理速度远快于应用层批量操作,减少网络IO消耗。
2. 分批分页读取+批量更新(适合必须在应用层处理计算的场景)
若更新逻辑无法用SQL实现,必须在Node.js中处理,则需避免一次性加载所有数据,改为分页分批读取,每处理完一批就执行更新,随后释放内存。
实现思路:
- 按
txndate或主键进行分页,每次读取固定数量的记录(比如1万条) - 处理当前批次记录后立即执行批量更新
- 完成后读取下一批,直至所有数据处理完毕
示例代码:
const batchSize = 10000; // 每批次处理1万条,可根据内存情况调整 let offset = 0; let totalUpdated = 0; async function processBatch() { try { // 分页读取当前批次数据,只加载需要的字段减少内存占用 const txnList = await db.sequelize.models.transaction.schema("dpunit").findAll({ order: [['txndate', 'asc']], limit: batchSize, offset: offset, attributes: ['id', 'txndate'] }); if (txnList.length === 0) { // 所有批次处理完成 res.send({ data: totalUpdated }); return; } // 处理当前批次记录 const updatedRecords = txnList.map(tranz => ({ id: tranz.id, expdate: new Date() // 替换为你的实际计算逻辑 })); // 批量更新当前批次 const result = await db.sequelize.models.transaction.schema("dpunit") .bulkCreate(updatedRecords, { updateOnDuplicate: ["expdate"] }); totalUpdated += result.length; offset += batchSize; // 递归处理下一批 processBatch(); } catch (err) { res.send({ error: err, processed: totalUpdated }); } } // 启动分批处理 processBatch();
注意事项:
- 仅读取必要字段(通过
attributes指定),避免加载冗余数据占用内存 - 灵活调整
batchSize:若仍出现内存溢出则调小,内存充足时可适当调大提升效率 - 可添加日志跟踪每批次处理进度
3. 使用Sequelize的流式查询(适合超大数据集逐行处理)
Sequelize支持流式查询,通过stream: true选项将数据以流的形式逐行读取,而非一次性加载到内存,适合处理超大规模数据。
示例代码:
let batch = []; const batchSize = 5000; let totalUpdated = 0; // 创建查询流 const stream = db.sequelize.models.transaction.schema("dpunit").findAll({ order: [['txndate', 'asc']], stream: true, attributes: ['id', 'txndate'] }).stream(); stream.on('data', async (tranz) => { // 暂停流,防止数据堆积 stream.pause(); // 处理当前记录 batch.push({ id: tranz.id, expdate: new Date() // 替换为你的实际计算逻辑 }); // 批次达到指定大小,执行批量更新 if (batch.length >= batchSize) { try { const result = await db.sequelize.models.transaction.schema("dpunit") .bulkCreate(batch, { updateOnDuplicate: ["expdate"] }); totalUpdated += result.length; batch = []; // 清空批次释放内存 } catch (err) { stream.destroy(err); // 出错时终止流 res.send({ error: err, processed: totalUpdated }); return; } } // 恢复流 stream.resume(); }); stream.on('end', async () => { // 处理剩余不足一个批次的记录 if (batch.length > 0) { const result = await db.sequelize.models.transaction.schema("dpunit") .bulkCreate(batch, { updateOnDuplicate: ["expdate"] }); totalUpdated += result.length; } res.send({ data: totalUpdated }); }); stream.on('error', (err) => { res.send({ error: err, processed: totalUpdated }); });
优势:内存占用极低,每次仅处理少量记录,不会一次性加载所有数据。
4. 临时调整Node.js内存限制(不推荐作为长期方案)
若以上方案无法立即实施,可临时调整Node.js堆内存大小,启动时添加--max-old-space-size参数:
node --max-old-space-size=8192 your-script.js # 分配8GB堆内存,根据服务器配置调整
缺点:治标不治本,6000万条记录即使分配更大内存,仍可能导致内存占用过高影响服务稳定性,仅适合临时应急。
内容的提问来源于stack exchange,提问作者Manihtraa
相关产品推荐
相关产品推荐

