顺丰 AI 全栈开发工程师(二面)面经:AI Coding、Agent 技术与千万级数据导出系统设计

校招面经 · AI 全栈开发 · Java 后端 · 系统设计

一、面试概览

本次面试为顺丰 AI 全栈开发工程师二面,整体考察方向比较综合,不仅涉及常规的前后端开发能力,还重点考察了 AI 辅助软件开发、Agent 技术理解以及复杂业务场景下的系统设计能力。

与单纯考察 Java 基础、数据库和前端框架的技术面试不同,这次面试更关注候选人能否将技术应用到实际业务中,尤其是面对复杂需求时,能否完成需求拆解、方案设计、开发落地和质量保障。

核心考察方向

  • AI 辅助前后端开发
  • Agent 技术与大模型应用
  • 千万级数据导出系统设计
  • Java 后端工程能力
  • 需求拆解、质量保障与团队协作

面试问题速览

序号面试问题考察重点
1介绍个人背景与技术栈全栈能力、项目经历
2如何利用 AI 辅助前后端开发?AI Coding、工程化
3介绍 Agent 相关研究与技术理解大模型应用、Agent
4如何设计千万级数据导出模块?SQL、分页、异步处理
5如何避免高并发导出拖垮系统?限流、线程池、资源隔离
6如果任务队列出现大量积压怎么办?消息队列、动态扩容
7对岗位有什么理解?自身还需要提升什么?岗位匹配、工程协作

二、AI 辅助前后端开发:如何利用 AI 提高开发效率?

这是本次面试比较重要的一条主线。

面试官希望了解的不只是会不会使用 AI 编程工具,而是能否形成一套完整的 AI 辅助开发流程。

2.1 如何利用 AI 辅助前后端开发?

问题:在实际项目开发中,你是如何利用 AI 辅助完成前后端开发的?

这个问题可以从需求分析、方案设计、代码生成、人工审查和测试验证几个阶段展开。

我采用的整体思路是:

在这里插入图片描述

第一步:需求理解与边界划分

首先不能直接把一整份业务需求交给 AI,让它一次性生成所有代码。

需要先理解业务目标,明确:

  • 当前功能解决什么问题?
  • 涉及哪些业务模块?
  • 前后端分别负责什么?
  • 模块之间如何交互?
  • 是否涉及已有业务逻辑和历史接口?

可以借助 AI 对需求文档进行梳理,提取核心功能、业务规则和异常情况,再由开发人员确认最终方案。

第二步:AI 辅助方案设计

在明确需求后,将业务需求、项目技术栈、已有接口和模块边界提供给 AI,让其辅助生成:

  • 功能模块设计文档。
  • 数据库表结构建议。
  • 前后端接口定义。
  • 页面组件划分。
  • 核心业务流程。

这里最重要的是人工审核。

AI 生成的方案不一定完全符合实际项目,尤其是涉及历史业务逻辑、模块耦合和特殊业务约束时,不能直接照搬。

第三步:任务拆解与代码生成

将完整需求拆分为相对独立的开发任务。

例如开发一个订单管理功能,可以拆分为:

  1. 数据库表结构设计。
  2. 后端实体类、DTO 和接口开发。
  3. 订单查询与分页逻辑。
  4. 前端列表页面开发。
  5. 查询条件与分页交互。
  6. 前后端接口联调。

每完成一个阶段,再让 AI 继续处理下一部分。

这种方式比一次性生成整个项目更加可控,也更容易定位问题。

第四步:代码审查与质量保障

AI 生成代码后,不能只看页面是否能够正常展示,还需要关注:

  • 是否符合原始业务需求?
  • 是否存在重复代码?
  • 是否引入不必要的依赖?
  • 是否存在接口参数不匹配?
  • 是否存在空值、异常和边界问题?
  • 是否影响已有功能?

尤其是后端核心业务逻辑,需要重点检查数据处理、权限校验和异常处理。

可以先通过 Mock 数据验证接口行为,再进行前后端联调,最后进行完整回归测试。

第五步:文档沉淀

每完成一个功能模块,就同步更新开发文档、接口文档和必要的测试记录。

这样既方便后续维护,也能够让 AI 在下一次开发时获得更准确的项目上下文。

2.2 为什么不能完全依赖 AI 生成代码?

这是 AI Coding 实际应用中非常重要的问题。

AI 生成代码的效率虽然很高,但仍然存在一些问题:

问题具体表现解决方式
需求理解偏差忽略隐藏业务约束提前提供完整需求和边界
上下文不足不清楚已有项目结构提供目录、接口和相关代码
代码修改范围过大修改不相关模块明确文件范围和修改目标
逻辑错误代码能够运行但结果错误人工审查、单元测试
功能退化新功能影响旧功能回归测试
安全问题忽略权限和参数校验安全审查和测试

