千万级订单数据导出解决方案(解决慢、OOM、锁表)
目录
一、核心问题根源
二、最优落地方案(生产首选:异步 + 分页流式导出 + 大文件分片)
方案架构
1. 数据库层优化(重中之重)
(1)必须加索引(避免全表扫描)
(2)分页方式:主键分页(游标分页)替代传统 Limit 分页
(3)查询优化(防止锁表、长事务)
2. 应用层:流式写入 Excel(彻底解决 OOM)
推荐组件
关键代码思路(伪代码)
3. 异步任务(解决前端超时、服务阻塞)
4. 文件存储与分片(超大文件优化)
方式 1:文件拆分(推荐给运营使用)
方式 2:导出为 CSV(大数据量终极方案)
三、备选方案(按场景选用)
方案 1:数据库原生导出(运维 / 技术侧快速导出,不面向前端)
MySQL into outfile(服务器本地导出)
方案 2:大数据架构(数据量持续暴涨,亿级数据)
四、关键避坑点(生产必看)
五、方案选型总结
最简落地步骤(直接照着做)
针对日单 1000+、总数据千万级的订单表导出场景,核心思路:分页流式读写、分批落地、异步任务、避免全表加载进内存,下面分方案、实操、避坑逐一说明,按落地优先级排序。
一、核心问题根源
- 直接
select * 全表 + 内存组装Excel:千万数据一次性加载 →内存溢出 (OOM) - 单页查询量大、网络 / IO 串行 →导出超时、速度慢
- 全表查询无索引、长事务 →数据库锁表、影响线上业务
二、最优落地方案(生产首选:异步 + 分页流式导出 + 大文件分片)
方案架构
前端触发导出 → 后端创建异步任务→ 数据库游标 / 分页流式读取→ 本地 / 对象存储逐行写入文件 → 完成后推送下载链接 / 消息通知。
1. 数据库层优化(重中之重)
(1)必须加索引(避免全表扫描)
千万级订单表绝对不能无分页、无索引查全表
- 分页唯一条件:自增主键 ID(order_id)建立主键索引
- 若有时间筛选(按日期导出):给
create_time建普通索引 - 禁止:
limit 1000000, 1000深分页(性能雪崩)
(2)分页方式:主键分页(游标分页)替代传统 Limit 分页
传统limit offset, size偏移量大时数据库遍历数据极慢,改用ID 游标分页(最优)
-- 第一页 select * from `order` where order_id > 0 order by order_id asc limit 1000; -- 下一页(用上一页最大ID作为条件) select * from `order` where order_id > #{lastMaxId} order by order_id asc limit 1000;- 分页步长建议:1000~2000 条 / 页(平衡 DB 压力与 IO 效率)
- 全程
order by 主键,保证数据不重复、不丢失
(3)查询优化(防止锁表、长事务)
- 读写分离:导出查询走从库,完全不影响主库下单、改单业务
- 关闭事务 / 只读事务:导出是纯查询,不要开启业务事务,避免长事务锁行
- MySQL 配置:开启
read-only、bulk_insert_buffer_size,适当调大net_read_timeout
2. 应用层:流式写入 Excel(彻底解决 OOM)
核心:不把全表数据加载到 JVM 内存,读一批、写一批、释放一批
推荐组件
- 阿里EasyExcel(首选):专为大文件导出设计,低内存、流式读写,原生支持分批写入,完全规避 POI OOM 问题
- 禁用原生 POI(HSSF/XSSF):千万行会直接撑爆堆内存
关键代码思路(伪代码)
// 1. 初始化Excel写入器(流式,只保留少量行在内存) ExcelWriter writer = EasyExcel.write(filePath, OrderDTO.class).build(); Long lastId = 0L; int pageSize = 1000; while (true) { // 2. 游标分页查询一批数据(仅1000条) List<Order> pageList = orderMapper.selectByCursor(lastId, pageSize); if (CollectionUtils.isEmpty(pageList)) { break; // 数据读取完毕 } // 3. 分批写入文件,写完立即释放集合内存 writer.write(pageList); // 4. 更新游标ID lastId = pageList.get(pageList.size() - 1).getOrderId(); // 手动GC(可选,加速内存回收) pageList.clear(); } // 收尾 writer.finish();3. 异步任务(解决前端超时、服务阻塞)
千万级导出耗时分钟级,绝对不能同步接口返回:
- 技术选型:Spring Task / XXL-Job / RabbitMQ/RocketMQ 异步队列
- 流程:
- 前端点击导出 → 调用同步接口,仅创建导出任务记录,返回「任务正在处理」
- 消息 / 定时任务消费:执行上述「DB 分页 + 流式写文件」逻辑
- 任务完成:文件上传至OSS/MinIO/ 本地文件服务器,数据库更新任务状态
- 前端轮询任务状态,完成后展示下载链接
- 限流保护:限制同时运行的导出任务数(如最多 3 个),防止批量导出打垮服务 / DB
4. 文件存储与分片(超大文件优化)
千万行 Excel 单文件会出现:打开卡顿、传输慢、损坏风险,两种优化:
方式 1:文件拆分(推荐给运营使用)
每10 万~20 万行拆分一个 Excel 文件,最终生成订单数据_01.xlsx、订单数据_02.xlsx,打包为 ZIP 压缩包供下载。
- 优势:单个文件体积小,打开、下载都流畅
方式 2:导出为 CSV(大数据量终极方案)
Excel 单文件行数有隐性上限,千万级优先导出 CSV:
- CSV 纯文本格式,写入速度远快于 Excel,IO 开销极低
- Excel/WPS/ 记事本均可直接打开,兼容性强
- 流式逐行拼接文本写入,内存占用最低
三、备选方案(按场景选用)
方案 1:数据库原生导出(运维 / 技术侧快速导出,不面向前端)
适合运维后台、离线统计,零应用压力
MySQL into outfile(服务器本地导出)
-- 导出订单表到服务器本地CSV(走DB层流式,无应用内存压力) SELECT order_id, user_id, amount, create_time FROM `order` WHERE create_time BETWEEN 'xxx' AND 'xxx' INTO OUTFILE '/data/export/order_2026.csv' CHARACTER SET utf8mb4 FIELDS TERMINATED BY ',' ENCLOSED BY '"' LINES TERMINATED BY '\n';- 优点:速度最快、不占用应用服务资源
- 限制:需要 MySQL 服务器文件写入权限,不能直接给前端使用,适合离线备份
方案 2:大数据架构(数据量持续暴涨,亿级数据)
如果后续订单突破亿级,引入数仓组件:
- 订单数据实时同步到 ClickHouse/Hive(binlog 同步)
- 导出查询全部走数仓,彻底隔离线上主库
- 配合 DataX/Sqoop 做离线批量导出
四、关键避坑点(生产必看)
- ** 禁止全表 Select *** 只查询业务需要的字段,减少网络传输、内存、文件体积。
- 严禁深分页 Limit N,M数据量越大,偏移分页越慢,坚持使用ID 游标分页。
- 线上导出必须走从库大查询会拉高 CPU、IO,主库优先保障下单、支付核心业务。
- JVM 参数微调(配合流式导出) 适当调大堆内存,但不要依赖堆内存存数据:
-Xms2g -Xmx2g,开启堆内存溢出快照-XX:+HeapDumpOnOutOfMemoryError。 - 超时配置 调整数据库连接超时、文件写入超时、网关超时,避免中途中断。
- 权限与清理 导出文件设置自动过期删除(如 7 天),防止磁盘占满。
五、方案选型总结
| 数据量级 | 推荐方案 |
|---|---|
| 百万级以内 | 异步任务 + EasyExcel 游标分页流式导出(单 Excel/CSV) |
| 千万级 | 异步 + 游标分页 + 拆分多文件 ZIP / 直接导出 CSV |
| 亿级 + | 数据同步到 ClickHouse/Hive + 离线导出 |
| 运维临时导出 | MySQLinto outfile原生导出 |
最简落地步骤(直接照着做)
- 订单表
order_id、create_time确认索引存在; - 接入 EasyExcel,实现ID 游标分页 + 逐批流式写入;
- 接入 MQ / 定时任务做异步导出,前端轮询状态;
- 导出走MySQL 从库,千万行优先输出 CSV 或拆分 Excel;
- 配置文件自动清理、任务限流。
