导读(3行收益):商城运营每天要导出订单,常见问题:导几万单接口直接超时、连点两次生成重复文件、大导出把数据库拖垮。本文给一套「异步任务+游标分页+文件下载」的方案,附任务表 SQL 和导出检查清单。看完能直接改造你的导出功能。
一、导出为什么不能同步做
同步导出的三个典型事故:
- 接口超时:几万行数据边查边组装,请求几十秒才返回,网关直接断开;
- 重复文件:用户等不及点了两次,生成两份一样的文件还不好删;
- 拖垮数据库:大导出全表扫描 + 内存组装,线上查询全变慢。
根源是把导出当成一次同步请求。正确做法是拆成「创建任务 → 后台执行 → 完成后下载」三步,用户提交后立刻得到任务编号,结果好了再通知。
什么样的导出该走异步?判断标准很简单:行数可能上万、执行超过几秒、用户不需要即时拿到结果的,一律异步。订单导出、报表导出、用户名单导出都是典型场景;只有「当前页所见即所得」的小数据导出(几十行)可以继续同步,避免为小功能引入整套任务体系。
二、导出任务表:把导出变成一条记录
CREATE TABLE `export_task` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`task_no` VARCHAR(32) NOT NULL UNIQUE,
`type` VARCHAR(16) NOT NULL COMMENT 'order/product/...',
`filter_json` TEXT NOT NULL COMMENT '导出筛选条件',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '0排队 1执行中 2成功 3失败',
`file_url` VARCHAR(255) DEFAULT NULL,
`row_count` INT DEFAULT 0,
`created_by` BIGINT NOT NULL,
`created_at` DATETIME NOT NULL,
`finished_at` DATETIME DEFAULT NULL
) COMMENT='导出任务表';
要点:同一用户同一筛选条件并发去重——提交前先查有没有未完成的相同任务,有就返回原任务,避免重复文件;任务编号返回给前端轮询状态。任务表同时是权限边界:created_by 记录创建人,下载前校验当前用户是否为任务创建人或有导出权限,防止 A 用户拿到 B 用户的导出链接。
三、超大分页:游标分页而不是 OFFSET
几万行导出不能用 LIMIT offset, size(OFFSET 越深越慢),改用游标分页:
-- 第一页:按主键取前 5000
SELECT * FROM `order` WHERE status = 1 ORDER BY id LIMIT 5000;
-- 下一页:记住上页最大 id,继续往后取
SELECT * FROM `order` WHERE status = 1 AND id > :last_id ORDER BY id LIMIT 5000;
避坑:分页期间数据会变(新订单插入),游标分页天然规避「翻页重复/漏行」;导出的一致性要求高时,任务开始时先记录一个快照边界(如 max(id)),只导出该边界之前的数据。
四、后台执行与文件生成
导出任务由后台 worker 消费,逐页查询、逐页写入文件:
def run_export(task):
last_id, count = 0, 0
with open(tmp_file, "w") as f:
f.write("订单号,金额,状态\n")
while True:
rows = query_page(last_id, 5000)
if not rows:
break
for r in rows:
f.write(f"{r.order_no},{r.amount},{r.status}\n")
last_id = rows[-1].id
count += len(rows)
upload_to_storage(tmp_file) # 上传对象存储
mark_task_success(task, url, count)
文件生成后存对象存储,任务表记录下载地址;用户端轮询到「成功」后展示下载按钮。下载链接建议带有效期签名,避免长期暴露。
内存控制是导出 worker 的隐形门槛:边查边写文件,不要一次性 SELECT * 灌进内存。上面示例逐页取 5000 行写一批,内存占用恒定;如果用了 ORM 的 all() 全量加载,几十万行直接内存溢出或 GC 卡死。文件写入建议先写本地临时文件再上传,比边查边传对象存储更稳(上传失败可重试,不污染线上文件)。
任务状态机要闭环,避免「执行中」卡死:
| 状态 | 说明 | 超时兜底 |
|---|---|---|
| 0 排队 | 等待 worker 消费 | 超 10 分钟无 worker 认领 → 告警 |
| 1 执行中 | 正在逐页导出 | 超 30 分钟 → 标记失败并重试 |
| 2 成功 | 文件已生成 | 清理临时文件 |
| 3 失败 | 可重试 | 记录失败原因,最多重试 3 次 |
五、踩坑清单
- [ ] 同步导出,几万行接口直接超时;
- [ ] 不防重复提交,连点生成重复文件;
- [ ] 用 OFFSET 分页,越翻越慢;
- [ ] 导出期间数据变化导致漏行/重复行;
- [ ] 文件直接落本机磁盘,磁盘打满或重启丢失;
- [ ] 导出失败没有重试与告警,任务卡在「执行中」永远不动;
- [ ] 下载链接不设有效期,长期暴露订单数据;
- [ ] worker 一次性全量加载,几十万行内存溢出。
六、工程落地建议
批量导出建议按「任务表+异步执行+游标分页+对象存储」落地,先覆盖订单导出再扩展其他类型。若团队没有现成后台底座,可基于成型平台(如乔拓云商城)的订单管理能力快速起步,重点核对导出任务与分页策略是否符合上述设计。
七、复盘清单(可直接抄走)
- [ ] 导出是否全部走异步任务;
- [ ] 同一用户同一条件是否防重复提交;
- [ ] 分页是否用游标而非 OFFSET;
- [ ] 文件是否存对象存储且带有效期链接;
- [ ] 失败任务是否有重试与告警;
- [ ] 导出权限是否校验,防止越权导出他人数据。
结语
导出卡死的根源是「把大活当同步干」。任务表、游标分页、文件下载三步拆开,几万行订单也能秒级提交、平稳导出。本文仅作技术分享,具体功能以各平台官方实时信息为准。