这里可以引入一个概念:

Context Engineering(上下文工程)

它强调如何为大模型组织和提供有效上下文,而不仅仅是编写 Prompt。

例如,在让 AI 开发某个后端接口时,可以提供:

  • 项目使用的 Spring Boot 版本。
  • 数据库表结构。
  • 已有 Service 和 Controller 代码。
  • 接口规范。
  • 业务约束。
  • 当前任务的修改范围。

上下文越准确,模型越容易生成符合现有项目结构的代码。

2.3 AI 辅助开发是否意味着可以减少测试工作?

并不是。

AI 能够提高代码生成速度,但不能保证代码质量。

实际上,代码生成越快,越需要建立完善的质量保障机制。

建议从以下几个层面进行验证:

  1. 单元测试: 验证单个方法和模块的输入输出。
  2. 接口测试: 验证请求参数、响应格式和异常处理。
  3. 集成测试: 验证前后端以及不同服务之间的协作。
  4. 回归测试: 验证新功能不会破坏原有功能。
  5. 代码审查: 检查业务逻辑、代码规范和安全问题。

最终应该形成:

AI 负责提高开发效率,开发人员负责技术决策和代码质量,测试环节负责验证系统是否真正满足业务要求。

三、Agent 技术:大模型应用与智能体开发

本次面试还涉及 Agent 平台及相关研究经历。

对于这一部分,重点是能够解释 Agent 的基本概念、应用价值,以及自己在论文研究中涉及的相关方向。

3.1 什么是 Agent?与普通 LLM 应用有什么区别?

传统的大模型应用通常采用:

用户输入 → Prompt → LLM → 输出结果

这种模式适合简单问答、文本生成和信息提取等任务。

但对于复杂业务,仅依靠一次模型调用往往不够。

例如,用户提出:

帮我查询某个地区近三个月销量最高的产品,并分析销量变化原因。

这个任务可能涉及:

  1. 理解用户需求。
  2. 拆解查询条件。
  3. 调用数据库或外部工具。
  4. 获取查询结果。
  5. 分析数据。
  6. 生成最终报告。

Agent 的核心价值在于让大模型不仅能够生成文本,还能够根据任务目标进行规划、调用工具、观察执行结果,并根据结果调整后续行动。

一个比较典型的 Agent 执行过程是:

在这里插入图片描述

需要注意,Agent 并不意味着一定要使用多个大模型或多个智能体。

单 Agent 可以通过工具调用完成任务,多 Agent 则可以通过职责划分和协作完成更加复杂的工作。

3.2 Agent 在实际业务中有哪些应用?

可以结合企业业务举例:

应用场景Agent 可以完成的工作
智能客服理解用户问题、查询订单、处理售后
智能研发分析需求、生成代码、执行测试
数据分析查询数据库、分析指标、生成报告
知识问答检索知识、组织证据、生成答案
流程自动化调用业务接口、执行审批和任务流转

对于 AI 全栈开发工程师而言,重点不仅是理解模型本身,还需要具备将 Agent 接入实际业务系统的能力。

例如:

  • 如何设计工具调用接口?
  • 如何管理执行状态?
  • 如何处理调用失败?
  • 如何控制模型调用成本?
  • 如何验证 Agent 的执行结果?
  • 如何保证业务操作的权限和安全?

这些才是 Agent 工程化落地时需要重点考虑的问题。

3.3 论文研究中涉及哪些 Agent 相关技术?

在研究生阶段,我的研究方向涉及大模型增强型知识库问答(KBQA),研究过程中也涉及 Agent、记忆机制以及智能体执行过程中的验证与优化等方向。

KBQA 的目标是让模型能够理解自然语言问题,并结合知识库完成结构化查询和答案生成。

例如:

用户提问:

某位导演执导的电影中,哪些电影获得过奥斯卡奖?

这类问题可能涉及多个实体和关系,需要结合知识库进行查询。

相比直接让大模型生成答案,研究中更加关注复杂问题的处理、执行结果验证,以及如何利用历史经验改善后续任务执行效果。

这里可以简单理解为:

  • Agent:让系统具备任务执行和工具调用能力。
  • Memory:帮助系统利用历史信息和执行经验。
  • RAG:通过检索相关知识为模型提供外部信息。
  • Validator:检查模型生成结果是否满足预期约束。
  • Router:根据任务特征选择合适的处理路径。

这些都是大模型应用从简单文本生成走向复杂业务执行时值得关注的技术方向。

本节侧重研究方向与技术概念,不展开论文中的具体系统架构和实现细节。

3.4 RAG 和 Agent 有什么区别?

这是两个容易混淆的概念。

