如何实现项目内超大规模数据的导出,如何设计实现的?
1 个回答
不要和差点意思的人周旋。王重阳# 1) 分级策略(入口层) * **小导出(≤10万行)**:同步接口直接返回文件(流式写,超时≤30s)。 * **大导出(>10万行)**:走**异步任务**:`POST /exports` 创建任务→返回 `jobId`→后台写文件→完成后通知并给下载链接。 限制并发/配额,避免把在线服务打爆。 # 2) 核心设计(后台导出流水线) **(1) 一致性快照** * 选择“导出时间点”`T0`,只导出 `created_at <= T0`(或使用 MySQL REPEATABLE READ + 短事务分页)。 * 超长导出避免长事务:用**keyset 分页**+时间上限,不持久占锁。 ```sql SELECT ... FROM t WHERE created_at <= :T0 AND (pk > :lastPk) ORDER BY pk ASC LIMIT :batch; ``` **(2) 读取与分页(流式)** * **MySQL**:JDBC流式游标,边读边写,内存恒定。 * 8.0 驱动:`useCursorFetch=true` + `stmt.setFetchSize(1000)` * 5.x 驱动:`stmt.setFetchSize(Integer.MIN_VALUE)` * **ES**:`search_after` 或 `scroll`,每批 5k–10k。 * **批大小**:5k–20k,根据字段宽度与磁盘吞吐压测确定。 **(3) 写文件(零拷贝/低内存)** * **CSV 优先**(通用 & 体积小);`.csv.gz` 压缩。 * **Excel 行上限**:单 sheet 1,048,576 行;需多 sheet/多文件切分或改 CSV/Parquet。 * **安全**:转义逗号/换行/引号;防 CSV 注入(以 `'` 前缀处理以 `=,+,-,@` 开头的单元格)。 **(4) 存储与分发** * 本地临时文件 → 上传 **对象存储**(OSS/S3)→ 生成**带时效**的预签名下载 URL(24–72h)。 * 大到 TB 级:分片并行上传(多 part)。 **(5) 控制面(任务编排)** * `export_job` 表:`job_id、params_hash、status、rows、bytes、file_url、created_by、expire_at、error`。 * Worker:基于 **Spring Batch / Quartz / 自写调度 + 有界线程池**。 * **可观测性**:每 N 批上报进度(行数、速度)、失败重试次数;支持**暂停/取消**。 * **幂等**:`params_hash` 命中且文件未过期直接复用;写文件时先 `.part`,成功后原子重命名。 # 3) 关键细节与避坑 * **OFFSET 慢** → 用 **keyset 分页**(基于主键/唯一键),吞吐稳定。 * **内存/OOM** → 全程**流式**:不把结果集一次性放内存;写端 `BufferedWriter` + `GZIPOutputStream`。 * **长导出一致性** → 以 `T0` 限边界;或先导出全量到对象存储,后续变更用 CDC 生成补丁文件。 * **权限与脱敏** → 字段白名单、按角色脱敏(手机号中间掩码),下载鉴权。 * **并发与限流** → 任务级并发上限(如 2–3/租户)、行速率限制,避免拖垮主库;尽量走**只读从库**/只读副本。 * **格式选择**: * **CSV**:通用,配 gzip; * **Parquet**:列存、压缩好,适合超大数据/数仓联动; * **XLSX**:仅限报表(行上限+内存高)。 * **超大规模(≥亿级)**:别让业务库挖矿——走**离线链路**:把数据同步到数仓(OSS + Spark/Flink 或 ClickHouse),用分布式作业产出分区化文件,再提供下载/打包索引。 # 4) 极简落地示例(JDBC→CSV.GZ,流式) ```java void exportCsv(Job job) throws Exception { Path tmp = Files.createTempFile("export-", ".csv.gz"); try (Connection conn = ds.getConnection()) { conn.setAutoCommit(false); // 只为设置会话级参数 try (PreparedStatement ps = conn.prepareStatement( "SELECT col1,col2,... FROM t WHERE created_at <= ? AND id > ? ORDER BY id LIMIT ?", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)) { // MySQL 8.x streaming ps.setFetchSize(1000); // url里带 useCursorFetch=true long lastId = 0L, rows = 0L; try (GZIPOutputStream gz = new GZIPOutputStream(Files.newOutputStream(tmp)); OutputStreamWriter osw = new OutputStreamWriter(gz, StandardCharsets.UTF_8); CSVPrinter csv = new CSVPrinter(osw, CSVFormat.DEFAULT.withHeader("col1","col2",...))) { while (true) { ps.setTimestamp(1, Timestamp.from(job.snapshotTime)); ps.setLong(2, lastId); ps.setInt(3, job.batchSize); try (ResultSet rs = ps.executeQuery()) { int count = 0; while (rs.next()) { csv.printRecord(sanitize(rs.getString(1)), rs.getString(2), ...); lastId = rs.getLong("id"); rows++; count++; } if (count == 0) break; } reportProgress(job.id, rows); } } } } String url = ossClient.uploadAndSign(tmp, "exports/" + job.id + ".csv.gz", Duration.ofDays(2)); jobRepo.markSuccess(job.id, url); } ``` > 说明:**边读边写**,内存常量级;`sanitize()` 处理 CSV 注入;进度通过 `rows` 上报。 # 5) 面试“60秒版本”回答模板 > 大规模导出我做异步化:`/exports` 创建任务,后台 worker 以 T0 为边界做**流式分页**(MySQL 用游标/ES 用 search\_after),每批 5k–20k,**边读边写 CSV.GZ** 到临时文件,上传到对象存储返回预签名链接。过程有进度、取消、重试、幂等与权限脱敏。避免 OOM 用流式 + gzip,避免慢分页用 **keyset**,避免长事务用时间上限 T0。超大规模则把数据同步到数仓/对象存储,用 Spark/Flink 生成分区化文件再分发。发布于 2025-08-13 15:53 •IP属地 安徽举报评论收藏
公众号进入网站体验更佳

