访问量 ... 次
访客数 ... 人
上月财务小姐姐点击“生成月度对账单”后,悠闲地端起咖啡杯。十分钟后,她盯着浏览器的加载动画,眼神从期待变成绝望,整个结算系统卡死了。 经过DBA监控发现,出现问题的正是以下SQL:
SELECT
order_id, amount, status
FROM
orders
WHERE
create_time BETWEEN '2024-05-01' AND '2024-05-31'
ORDER BY amount DESC LIMIT 500000,1000
由于create_time
字段仅有单列索引,未建立覆盖索引(create_time
,amount
,status
),导致MySQL被迫进行全表扫描,800万行数据触发12次磁盘临时文件排序。
DBA监控发现该查询扫描行数达821万行,锁持有时间超300秒,最终引发连接池占满、主从延迟告警,结算系统瘫痪1小时。
# 慢查询日志
Rows_examined: 8210000 # 扫描821万行
Sort_merge_passes: 12 # 12次磁盘排序
Lock_time: 302.6s # 锁表超5分钟
为什么这条SQL成了性能杀手?
LIMIT 500000,1000
导致MySQL需要先读取500000+1000条数据,然后丢弃前500000条。create_time
的单列索引,无法覆盖查询(需要amount
排序和status
、order_id
字段)。针对以上分析,解决思路如下:
create_time
, amount
, status
, order_id
),这样查询可以直接在索引上完成,避免回表。fetchSize
设置),避免一次性加载所有数据到内存。具体到该场景的解决方案:
create_time, amount, status, order_id
),这样查询可以完全在索引上完成,避免回表,同时排序可以利用索引的有序性。
-- 创建专用索引
ALTER TABLE orders ADD INDEX idx_cover
(create_time, amount, status, order_id)
LIMIT offset, row_count
中的offset
。
/* 优化后查询(响应时间<1s) */
SELECT order_id, amount, status
FROM orders
WHERE create_time BETWEEN '2024-05-01' AND '2024-05-31'
AND amount < ? -- 上一批最小值(降序时)
AND order_id > ? -- 解决金额重复问题
ORDER BY amount DESC, order_id ASC
LIMIT 5000; -- 分批大小
@RestController
@RequestMapping("/export")
public class DataExportController {
@PostMapping("/orders")
public ResponseData createExportTask(@RequestBody ExportRequest request) {
String taskId = "export_" + System.currentTimeMillis();
// 存入Redis并设置24小时过期
redisTemplate.opsForValue().set(
"export:task:" + taskId,
ExportTask.builder()
.status(ExportStatus.PENDING)
.params(request)
.progress(0)
.build(),
24, TimeUnit.HOURS
);
// 提交到线程池异步执行
exportExecutor.submit(() -> processExportTask(taskId));
return ResponseData.success(taskId);
}
@GetMapping("/status/{taskId}")
public ResponseData getTaskStatus(@PathVariable String taskId) {
ExportTask task = (ExportTask) redisTemplate.opsForValue()
.get("export:task:" + taskId);
if (task == null) {
return ResponseData.error("任务不存在或已过期");
}
return ResponseData.success(task);
}
}
public void processExportTask(String taskId) {
ExportTask task = getTask(taskId);
ExportParams params = task.getParams();
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement(
ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY)) {
stmt.setFetchSize(BATCH_SIZE); // 每次读取5000条
String query = buildQuery(params);
ResultSet rs = stmt.executeQuery(query);
Path tempFile = Files.createTempFile("export_", ".csv");
try (CSVPrinter printer = new CSVPrinter(
Files.newBufferedWriter(tempFile),
CSVFormat.DEFAULT)) {
int count = 0;
while (rs.next()) {
// 处理结果集
printer.printRecord(
rs.getString("order_id"),
rs.getBigDecimal("amount"),
rs.getString("status")
);
// 每1000条更新一次进度
if (++count % 1000 == 0) {
updateProgress(taskId, count);
}
}
updateProgress(taskId, count, ExportStatus.COMPLETED);
// 上传到文件存储
String fileUrl = fileStorageService.upload(tempFile);
task.setFileUrl(fileUrl);
saveTask(taskId, task);
// 发送通知
notifyService.sendExportComplete(params.getUserId(), fileUrl);
}
} catch (Exception e) {
updateProgress(taskId, 0, ExportStatus.FAILED);
logger.error("导出任务失败", e);
}
}
private String buildQuery(ExportParams params) {
return String.format(
"SELECT order_id, amount, status " +
"FROM orders " +
"WHERE create_time BETWEEN '%s' AND '%s' " +
"ORDER BY create_time ASC", // 按时间顺序避免随机IO
params.getStartTime(),
params.getEndTime());
}
空间换时间(索引) + 分而治之(分批) + 异步解耦(任务队列) = 百万级数据处理最优解
财务系统现在每月处理千万级订单导出,峰值期间系统负载保持在40%以下,财务小姐姐终于能准时下班了。