对比维度RAGAgent
核心目标为模型提供外部知识完成具有目标的任务
主要机制检索、增强、生成规划、执行、观察
是否需要工具调用不一定通常需要
是否需要多轮执行不一定经常需要
典型场景企业知识库问答智能客服、自动化工作流

二者并不冲突。

RAG 可以作为 Agent 的一个知识检索工具,Agent 在执行任务时可以通过 RAG 获取相关知识,再根据检索结果决定下一步行动。

四、核心场景设计:千万级数据导出系统

本次面试重点

这是本次二面中最值得深入复习的场景设计题。

面试官提出了一个实际业务需求:

假设公司有多个业务系统,系统中都有数据查询和导出功能。现在希望设计一个通用的数据导出模块,用户可以根据查询条件导出对应的数据,后台生成 Excel 文件,完成后通知用户下载。

初看起来,这只是一个普通的 Excel 导出功能。

但如果面对千万级数据量、多业务系统同时调用,以及大量用户并发导出,就会出现数据库压力、线程资源耗尽、内存溢出和磁盘空间不足等问题。

因此,这道题的关键不是如何生成 Excel,而是:

如何在大量数据和高并发请求下,保证导出任务能够稳定执行,同时不影响正常业务。

4.1 设计一个通用的数据导出模块,需要考虑哪些问题?

首先从业务流程出发。

在这里插入图片描述

从这个流程可以发现,导出模块主要存在以下几类风险。

风险产生原因可能造成的影响
数据库压力过大千万级数据查询、多表关联慢 SQL、数据库负载过高
内存溢出一次性加载大量数据JVM OOM
线程资源耗尽大量同步导出请求线程池耗尽、服务阻塞
磁盘空间不足大量临时文件和 Excel 文件磁盘写满
重复任务用户频繁点击导出资源浪费、任务积压
数据安全问题通用接口权限控制不严越权导出、数据泄露
服务稳定性问题导出与普通业务共享资源正常业务受到影响

4.2 千万级数据如何分页查询?

问题:如果有千万级数据需要导出,应该如何查询?

首先要避免一次性将所有数据加载到内存中。

例如:

List<Order> orders = orderMapper.selectAll();

如果一次性查询几百万甚至上千万条记录,可能造成:

  • 数据库需要返回大量数据。
  • JDBC 驱动需要处理大量结果。
  • Java 对象占用大量堆内存。
  • 后续 Excel 生成继续消耗内存。

因此,通常需要采用分批查询。

这里有两种常见方案。

方案一:LIMIT 分页

例如:

SELECT *
FROM orders
ORDER BY id
LIMIT 0, 1000;

下一批:

SELECT *
FROM orders
ORDER BY id
LIMIT 1000, 1000;

这种方式比较容易理解和实现。

但随着 OFFSET 增大,数据库可能需要扫描并跳过越来越多的记录,导致深分页性能下降。

方案二:游标分页(Keyset Pagination)

游标分页的核心思路是:不再告诉数据库跳过多少条记录,而是告诉数据库从上一批最后一条记录之后继续查询。

例如:

SELECT *
FROM orders
WHERE id > 100000
ORDER BY id
LIMIT 1000;

下一次查询时,将上一批最后一条记录的 ID 作为新的游标。

这样就不需要随着页数增加而不断跳过前面的记录。

Java 实现示例:

public void exportOrders() {

    Long lastId = 0L;
    int batchSize = 1000;

    while (true) {

        List<Order> orders =
            orderMapper.selectByCursor(lastId, batchSize);

        if (orders.isEmpty()) {
            break;
        }

        // 处理当前批次数据
        writeToExcel(orders);

        // 更新游标
        lastId = orders.get(orders.size() - 1).getId();
    }
}

对应 SQL:

SELECT *
FROM orders
WHERE id > #{lastId}
ORDER BY id ASC
LIMIT #{batchSize};

为什么游标分页更适合大数据量导出?

假设总共有 1000 万条数据,每批查询 1000 条:

  • 总共需要查询约 10000 批。
  • 每次只需要处理当前批次。
  • 不需要将所有数据放入内存。
  • 可以边查询、边处理、边写入文件。

需要注意,游标分页并不意味着数据库查询成本完全固定,仍然需要合理的索引设计。

索引优化

例如:

SELECT id, order_no, amount, create_time
FROM orders
WHERE status = 1
  AND id > #{lastId}
ORDER BY id
LIMIT 1000;

可以根据查询条件和排序方式考虑联合索引,例如:

CREATE INDEX idx_status_id
ON orders(status, id);

具体索引是否有效,还需要结合数据分布、查询选择性和 EXPLAIN 执行计划进行判断。

进一步思考:导出过程中数据发生变化怎么办?

例如,导出任务执行期间,有新订单不断插入。

