记一次十五万数据导出内存溢出的问题优化实战

业务场景

导出订单报表需关联六张表(账户表、流水表、关联订单表、支付方式表、支付渠道表、支付信息表),具体要求:

  • 微服务化限制:禁止连表查询
  • 数据规模:约15万条订单数据
  • 响应要求:前端同步返回,等待时间不超过15秒

问题排查

原始实现分析

// 原始代码流程
1. 多条件查询订单报表
2. 提取关联字段存入List
3. 调用方法逐条查询关联表
4. Java内存中拼接数据
5. 使用EasyExcel导出

优化历程

第一次优化:基础性能提升

优化措施

数据量控制

SELECT COUNT(1) FROM orders WHERE ...
  • 增加数据量校验,限制最大导出量10万条

数据结构优化

// 优化前:List.contains() O(n)
List<Long> orderIds = queryOrderIds();

// 优化后:HashSet.contains() O(1)
Set<Long> orderIdSet = new HashSet<>(queryOrderIds());

并发查询优化

// 线程池配置
ThreadPoolExecutor executor = new ThreadPoolExecutor(
6, // 核心线程数=关联表数量
12,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
new ThreadFactoryBuilder().setNamePrefix("关联查询-").build()
);

// 并发执行查询任务
CompletableFuture<Map<Long, Payment>> paymentFuture = CompletableFuture.supplyAsync(
() -> paymentService.batchQuery(paymentIds), executor
);

优化效果

  • 匹配速度提升 300%(得益于 Set/Map 结构)
  • 流水表/支付信息表查询耗时减少 60%(多线程并发)
指标 优化前 优化后
流水表查询耗时 28s 9s
支付信息表查询 32s 11s
内存匹配耗时 15s 0.3s

遗留问题

  • MyBatisPlus 批量 IN 查询超万级数据时,数据库性能急剧下降

第二次优化:SQL深度调优

慢 SQL 定位

  1. 订单表条件查询(30s+)
  2. 流水表 IN 查询(报错)
  3. 支付信息表 IN 查询(报错)

解决方案

IN查询限制

/* 原始问题SQL */
SELECT * FROM payment_info
WHERE order_id IN (?,?,...10000+)

/* 优化方案:虚拟表JOIN */
SELECT pi.*
FROM payment_info pi
JOIN (VALUES (1),(2)...(100000)) AS tmp(order_id)
ON pi.order_id = tmp.order_id
  • 执行计划优化:PostgreSQL 自动为 VALUES 子句创建哈希索引
  • 性能对比:10 万级数据查询耗时从 30s+ 降至 20s-

执行计划对比

- Sort (cost=294573.28..297033.41 rows=984050 width=103)
- Sort Key: pi.order_id
- -> Seq Scan on payment_info pi (cost=0.00..29457.33 rows=984050 width=103)
+ Hash Join (cost=2254.33..41085.11 rows=984050 width=103)
+ Hash Cond: (pi.order_id = tmp.order_id)
+ -> Seq Scan on payment_info pi (cost=0.00..29457.33 rows=984050 width=103)
+ -> Hash (cost=1154.33..1154.33 rows=100000 width=8)
+ -> Values Scan on tmp (cost=0.00..1154.33 rows=100000 width=8)

索引优化实战

-- 创建复合索引
CREATE INDEX idx_order_time_status ON orders(create_time, delete_time);

-- 优化后查询
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE create_time BETWEEN 1714920142325 AND 1744920142325
AND delete_time IS NULL
ORDER BY id LIMIT 100000;

索引效果验证

查询条件 执行时间 扫描方式
无索引 32.4s 全表扫描
单字段(create_time) 18.7s 索引范围扫描
复合索引(create_time+delete_time) 12.3s 索引跳跃扫描
字段顺序 选择性 优化效果
create_time 低选择性 过滤 80% 数据
delete_time 高选择性 精准匹配剩余 20% 数据
  • 索引跳跃扫描:PostgreSQL 12+ 可跳过未删除数据段
  • 隐式排序:天然支持 ORDER BY create_time,避免 filesort

优化效果

  • 订单表查询耗时从 30s → 12s
  • IN 查询报错问题彻底解决

第三次优化:时间区间替代IN查询

核心思路

  • IN 查询改写为 时间区间过滤
WHERE (create_time BETWEEN 1714920142325 AND 1744920142325)

雪花算法ID的隐藏优势

雪花算法ID结构:
0 | 0001100 10111110 10001001 01011100 | 00011 | 0000000000
└─符号位 时间戳(41位) 数据中心 机器ID 序列号

排序性能对比

// 优化前
List<Order> orders = orderService.query()
.orderByDesc("create_time")
.list();

// 优化后
List<Order> orders = orderService.query()
.orderByDesc("id") // 等效时间排序
.list();
  • 索引穿透率:B+Tree 对连续 LongID 的页命中率提升 30%
  • 实测数据ORDER BY idORDER BY create_time 快 2.8 倍
数据量 排序方式 耗时
15万条 ORDER BY create_time 4.2s
15万条 ORDER BY id 1.5s

第四次优化:内存控制

问题现象

  • 15 万数据导出时内存突增至 4GB

EasyExcel内存模型调优

// 内存模式(默认)
ExcelWriter writer = EasyExcel.write()
.inMemory(true)
.file("report.xlsx")
.build();

// 磁盘缓存模式(优化后)
ExcelWriter writer = EasyExcel.write()
.inMemory(false)
.tempTemplate("/tmp/excel_cache")
.file("report.csv")
.build();
模式 内存占用 关键技术
内存模式 4GB 全量 List<Row> 存储
磁盘模式 1.7GB 分页缓存 + 临时文件
  • 对象复用:通过 WriteCellStyleRepository 缓存样式对象
  • 分段刷新:每 2000 行触发磁盘刷新(autoTrim(true)

格式转换的代价

格式 内存开销 适合场景
XLSX 10 万条以下带样式数据
CSV 百万级无样式数据
CSV.GZ 最低 网络传输场景

内存占用对比

阶段 内存模式 磁盘模式
数据加载完成 1.2GB 1.2GB
开始写入 3.8GB↑ 1.5GB
写入完成 4.1GB 1.7GB

CSV格式优化

// CSV写入配置
EasyExcel.write(os, ExcelVO.class)
                    .excelType(ExcelTypeEnum.CSV)
                    .inMemory(false)
                    .sheet("sheet")
                    .doWrite(records);

最终效果

  • 内存占用从 4GB → 1.7GB
  • 导出耗时稳定在 15s 内

最终效果

指标 优化前 优化后
总耗时 40s+ 15s-
内存峰值 4GB 1.7GB
最大数据量 10万 15万
查询QPS 12 63

进阶思考

数据分片策略的演进

当数据量突破 50 万时建议采用:

  1. 逻辑分片:按时间范围分段导出(如按月划分)
  2. 物理分片:使用 PostgreSQL 原生分区表
  3. 混合方案
-- 第一层时间过滤
WHERE create_time >= ? AND create_time < ?
-- 第二层ID分片
AND id BETWEEN ? AND ?

性能优化的本质

  • 资源再分配:在时间、空间、开发成本间寻找平衡点
  • 技术组合:需同时考虑数据库原理、JVM 内存模型、分布式 ID 生成机制
  • 可观测性:通过监控指标持续验证优化效果
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。