3. 异步导出架构:生产者-消费者模式

异步导出架构的核心是生产者-消费者模式。该模式将数据生产(业务系统触发导出请求)与数据消费(导出服务实际执行导出)解耦,中间通过 Kafka 消息队列作为缓冲层。这种设计能够有效应对业务高峰期的突发请求,避免导出服务被瞬时流量冲垮。

架构核心原则:生产者不等待消费者处理完成,消费者按自身能力拉取消息,两者通过 Kafka 分区机制实现水平扩展。

3.1 生产者-消费者模式在导出场景中的角色

角色 组件 职责 关键行为
生产者 业务服务 / API 网关 接收用户导出请求,生成导出任务消息 将消息发送到 Kafka 指定 topic,不关心消费进度
消息队列 Kafka 集群 持久化存储导出任务消息,提供削峰填谷能力 按分区顺序存储消息,保留消息直到被消费或过期
消费者 导出服务(Exporter) 从 Kafka 拉取消息,执行数据库查询与文件生成 按分区并发消费,提交 offset 标记处理完成
异步导出生产者-消费者模式架构 生产者 业务服务 / API Kafka 消息队列 Topic: export_requests 消费者 导出服务 (Exporter) 发送消息 拉取消息 解耦优势 • 生产者无需等待导出完成,请求响应时间大幅缩短(通常 < 50ms) • 消费者可独立扩缩容,根据积压情况动态调整消费线程数 • Kafka 提供消息持久化,即使导出服务重启也不会丢失任务

3.2 导出服务(Exporter)的设计

导出服务是异步导出架构中的核心消费者组件。它从 Kafka 拉取导出任务消息,协调数据库查询与文件生成两个关键步骤。设计时需要重点关注消费并发度、任务状态管理和失败重试机制。

3.2.1 消费线程模型

导出服务通常采用多线程消费者组模型,每个消费者实例运行多个消费线程,每个线程绑定一个 Kafka 分区。这种模型能够最大化吞吐量,同时保证同一分区内的消息顺序消费。

参数 推荐值 说明
消费者组实例数 3 ~ 10 根据导出任务量级和服务器资源调整
每个实例消费线程数 5 ~ 20 受限于数据库连接池和文件 IO 能力
max.poll.records 500 ~ 2000 单次拉取最大消息数,避免处理超时导致 rebalance
max.poll.interval.ms 300000 (5分钟) 消费线程处理一批消息的最大时间,超时触发 rebalance
常见陷阱:如果导出任务处理时间较长(如生成大文件),需要合理设置 max.poll.interval.ms 和 max.poll.records。否则消费线程在处理过程中未及时发起 poll 请求,会被 Kafka 判定为死亡,触发消费者组 rebalance,导致处理中的任务被重复消费。

3.2.2 任务状态机

每个导出任务在 Exporter 内部经历明确的状态流转,确保异常场景下可追溯、可恢复。

导出任务状态机 PENDING PROCESSING COMPLETED FAILED 消费线程获取 生成成功 生成失败 重试(最多 3 次)

3.3 数据库写入与文件生成

导出服务的核心业务逻辑分为两个阶段:数据库查询与写入和文件生成与上传。这两个阶段对资源的需求不同,需要分别优化。

3.3.1 数据库写入阶段

导出服务首先根据任务参数查询数据库,获取需要导出的数据。对于大数据量导出,需要采用分页查询 + 流式写入策略,避免一次性加载过多数据导致 OOM。

// 伪代码:分页查询并写入临时文件
public void exportData(ExportTask task) {
    int pageSize = 5000;
    int pageNum = 0;
    boolean hasMore = true;

    try (BufferedWriter writer = new BufferedWriter(
            new FileWriter(task.getTempFilePath()))) {

        while (hasMore) {
            // 1. 分页查询数据库
            List<Record> records = database.queryPage(
                task.getQuerySql(), pageNum, pageSize);

            if (records.isEmpty()) {
                hasMore = false;
                break;
            }

            // 2. 将记录写入临时文件
            for (Record record : records) {
                writer.write(record.toCsvLine());
                writer.newLine();
            }

            // 3. 更新进度(可选)
            task.setProcessedCount(task.getProcessedCount() + records.size());
            pageNum++;
        }
    } catch (IOException e) {
        // 标记任务失败,触发重试
        task.markFailed(e);
    }
}
优化建议:对于超大数据量导出(百万级以上),建议使用数据库游标(Cursor)代替分页查询。游标方式避免 OFFSET 带来的性能衰减,且能保持稳定的内存占用。

3.3.2 文件生成阶段

文件生成阶段将临时数据文件转换为最终导出文件(CSV、Excel、PDF 等),并上传到对象存储或提供下载链接。此阶段是 CPU 密集和 IO 密集的混合操作。

文件类型 生成方式 内存消耗 适用场景
CSV 流式写入,逐行追加 低(约几 MB) 大数据量(百万行以上)
Excel (XLSX) POI SXSSF 流式写入 中(约几十 MB) 中等数据量(十万行以内)
PDF 模板渲染 + 逐页生成 高(取决于模板复杂度) 报表、发票等格式化文档

3.3.3 两阶段资源隔离

数据库写入和文件生成对系统资源的需求不同,建议在 Exporter 内部进行线程池隔离,避免文件生成阶段的 CPU 密集操作阻塞数据库查询线程。

Exporter 内部线程池隔离设计 Kafka 消费线程 拉取消息 & 分发 DB 查询线程池 核心线程: 8, 最大: 16 文件生成线程池 核心线程: 4, 最大: 8 OSS 上传 隔离设计要点 • DB 查询线程池:IO 密集型,设置较大核心线程数,使用 SynchronousQueue 避免任务堆积 • 文件生成线程池:CPU 密集型,设置较小核心线程数(CPU 核数 + 1),使用 LinkedBlockingQueue
关键指标监控:在生产环境中,需要重点监控以下指标以判断导出服务是否健康:
• DB 查询线程池活跃线程数:接近最大值时说明数据库查询成为瓶颈
• 文件生成线程池队列长度:持续增长说明文件生成速度跟不上查询速度
• Kafka 消费 lag:持续增长说明导出服务整体处理能力不足