如果不加限制,可能出现导出范围不断变化的问题。

可以在任务开始时记录最大 ID:

SELECT MAX(id) FROM orders;

假设得到 maxId = 20000000,后续查询统一增加:

WHERE id > #{lastId}
  AND id <= #{maxId}
ORDER BY id
LIMIT 1000;

这样能够避免任务执行期间新插入的数据不断进入当前导出范围。

如果业务要求严格的数据一致性,还需要进一步考虑数据库快照、数据版本或专门的数据导出机制。

4.3 如何解决 Excel 导出过程中的内存溢出?

解决查询阶段的内存问题还不够。

即使每次只查询 1000 条数据,如果将所有批次的数据不断追加到一个巨大的 Excel 对象中,仍然可能发生 OOM。

因此需要采用流式写入(Streaming Write)。

常见 Java Excel 处理方案:

  • Apache POI:Java 中常用的 Excel 操作库。
  • SXSSF:Apache POI 提供的流式 Excel 写入方案。
  • EasyExcel:阿里巴巴开源的 Excel 处理工具,支持大数据量读写。

以 Apache POI 的 SXSSF 为例:

SXSSFWorkbook workbook = new SXSSFWorkbook(100);

try {
    Sheet sheet = workbook.createSheet("订单数据");

    // 按批次查询
    Long lastId = 0L;

    while (true) {

        List<Order> orders =
            orderMapper.selectByCursor(lastId, 1000);

        if (orders.isEmpty()) {
            break;
        }

        for (Order order : orders) {

            Row row = sheet.createRow(
                sheet.getLastRowNum() + 1
            );

            row.createCell(0)
               .setCellValue(order.getId());

            row.createCell(1)
               .setCellValue(order.getOrderNo());

            row.createCell(2)
               .setCellValue(order.getAmount());
        }

        lastId = orders.get(orders.size() - 1).getId();
    }

    try (FileOutputStream output =
             new FileOutputStream("orders.xlsx")) {

        workbook.write(output);
    }

} finally {

    workbook.dispose();
    workbook.close();
}

这里:

new SXSSFWorkbook(100);

表示内存中保留一定数量的行,超出窗口的行会写入临时磁盘文件。

因此能够显著降低大数据量 Excel 生成时的堆内存占用。

但需要注意:

  1. 流式写入并不是完全不占用内存。
  2. SXSSF 会使用临时磁盘文件。
  3. 临时文件需要及时清理。
  4. Excel 单个 Sheet 存在最大行数限制,超过限制需要拆分 Sheet 或文件。
  5. 文件生成完成后仍然需要考虑存储和下载问题。

4.4 为什么需要异步导出?

问题:如果用户点击导出后,后端直接同步执行 SQL 和 Excel 生成,会有什么问题?

假设一个导出任务需要执行 30 秒。

如果采用同步方式:

用户请求
   |
   v
Controller
   |
   v
查询数据库
   |
   v
生成 Excel
   |
   v
返回文件

整个过程中 HTTP 请求会一直占用服务端线程。

如果同时有 100 个用户提交导出任务,就可能出现大量请求线程被长期占用。

最终可能造成:

  • Tomcat 工作线程耗尽。
  • 普通业务请求无法及时处理。
  • 请求超时。
  • 服务响应能力下降。

因此,更合理的方式是将导出任务异步化。

异步导出的基本流程:

  1. 用户提交导出请求。
  2. 服务端进行权限和参数校验。
  3. 创建导出任务并保存任务信息。
  4. 将任务提交到消息队列。
  5. 后台消费者异步执行导出。
  6. 生成文件并保存。
  7. 更新任务状态。
  8. 通知用户下载。

任务表可以设计为:

CREATE TABLE export_task (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    task_name VARCHAR(255),
    status VARCHAR(32),
    file_url VARCHAR(1024),
    progress INT DEFAULT 0,
    error_message TEXT,
    create_time DATETIME,
    update_time DATETIME
);

任务状态可以设计为:

状态含义
PENDING等待执行
RUNNING正在执行
SUCCESS执行成功
FAILED执行失败
CANCELLED已取消

前端提交任务后,不再一直等待 Excel 文件生成,而是返回任务 ID。

随后可以通过轮询接口查询任务状态:

GET /api/export/task/{taskId}

返回示例:

{
  "taskId": 10001,
  "status": "RUNNING",
  "progress": 65
}

也可以通过 WebSocket 推送任务进度,避免频繁轮询。

两种方式的区别:

方式特点
前端轮询实现简单,但存在重复请求
WebSocket服务端主动推送,实时性更好
SSE服务端单向推送,适合进度通知

4.5 如何避免用户重复提交导出任务?

原始面试中还讨论了用户短时间内频繁点击导出按钮的问题。

