业务场景
导出订单报表需关联六张表(账户表、流水表、关联订单表、支付方式表、支付渠道表、支付信息表),具体要求:
- 微服务化限制:禁止连表查询
- 数据规模:约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 定位
- 订单表条件查询(30s+)
- 流水表 IN 查询(报错)
- 支付信息表 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 id
比ORDER 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 万时建议采用:
- 逻辑分片:按时间范围分段导出(如按月划分)
- 物理分片:使用 PostgreSQL 原生分区表
- 混合方案:
-- 第一层时间过滤
WHERE create_time >= ? AND create_time < ?
-- 第二层ID分片
AND id BETWEEN ? AND ?
性能优化的本质
- 资源再分配:在时间、空间、开发成本间寻找平衡点
- 技术组合:需同时考虑数据库原理、JVM 内存模型、分布式 ID 生成机制
- 可观测性:通过监控指标持续验证优化效果