如果没有任何限制,用户可能在几秒内提交几十个相同任务。

这不仅会浪费数据库资源,还会导致任务队列积压。

可以从两个层面处理。

前端:防抖与按钮状态控制

例如:

const exporting = ref(false);

async function handleExport() {

  if (exporting.value) {
    return;
  }

  exporting.value = true;

  try {
    await createExportTask();
  } finally {
    exporting.value = false;
  }
}

这里通过状态控制,避免用户重复点击。

也可以使用防抖函数控制短时间内的重复请求。

但需要注意,前端防抖只能改善用户交互,不能作为后端防重复提交的唯一手段。

后端:幂等控制

可以为每次导出请求生成业务唯一标识,或者根据用户、导出类型和查询条件生成请求指纹。

例如:

userId + exportType + queryCondition

在一定时间窗口内,如果已经存在相同的执行中任务,就可以直接返回原任务 ID,而不是重新创建任务。

如果涉及分布式环境,还可以使用 Redis 原子操作或数据库唯一约束保证并发情况下的幂等性。

4.6 如何保证导出文件的安全性?

导出功能往往涉及用户订单、客户信息和业务数据,因此不能只考虑性能。

需要重点考虑以下问题:

第一,权限校验。

不能因为用户能够调用通用导出接口,就允许其导出所有业务数据。

需要在服务端验证:

  • 用户是否有权访问当前业务模块。
  • 用户是否有权查询对应数据。
  • 用户是否有权导出指定字段。
  • 是否需要进行数据范围隔离。

第二,下载链接安全。

不能直接暴露永久有效的文件访问地址。

可以使用具有有效期的签名 URL 或一次性下载 Token。

例如:

/download/export/10001?token=xxxx

服务端验证 Token 是否有效、是否过期,以及当前用户是否有权访问对应文件。

第三,临时文件清理。

大量导出文件如果长期保留,会占用大量磁盘空间。

可以通过定时任务清理过期文件:

每天凌晨
   |
   v
扫描临时文件目录
   |
   v
判断文件是否过期
   |
   v
删除无效文件

如果使用对象存储,也可以通过生命周期规则自动清理过期文件。

4.7 高并发导出如何避免拖垮整个系统?

这是本次面试中进一步深入追问的重点。

前面虽然已经实现了异步化,但如果大量任务同时进入后台,仍然可能出现资源耗尽问题。

例如:

  • 大量消费者同时查询数据库。
  • 多个任务同时生成 Excel。
  • 线程池任务队列不断增长。
  • JVM 堆内存持续升高。
  • 普通业务线程受到影响。

因此需要进一步设计系统级保护机制。

第一:入口限流

首先在导出接口入口处进行 QPS 限制。

例如:

每秒最多允许 20 个导出请求

超过限制后,可以根据业务情况选择:

  • 直接返回请求过于频繁。
  • 将请求放入等待队列。
  • 提示用户稍后重试。

实际阈值需要结合数据库承载能力、服务器配置和任务平均耗时进行压测确定。

可以使用 Redis + Lua 实现分布式限流,也可以通过网关层限流。

第二:线程池隔离

不能让导出任务和普通业务共用一个无限制的线程池。

例如:

应用服务
   |
   |------------------------|
   |                        |
   v                        v
普通业务线程池           导出任务线程池
   |                        |
   v                        v
普通业务请求             Excel 导出任务

这样即使导出任务出现阻塞,也能减少对普通业务请求的影响。

Java 中可以使用 ThreadPoolExecutor:

ThreadPoolExecutor exportExecutor =
    new ThreadPoolExecutor(
        2,
        4,
        60,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(100),
        new ThreadPoolExecutor.AbortPolicy()
    );

这里:

参数含义示例
corePoolSize核心线程数2
maximumPoolSize最大线程数4
keepAliveTime非核心线程空闲存活时间60 秒
workQueue任务等待队列容量 100
handler拒绝策略AbortPolicy

需要注意,线程池的执行逻辑是:

  1. 先创建核心线程。
  2. 核心线程都在忙碌时,优先将任务放入阻塞队列。
  3. 队列满后,才会继续创建非核心线程。
  4. 达到最大线程数且队列也满时,触发拒绝策略。

为什么不建议使用无界队列?

如果使用无界队列,任务数量不断增加时,队列可能持续占用内存,最终引发 OOM。

因此需要结合任务平均耗时、机器资源和可接受等待时间设置合理的队列容量。

另外,线程池隔离只是 JVM 内部的逻辑隔离。如果需要更强的资源隔离,还可以将导出服务部署为独立进程或容器。

第三:控制消费者任务拉取速度

消息队列中的任务并不是越快消费越好。

如果某台机器已经处于高负载状态,就不应该继续无限制拉取任务。

可以根据:

  • CPU 使用率。
  • JVM 堆内存使用率。
  • 当前运行任务数量。
  • 数据库连接池使用情况。
  • 磁盘剩余空间。

动态控制消费者拉取任务的数量。

例如:

机器负载较低:
允许拉取 10 个任务

机器负载较高:
只允许拉取 3 个任务

机器接近资源上限:
暂停拉取新任务

这里体现的是一种资源感知的任务调度思路。

第四:JVM 内存保护

对于大数据量导出,需要关注 JVM 堆内存。

可以通过 JVM 参数限制最大堆内存:

-Xms2g
-Xmx2g

同时监控:

  • Heap 使用率。
  • Old Generation 使用情况。
  • GC 次数和耗时。
  • Full GC 频率。
  • OOM 异常。

当内存使用率达到预警阈值时,可以暂停接收新的导出任务,等待当前任务完成和资源释放。

这里需要纠正一个容易混淆的说法:

GC 不是 OOM 的万能解决方案。

如果程序持续持有大量对象,或者一次性加载过多数据,即使不断触发 GC,也无法解决内存不足问题。

真正的解决方法是控制对象生命周期、分批处理、流式写入和限制并发任务数量。

4.8 如果任务队列出现大量积压怎么办?

面试官继续追问:

如果任务数量越来越多,当前机器已经无法满足处理需求,队列中出现大量积压,应该怎么办?

这实际上考察的是系统的容量规划和过载处理能力。

可以从三个方面考虑。

第一:任务优先级划分

将导出任务按照业务重要程度分级:

优先级任务类型处理策略
高优先级核心业务导出优先调度
普通优先级常规报表导出正常排队
低优先级历史数据批量导出低峰期执行

通过不同优先级的队列,避免大量普通导出任务占用全部处理资源。

第二:动态扩容消费者

如果任务积压持续增加,且确认瓶颈主要在消费者计算资源,可以增加消费者节点。

例如:

扩容前:

MQ
 |
 |------> Consumer 1


扩容后:

MQ
 |
 |------> Consumer 1
 |------> Consumer 2
 |------> Consumer 3
 |------> Consumer 4

多个消费者并行处理任务,提高整体吞吐量。

不过需要注意:

消费者数量并不是越多越好。

如果数据库已经成为瓶颈,继续增加消费者只会加重数据库压力。

因此扩容前需要先判断瓶颈究竟位于 CPU、内存、数据库、磁盘还是网络。

第三:背压机制(Backpressure)

当下游处理能力不足时,不能让上游继续无限制地产生任务。

可以通过以下方式控制:

  • 限制任务提交速度。
  • 限制消息队列积压量。
  • 控制消费者拉取数量。
  • 暂停低优先级任务。
  • 对超出系统承载能力的请求进行拒绝或延迟处理。

其核心思想是:

当下游无法处理更多任务时,上游需要感知压力并降低任务生产速度,避免整个系统进入持续过载状态。

4.9 消息队列如何保证任务可靠执行?

在异步导出系统中,消息队列不仅用于提高并发处理能力,还承担任务解耦和缓冲的作用。

但引入 MQ 后,也会出现新的问题。

例如:

  • 消息发送失败怎么办?
  • 消费者执行过程中宕机怎么办?
  • 消息重复投递怎么办?
  • 任务执行失败后如何恢复?

可以从以下几个方面解决。

1. 消息发送可靠性

例如,数据库已经创建任务,但是 MQ 消息发送失败。

此时可能出现任务状态一直是 PENDING,却没有消费者执行。

可以采用本地消息表或 Transactional Outbox 模式:

在同一个数据库事务中保存任务信息和待发送消息,再通过后台程序可靠地将消息发送到 MQ。

2. 消费端幂等

消息队列可能重复投递消息。

因此消费者需要检查任务状态,避免同一个任务被重复执行。

例如:

UPDATE export_task
SET status = 'RUNNING'
WHERE id = #{taskId}
  AND status = 'PENDING';

只有成功将任务从 PENDING 更新为 RUNNING 的消费者才能继续执行。

对于分布式环境,还需要进一步考虑任务锁、租约和超时恢复。

3. 失败重试

如果任务因为临时网络异常或数据库连接问题失败,可以进行有限次数的重试。

例如:

第一次失败
    |
    v
等待 10 秒
    |
    v
第二次重试
    |
    v
等待 30 秒
    |
    v
第三次重试
    |
    v
仍然失败?
    |
    v
进入死信队列 / 人工处理

这里可以使用指数退避策略,避免大量失败任务同时重试造成新的流量冲击。

4. 任务恢复

如果消费者执行任务期间突然宕机,可以通过任务表中的状态和更新时间判断任务是否超时。

对于超时任务,可以将其重新放入待执行队列。

这样即使单个消费者发生故障,也不会导致整个导出任务永久丢失。

4.10 最终方案总结:如何设计一个稳定的大数据导出系统?

模块主要职责关键技术
接入层请求校验与流量控制网关、QPS 限流
任务管理层创建任务、状态管理Spring Boot、MySQL
消息队列异步调度与任务缓冲RabbitMQ、Kafka
消费执行层任务执行与并发控制ThreadPoolExecutor
数据查询层大数据量分批查询游标分页、索引优化
文件生成层Excel 流式写入EasyExcel、SXSSF
文件存储层文件持久化与下载对象存储、临时目录
通知层任务进度和结果通知WebSocket、SSE
监控层资源监控与异常告警JVM、GC、任务监控

最终可以归纳为八个关键设计原则:

  1. 查询层: 使用游标分页,避免深分页和一次性加载。
  2. 处理层: 采用流式处理,控制单次任务内存占用。
  3. 执行层: 使用异步任务,避免长时间占用 HTTP 请求线程。
  4. 调度层: 使用消息队列,实现任务解耦和削峰填谷。
  5. 资源层: 使用线程池隔离和并发控制,避免资源耗尽。
  6. 安全层: 做好用户权限、下载链接和文件生命周期管理。
  7. 可靠性层: 做好任务幂等、失败重试和任务恢复。
  8. 扩展层: 通过任务优先级和动态扩容提高系统处理能力。

五、其他面试问题

5.1 如何设计一个通用的数据导出接口?

对于多个业务系统共用的导出模块,可以考虑模板化设计。

例如定义统一的导出接口:

public interface ExportService<T> {

    // 查询当前批次数据
    List<T> fetchData(Long lastId, int batchSize);

    // 写入 Excel
    void writeData(List<T> data);

    // 获取导出文件名称
    String getFileName();
}

不同业务系统实现自己的查询逻辑和数据转换逻辑,而通用模块负责:

  • 任务创建。
  • 任务调度。
  • 分页执行。
  • 文件生成。
  • 文件存储。
  • 任务通知。

这样能够减少重复代码,提高模块复用性。

需要注意,不同业务的数据权限和查询条件仍然应该由对应业务模块负责,不能因为追求通用性而把所有 SQL 和权限逻辑都放进一个公共接口。

5.2 如果 SQL 查询速度很慢,应该如何优化?

可以按照以下步骤进行分析:

第一步:使用 EXPLAIN 分析执行计划。

EXPLAIN
SELECT *
FROM orders
WHERE user_id = 1001;

重点关注:

  • 是否使用索引。
  • 扫描的数据行数。
  • 是否出现全表扫描。
  • 是否产生临时表。
  • 是否存在额外排序。

第二步:优化索引。

根据查询条件建立合理索引,避免不必要的全表扫描。

第三步:减少不必要的数据读取。

避免:

SELECT *
FROM orders;

尽可能只查询实际需要的字段。

第四步:优化多表关联。

检查 JOIN 条件、关联字段索引和数据量,必要时调整查询方式。

第五步:考虑业务层面的数据拆分。

如果单表数据量过大,可以根据业务特点考虑分区、分库分表或离线数据处理。

但分库分表并不是默认选择,应该在索引优化、SQL 优化和架构调整仍无法满足需求时再考虑。

5.3 如果多个业务系统同时调用通用导出接口,如何处理?

重点关注两个方面:

业务隔离:

不同业务可以使用独立的任务类型、查询策略和处理配置,避免不同业务之间产生不必要的耦合。

资源隔离:

根据业务重要程度设置不同的任务优先级和资源配额。

对于核心业务,可以设置独立线程池或独立消费者组,避免普通导出任务占用全部资源。

如果业务规模较大,还可以进一步将导出服务独立部署。

5.4 如果导出过程中某一批数据处理失败怎么办?

不能简单地将整个任务直接判定为成功。

可以根据业务要求选择:

  • 整体失败:适用于要求完整性和一致性的导出。
  • 分批重试:适用于可恢复的临时错误。
  • 失败记录:记录失败批次和具体原因。
  • 断点续传:记录最后成功处理的游标,从失败位置继续执行。

需要特别注意,断点续传要求写入操作具备一定的幂等性,避免重复写入数据。

5.5 介绍一下自己的知识图谱项目

面试过程中也涉及个人 AI 项目经历。

研究生阶段参与专利知识图谱构建,主要面向专利文档中的技术知识提取与结构化组织。

项目主要包含以下几个环节:

  1. 文档解析: 对电子 PDF 和扫描 PDF 采用不同的解析方式,扫描文档结合 OCR 进行识别。
  2. 文本预处理: 清洗文档内容,提取有效文本和段落。
  3. 信息抽取: 结合正则表达式与大模型进行实体、关系和语义信息提取。
  4. 知识组织: 将技术问题、技术方案和技术效果等信息进行关联。
  5. 图谱存储: 将结构化数据存储到 Neo4j 等图数据库中。

这里可以进一步思考两个问题:

为什么不完全使用大模型进行信息抽取?

因为专利文档存在大量格式相对固定的内容,例如编号、标题、日期等,这些信息使用正则表达式处理更加高效、稳定。

而对于复杂的技术实体、语义关系和上下文信息,大模型具有更强的理解能力。

因此可以采用规则与大模型结合的方式,在保证效率的同时提高复杂信息抽取能力。

如何保证知识图谱质量?

需要关注:

  • OCR 识别错误。
  • 实体重复。
  • 实体边界识别错误。
  • 关系抽取错误。
  • 实体之间的错误关联。

可以通过实体规范化、去重、规则校验和人工抽样检查等方式提高质量。

5.6 对 AI 全栈开发工程师岗位有什么理解?

面试官介绍,该岗位整体偏向后端全栈开发,同时涉及 AI 工具和智能体相关业务。

具体业务方向还需要根据后续用人部门的编制安排确定。

我对岗位的理解是:

AI 全栈开发工程师不仅需要具备前后端业务开发能力,还需要理解大模型应用的基本原理,能够将 AI 能力接入现有业务系统。

一方面,需要具备 Java 后端和 Vue 前端开发能力,完成业务功能、接口设计和系统集成。

另一方面,需要理解大模型 API、RAG、Agent 和工具调用等技术,能够将其应用于实际业务场景。

此外,随着 AI 编程工具逐渐普及,开发人员还需要具备较强的需求拆解、代码审查、测试验证和跨团队协作能力。

六、面试官反馈与能力提升方向

本次面试最后还围绕岗位职责和个人能力提升进行了交流。

面试官给出的建议主要有以下几个方面:

方向反馈与建议
全栈能力需要保持前端和后端两方面的技术广度
需求分析提升复杂业务需求的拆解能力
质量保障不能只关注功能实现,还需要重视测试和验证
团队协作加强与测试团队的沟通和协同
AI 工程化关注 AI 工具在实际业务开发中的落地

其中,我认为最值得关注的是质量保障。

在 AI Coding 越来越普及的情况下,代码生成的成本正在降低,但这并不意味着软件开发的整体难度同步降低。

相反,开发人员需要更加关注:

  • 如何确保需求理解正确?
  • 如何判断 AI 生成代码是否可靠?
  • 如何设计有效的测试用例?
  • 如何避免新功能影响已有业务?
  • 如何与测试人员共同保障交付质量?

这也是从单纯的代码开发向工程化交付转变的重要能力。

七、面试复盘与总结

这次顺丰 AI 全栈开发工程师二面,整体上不是单纯考察某一个技术栈,而是更加关注实际工程能力。

我认为有三个比较明显的特点。

1. AI 能力不再只是会调用大模型 API。

面试官关注的是如何把 AI 真正融入开发流程,包括需求分析、代码生成、任务拆解和质量保障。

对于 AI 全栈工程师而言,既需要理解大模型应用,也需要具备传统软件工程能力。

2. 系统设计题更加关注完整的工程闭环。

千万级数据导出题并不是简单地回答使用分页、线程池和消息队列。

真正需要考虑的是:

从用户提交请求,到后台执行任务,再到文件生成、下载和过期清理,整个链路如何保证稳定性。

尤其是任务积压、资源隔离、消息可靠性和故障恢复,都是容易继续深入追问的方向。

3. 工程交付质量非常重要。

面试官多次强调需求拆解、质量保障和测试协作。

这说明企业在引入 AI Coding 后,并不是只需要能够快速生成代码的开发人员,而是需要能够对最终交付结果负责的工程师。

最后总结

这次面试让我比较深刻地认识到,AI 全栈开发并不是简单地将前端、后端和大模型技术拼接在一起。

它更强调三个层面的综合能力:

  • 开发能力: 能够独立完成前后端业务开发。
  • AI 能力: 能够理解和应用大模型、RAG、Agent 等技术。
  • 工程能力: 能够完成复杂需求拆解、系统设计、质量验证和稳定性保障。

尤其是在 AI Coding 逐渐普及的背景下,真正有价值的能力可能不再只是写出多少代码,而是能否正确理解问题、选择合理方案,并确保最终系统稳定可靠地运行。

整理说明:本文依据本次二面会议纪要及原始交流记录整理,技术扩展部分用于补充知识点和可能的延伸追问,不代表面试过程中均实际讨论或实现过。文中的代码为便于理解的示例,具体生产环境参数应结合实际业务和压测结果确定。

Logo

更多推荐