16-项目经验
注意:以下所有项目案例均为虚构示例,面试时必须替换成自己的真实项目经历。
介绍一下你的项目架构?
原始问法:
- 介绍一下你的项目架构?
来源题目:
SRC-16-156-498
面试先答
**(虚构示例,需替换)**我之前做过一个 企业级智能客服 Agent 平台,面向内部 1000+ 客服人员,日处理会话量 5 万+。整体架构采用 分层微服务 + Agent 中台 的设计:接入层(API Gateway + Nginx 负载均衡)→ 业务层(会话服务、知识库服务、Agent 编排服务、工具服务)→ 数据层(MySQL 主从、Redis 集群、ElasticSearch、Milvus 向量库)→ AI 层(大模型服务、RAG 检索服务、Prompt 管理服务)。核心是 Agent 编排服务,通过 ReAct 循环驱动大模型进行意图识别 → 工具调用 → 结果生成,结合 RAG 增强和 Prompt 工程优化,实现 85%+ 的问题自动解决率。
核心结论
- (虚构)架构风格:分层微服务 + Agent 中台
- 核心模块:接入层 → 业务层 → 数据层 → AI 层
- 技术亮点:ReAct 循环 + RAG + Prompt 工程
1. 架构分层图(虚构示例)
┌─────────────────────────────────────────────────┐
│ 接入层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Nginx │ │API Gateway│ │ 负载均衡 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────┬───────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 业务层 │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ 会话服务 │ │ 权限服务 │ │
│ └─────────────┘ └──────────────┘ │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ Agent编排 │ │ 工具服务 │ │
│ └─────────────┘ └──────────────┘ │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ 知识库服务 │ │ 消息通知 │ │
│ └─────────────┘ └──────────────┘ │
└─────────────────────┬───────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ AI 层 │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ 大模型服务 │ │ RAG 检索服务 │ │
│ └─────────────┘ └──────────────┘ │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ Prompt管理 │ │ 可观测平台 │ │
│ └─────────────┘ └──────────────┘ │
└─────────────────────┬───────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 数据层 │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ MySQL 主从 │ │ Redis 集群 │ │
│ └─────────────┘ └──────────────┘ │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ ElasticSearch│ │ Milvus 向量库 │ │
│ └─────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────┘
2. 核心模块说明(虚构示例)
| 模块 | 技术栈 | 核心职责 |
|---|---|---|
| 会话服务 | Spring Boot + WebSocket | 管理客服与用户的实时会话 |
| Agent 编排服务 | Spring Boot + LangChain4j | ReAct 循环驱动、工具调度 |
| RAG 检索服务 | Milvus + ES | 向量检索 + 关键词检索 + 重排序 |
| 大模型服务 | GPT-4o API | 意图识别、文本生成 |
| 知识库服务 | MySQL + ES + Milvus | 知识文档管理、向量化 |
| 工具服务 | Spring Boot | 订单查询、工单创建、数据统计 |
3. 面试回答要点
- 架构分层清晰:接入/业务/AI/数据四层,职责明确
- 技术选型有理有据:为什么用微服务、为什么选 Milvus
- 突出自己负责的模块:重点讲自己主导的 Agent 编排或 RAG 模块
- 量化成果:85% 自动解决率、5 万+ 日会话量
项目中最有挑战/出彩的地方是什么?
原始问法:
- 项目中最有挑战/出彩的地方是什么?
来源题目:
SRC-16-156-499
面试先答
**(虚构示例,需替换)**我认为项目中最出彩的是 RAG 检索效果优化 这块。项目初期 RAG 的 Top 5 召回率只有 72%,回答经常答非所问。我主导做了 系统性的四维度优化:Chunk 语义切分(替代固定长度切分,召回率 +8%)、混合检索融合(BM25 + 向量 + RRF 融合,召回率 +12%)、Cross-Encoder 重排序(精排 Top K 结果,准确率 +5%)、Query 动态扩展(同义词扩展 + 子问题分解,召回率 +3%)。最终 Top 5 召回率从 72% 提升到 93%,用户满意度从 68% 提升到 89%。
核心结论
- (虚构)出彩点:RAG 检索效果系统性优化
- 优化手段:Chunk 切分 + 混合检索 + 重排序 + Query 扩展
- 成果量化:召回率 72%→93%,满意度 68%→89%
1. STAR 结构回答框架
S(Situation)- 背景
├── 项目初期 RAG 效果差
├── 用户反馈回答不准确
├── 数据:Top 5 召回率 72%
└── 影响:客服效率低,用户投诉多
T(Task)- 任务
├── 目标:将 Top 5 召回率提升到 90%+
├── 约束:不改业务逻辑,纯优化
└── 时间:2 周
A(Action)- 行动
├── 1. 构建评测数据集(2000 条标注)
├── 2. Chunk 语义切分(语义边界 + 50% 重叠)
├── 3. 混合检索融合(BM25 + 向量 + RRF)
├── 4. Cross-Encoder 重排序
└── 5. Query 动态扩展
R(Result)- 结果
├── Top 5 召回率:72% → 93%
├── 用户满意度:68% → 89%
├── 客服平均处理时间:8 分钟 → 3 分钟
└── 获得季度项目之星
2. 关键技术实现(虚构示例)
// 混合检索核心实现
public class HybridRetrievalEngine {
private final BM25Search bm25;
private final VectorSearch vectorSearch;
private final CrossEncoderReranker reranker;
public RetrievalResult retrieve(String query) {
// Step 1: Query 扩展
String expandedQuery = queryExpander.expand(query);
// Step 2: 双路召回
List<ScoredDoc> bm25Results = bm25.search(expandedQuery, 50);
List<ScoredDoc> vectorResults = vectorSearch.search(expandedQuery, 50);
// Step 3: RRF 融合
List<ScoredDoc> fused = rrfFusion(bm25Results, vectorResults);
// Step 4: Cross-Encoder 重排序
List<ScoredDoc> reranked = reranker.rerank(expandedQuery, fused, 20);
return new RetrievalResult(reranked);
}
// Reciprocal Rank Fusion
private List<ScoredDoc> rrfFusion(List<ScoredDoc> list1,
List<ScoredDoc> list2) {
Map<String, Double> rrfScores = new HashMap<>();
int k = 60;
for (int i = 0; i < list1.size(); i++) {
rrfScores.merge(list1.get(i).getId(),
1.0 / (k + i + 1), Double::sum);
}
for (int i = 0; i < list2.size(); i++) {
rrfScores.merge(list2.get(i).getId(),
1.0 / (k + i + 1), Double::sum);
}
return rrfScores.entrySet().stream()
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
.map(e -> new ScoredDoc(e.getKey(), e.getValue()))
.collect(Collectors.toList());
}
}
3. 面试回答要点
- 用数据说话:72% → 93%,用户满意度 68% → 89%
- 展示思考过程:为什么选这些优化手段,每一步的决策依据
- 体现主人翁精神:主动发现问题、主导优化方案、推动落地
- 总结复盘:优化过程中的经验教训
项目中遇到最难的问题是什么?怎么解决的?
原始问法:
- 项目中遇到最难的问题是什么?怎么解决的?
来源题目:
SRC-16-156-500
面试先答
**(虚构示例,需替换)**项目中最难的问题是 生产环境 MySQL 数据库连接池突然耗尽 的线上故障。现象:高峰期(每小时整点)大量请求超时,数据库连接数飙升到 maxActive=200 上限,应用报错 "Too many connections"。排查过程:先用 Arthas 观察到连接获取后未及时释放 → 定位到 Agent 工具调用的数据库操作 → 发现是 Spring 的 @Transactional 在异步线程中没有正确传播导致事务未提交、连接未释放。解决方案:一是将数据库操作从 Agent 工具中剥离到独立服务;二是使用 TransactionTemplate 编程式事务确保连接正确释放;三是增加连接池监控告警。结果:连接池峰值从 198 降到 45,零超时。
核心结论
- (虚构)问题:MySQL 连接池耗尽导致的性能故障
- 根因:
@Transactional在异步线程中未正确传播 - 解决方案:架构调整 + 编程式事务 + 监控告警
- 教训:异步场景下的事务管理需要特别注意
1. 问题排查过程(虚构示例)
故障现象:
├── 高峰期整点(每小时)请求超时
├── 数据库连接数达 200 上限
└── 应用日志:Too many connections
排查步骤:
Step 1: 确认时间规律
├── 观察到每小时整点触发
├── 整点有定时任务 + 业务高峰
└── 初步怀疑:定时任务或高并发导致
Step 2: Arthas 现场诊断
├── thread -n 3: 观察到大量线程卡在 getConnection()
├── trace DataSource.getConnection: 连接获取耗时超长
└── 定位到 Agent 工具模块的数据库操作
Step 3: 代码审查
├── Agent 工具用 @Transactional 注解
├── 调用链经过异步线程(@Async)
├── @Transactional 默认不传播到异步线程
└── 事务未提交 → 连接未释放 → 连接池耗尽
Step 4: 验证假设
├── 手动触发 Agent 工具调用
├── 监控连接池变化
└── 确认:每次调用增加 3-5 个未释放连接
2. 解决方案(虚构示例)
// 方案一:使用编程式事务(确保事务在当前线程提交)
@Service
public class ToolDataService {
private final TransactionTemplate transactionTemplate;
public void executeInTransaction(Runnable operation) {
transactionTemplate.execute(status -> {
operation.run();
return null;
});
}
}
// 方案二:架构调整 —— 将数据库操作剥离到独立服务
@Service
public class OrderToolService {
private final OrderRepository orderRepository;
private final ToolDataService toolDataService;
public ToolResult queryOrder(String orderId) {
return toolDataService.executeInTransaction(() -> {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new BusinessException("订单不存在"));
return ToolResult.success(order);
});
}
}
// 方案三:连接池监控告警
@Configuration
public class ConnectionPoolMonitor {
@Scheduled(fixedRate = 5000) // 每 5 秒检查
public void monitor() {
HikariPoolMXBean pool = getHikariPoolMXBean();
int active = pool.getActiveConnections();
int idle = pool.getIdleConnections();
int waiting = pool.getThreadsAwaitingConnection();
if (waiting > 0 || active > 150) {
alertService.alert("数据库连接池告警",
String.format("活跃:%d 空闲:%d 等待:%d", active, idle, waiting));
}
}
}
3. 面试回答要点
- 遵循 STAR 结构:现象 → 排查 → 根因 → 解决方案 → 效果
- 展示排查能力:如何一步步定位问题(Arthas、日志、代码审查)
- 体现深度思考:为什么选择这个方案,有哪些备选方案
- 总结经验教训:异步场景、事务管理、连接池监控
项目的技术选型是怎么考虑的?
原始问法:
- 项目的技术选型是怎么考虑的?
来源题目:
SRC-16-156-501
面试先答
**(虚构示例,需替换)**我们的技术选型遵循 "成熟稳定优先、团队能力匹配、扩展性考虑、社区活跃" 四大原则。关键选型决策包括:大模型选 GPT-4o(效果最好、Function Call 稳定)、向量数据库选 Milvus(开源、支持超大规模、社区活跃)、Agent 框架选 LangChain4j(Java 生态最成熟、Spring Boot 集成好)、缓存选 Redis Cluster(成熟稳定、性能满足)、消息队列选 RocketMQ(金融级可靠性、国内生态好)。没有为了追新而选技术,所有选型都经过 技术预研 + POC 验证 + 团队培训 三步流程。
核心结论
- 选型原则:成熟稳定 > 团队匹配 > 扩展性 > 社区活跃
- 关键决策:GPT-4o(模型)、Milvus(向量库)、LangChain4j(框架)
- 决策流程:预研 → POC → 培训 → 落地
1. 选型决策矩阵(虚构示例)
| 决策项 | 候选方案 | 最终选择 | 决策理由 |
|---|---|---|---|
| 大模型 | GPT-4o / Claude 3.5 / 千问 | GPT-4o | Function Call 稳定、效果最好 |
| 向量数据库 | Milvus / Weaviate / Qdrant | Milvus | 开源、支持亿级、Java SDK 完善 |
| Agent 框架 | LangChain4j / Spring AI / 自研 | LangChain4j | Java 生态成熟、Spring Boot 集成 |
| 缓存 | Redis / Caffeine / Hazelcast | Redis Cluster | 成熟稳定、集群方案成熟 |
| 消息队列 | RocketMQ / Kafka / Pulsar | RocketMQ | 金融级可靠、事务消息 |
| 搜索引擎 | ElasticSearch / OpenSearch | ElasticSearch | 团队熟悉、生态完善 |
| 前端 | React / Vue / Angular | React + TypeScript | 组件库丰富、类型安全 |
2. 选型决策流程
需求分析
↓
┌─────────────────────┐
│ 1. 明确选型标准 │
│ - 核心需求 │
│ - 团队能力 │
│ - 运维成本 │
│ - 社区生态 │
└─────────┬───────────┘
↓
┌─────────────────────┐
│ 2. 候选方案调研 │
│ - 技术文档阅读 │
│ - 社区口碑调研 │
│ - 同类项目参考 │
└─────────┬───────────┘
↓
┌─────────────────────┐
│ 3. POC 验证 │
│ - 搭建 Demo │
│ - 性能测试 │
│ - 集成测试 │
│ - 团队技术评估 │
└─────────┬───────────┘
↓
┌─────────────────────┐
│ 4. 决策与落地 │
│ - 技术评审会 │
│ - 团队培训 │
│ - 灰度使用 │
│ - 全量推广 │
└─────────────────────┘
3. 面试回答要点
- 展示决策依据:不是拍脑袋选的,有数据和调研支撑
- 体现权衡能力:每个选型都有利弊,说明为什么选这个
- 展示务实态度:不追新、不炫技,适合团队的才是最好的
- 提及踩过的坑:如有选型不当的经历,展示复盘能力
项目中你负责的模块是什么?核心业务场景是什么?
原始问法:
- 项目中你负责的模块是什么?核心业务场景是什么?
来源题目:
SRC-16-156-502
面试先答
**(虚构示例,需替换)**我在智能客服 Agent 平台中负责 两个核心模块:Agent 编排服务 和 RAG 检索服务。Agent 编排服务 是整个系统的大脑,负责接收用户消息 → 调用大模型进行意图识别 → 调度工具(订单查询、工单创建等)→ 生成回复。核心业务场景包括:用户咨询订单状态(高频、需实时)、申请退款/换货(涉及多系统调用)、业务规则查询(依赖知识库 RAG 检索)。RAG 检索服务 负责构建企业知识库的语义检索能力,支撑 Agent 回答准确率。我主导了这两个模块的架构设计、核心代码实现和性能优化。
核心结论
- (虚构)负责模块:Agent 编排服务 + RAG 检索服务
- 核心场景:订单查询、退款申请、业务规则咨询
- 贡献:架构设计、核心实现、性能优化
1. 负责模块详解(虚构示例)
模块一:Agent 编排服务
@Service
public class AgentOrchestrationService {
private final ChatModel chatModel;
private final ToolRegistry toolRegistry;
private final RAGRetrievalService ragService;
private final ContextManager contextManager;
public AgentResponse processUserMessage(Session session, String userMessage) {
// Step 1: 构建上下文
List<Message> messages = contextManager.buildContext(session);
messages.add(new UserMessage(userMessage));
// Step 2: RAG 知识检索
List<Document> knowledge = ragService.retrieve(userMessage);
if (!knowledge.isEmpty()) {
messages.add(new SystemMessage(
"参考以下知识回答:" + formatDocuments(knowledge)
));
}
// Step 3: 调用大模型(ReAct 循环)
ChatResponse response;
int maxIterations = 5;
for (int i = 0; i < maxIterations; i++) {
response = chatModel.chat(messages);
if (response.isToolCall()) {
// 工具调用
ToolResult toolResult = executeToolCall(response.getToolCall());
messages.add(new AssistantMessage(response.getText()));
messages.add(new ToolMessage(toolResult.toString()));
} else {
// 最终回复
break;
}
}
// Step 4: 保存上下文
contextManager.saveContext(session, messages);
return AgentResponse.from(response);
}
}
模块二:RAG 检索服务
@Service
public class RAGRetrievalService {
private final BM25Search bm25Search;
private final VectorSearch vectorSearch;
private final CrossEncoderReranker reranker;
public List<Document> retrieve(String query) {
// 双路召回
List<ScoredDoc> bm25Results = bm25Search.search(query, 50);
List<ScoredDoc> vectorResults = vectorSearch.search(query, 50);
// RRF 融合
List<ScoredDoc> fused = rrfFusion(bm25Results, vectorResults);
// Cross-Encoder 重排序
return reranker.rerank(query, fused, 5);
}
public void indexDocument(String docId, String content) {
ChunkService chunkService = new ChunkService();
List<Chunk> chunks = chunkService.semanticChunk(content);
for (Chunk chunk : chunks) {
// 同时写入 ES 和 Milvus
bm25Search.index(docId, chunk.getText());
vectorSearch.index(docId, chunk.getText());
}
}
}
2. 核心业务场景(虚构示例)
| 场景 | 频率 | 核心流程 | 技术要点 |
|---|---|---|---|
| 订单查询 | 高频 | 意图识别→工具调用→返回结果 | Function Call 稳定性 |
| 退款申请 | 中频 | 多工具编排→事务处理→状态流转 | 多 Agent 协作 |
| 业务规则咨询 | 中频 | RAG 检索→知识注入→生成回答 | 检索准确率 |
| 投诉工单创建 | 低频 | 意图识别→表单生成→工单创建 | 多轮对话理解 |
3. 面试回答要点
- 明确职责边界:清晰说明自己负责的模块
- 量化贡献:优化了什么、提升了多少
- 展示技术深度:核心模块的关键设计和实现
- 体现业务理解:知道模块在整个业务中的位置和价值
项目压测过吗?并发量多少?QPS/TPS/RT是多少?
原始问法:
- 项目压测过吗?并发量多少?QPS/TPS/RT是多少?
来源题目:
SRC-16-156-503
面试先答
**(虚构示例,需替换)**项目做过完整的压力测试。压测场景:模拟客服高峰期会话量(整点集中涌入),使用 JMeter + 阿里云 PTS 进行。核心指标:并发量:稳定 5000 QPS,峰值 8000 QPS;TPS:平均 3500 TPS(按订单查询为基准);RT:P99 响应时间 < 500ms,平均 < 100ms;错误率:< 0.1%。压测过程发现的问题:压测初期 Redis 热点 Key 导致部分缓存节点过载,通过 热点 Key 散列 + 本地缓存二级缓存 优化解决;MySQL 慢查询导致 RT 突增,通过 索引优化 + 读写分离 解决。最终所有指标满足 SLA 要求。
核心结论
- (虚构)压测指标:5000 QPS 稳定,P99 RT < 500ms,错误率 < 0.1%
- 发现并解决:Redis 热点 Key、MySQL 慢查询
- 优化后满足 SLA 要求
1. 压测方案(虚构示例)
压测工具与场景
| 工具 | 用途 | 版本 |
|---|---|---|
| JMeter | 接口压测 | 5.6 |
| 阿里云 PTS | 全链路压测 | - |
| JConsole | JVM 监控 | - |
| SkyWalking | 链路追踪 | 9.x |
压测场景设计
| 场景 | 比例 | 描述 |
|---|---|---|
| 订单查询 | 60% | 查询订单详情 |
| RAG 问答 | 25% | 知识库问答 |
| 工单创建 | 10% | 创建投诉工单 |
| 退款申请 | 5% | 申请退款 |
2. 压测结果(虚构示例)
| 指标 | 压测前 | 压测后 | SLA 目标 |
|---|---|---|---|
| QPS(稳定) | 2000 | 5000 | ≥ 3000 |
| QPS(峰值) | 3500 | 8000 | ≥ 5000 |
| P99 RT | 1200ms | 480ms | < 500ms |
| 平均 RT | 350ms | 95ms | < 200ms |
| 错误率 | 2.5% | 0.08% | < 0.1% |
3. 压测中发现的问题与优化(虚构示例)
问题一:Redis 热点 Key
// 热点 Key 散列
public class HotKeyDispatcher {
// 将热点 Key 散列到不同 Key,避免单 Key 过热
public String disperseKey(String originalKey) {
int slot = Math.abs(originalKey.hashCode()) % 16;
return originalKey + ":slot" + slot;
}
// 二级缓存:本地缓存 + Redis
@Cacheable(cacheNames = "local", key = "#root.args[0]")
public Product getProduct(String productId) {
return productRepository.findById(productId);
}
}
问题二:MySQL 慢查询
-- 添加复合索引优化查询
ALTER TABLE orders
ADD INDEX idx_user_status_create (user_id, status, create_time);
-- 使用读写分离
@DataSource("slave") // 读操作走从库
public List<Order> queryByUserId(Long userId) {
return orderRepository.findByUserId(userId);
}
4. 面试回答要点
- 要有具体数字:不要说"很高",要说"5000 QPS"
- 展示压测方法论:场景设计、工具选择、指标体系
- 体现优化能力:压测发现问题 → 分析 → 解决 → 验证
- 诚实面对不足:如果有指标未达标,说明改进计划
项目中做过哪些性能优化?
原始问法:
- 项目中做过哪些性能优化?
来源题目:
SRC-16-156-504
面试先答
**(虚构示例,需替换)**我在项目中做过 四层性能优化:应用层(对象池复用、异步化改造、本地缓存)→ 数据库层(索引优化、SQL 调优、读写分离、分库分表)→ 缓存层(Redis Cluster、多级缓存、热点 Key 处理)→ 架构层(CDN 静态化、消息队列削峰、服务降级)。最有效的三项优化是:MySQL 分库分表(单表 500 万→按用户 ID 分 10 库 100 表,查询 RT 从 800ms 降到 50ms)、Redis 多级缓存(本地缓存 + Redis + DB,热点数据命中率 99%+)、异步化改造(核心接口同步→异步 CompletableFuture,吞吐量 +200%)。
核心结论
- 四层优化:应用层 → 数据库层 → 缓存层 → 架构层
- 关键优化:分库分表、多级缓存、异步化
- 量化成果:查询 RT 降 94%,吞吐量 +200%
1. 优化全景图(虚构示例)
┌─────────────────────────────────────────────────┐
│ 架构层优化 │
│ CDN 静态化 | MQ 削峰 | 服务降级 │
└─────────────────────┬───────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 应用层优化 │
│ 对象池复用 | 异步化 | 本地缓存 | 代码优化 │
└─────────────────────┬───────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 缓存层优化 │
│ Redis Cluster | 多级缓存 | 热点 Key | 穿透防护 │
└─────────────────────┬───────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 数据库层优化 │
│ 分库分表 | 索引优化 | SQL 调优 | 读写分离 │
└─────────────────────────────────────────────────┘
2. 关键优化实现(虚构示例)
数据库分库分表
// ShardingSphere 分库分表配置
@Configuration
public class ShardingConfig {
@Bean
public ShardingRuleConfiguration shardingRule() {
ShardingRuleConfiguration rule = new ShardingRuleConfiguration();
rule.setTableShardingStrategyConfigs(Arrays.asList(
new TableShardingStrategyConfiguration(
"t_order",
"user_id",
"mod"
)
));
// 按 user_id 分 10 库 100 表
rule.setDatabaseShardingStrategyConfigs(...);
return rule;
}
}
多级缓存
@Service
public class MultiLevelCacheService {
private final CacheManager localCacheManager; // Caffeine 本地缓存
private final RedisTemplate redisTemplate; // Redis 分布式缓存
public Object getData(String key) {
// L1: 本地缓存(Caffeine,10 分钟过期)
Cache localCache = localCacheManager.getCache("local");
Object value = localCache.get(key);
if (value != null) return value;
// L2: Redis 缓存(1 小时过期)
value = redisTemplate.opsForValue().get(key);
if (value != null) {
localCache.put(key, value); // 回填本地缓存
return value;
}
// L3: 数据库查询
value = queryFromDatabase(key);
// 写入两级缓存
redisTemplate.opsForValue().set(key, value, 1, TimeUnit.HOURS);
localCache.put(key, value);
return value;
}
}
异步化改造
// 核心接口异步化
@Service
public class OrderService {
@Async("orderExecutor")
public CompletableFuture<OrderResult> createOrderAsync(OrderDTO dto) {
// 同步逻辑改为异步执行
Order order = orderRepository.save(convertToEntity(dto));
// 异步发送通知
notificationService.sendOrderCreatedNotification(order);
return CompletableFuture.completedFuture(
new OrderResult(order.getId(), "SUCCESS")
);
}
// 批量并行处理
public List<CompletableFuture<OrderResult>> batchCreateOrders(
List<OrderDTO> dtos) {
return dtos.stream()
.map(this::createOrderAsync)
.collect(Collectors.toList());
}
}
3. 面试回答要点
- 分层展示:从底层到上层,展示系统性的优化思路
- 量化效果:每个优化都有数据支撑
- 展示取舍:说明哪些优化没做、为什么
- 体现体系化思维:不是零散的优化,而是系统性规划
项目中如何保证接口的幂等性?
原始问法:
- 项目中如何保证接口的幂等性?
来源题目:
SRC-16-156-505
面试先答
**(虚构示例,需替换)**我们项目中对 创建型接口(创建订单、支付、退款等)实现了严格的幂等性保证。核心方案是 唯一 Token + Redis 状态锁 + 数据库唯一索引 三重保障:第一重:客户端请求前先获取唯一幂等 Token(UUID),请求时携带;第二重:服务端收到请求后先通过 Token 检查 Redis 中是否存在,存在则直接返回之前的结果;第三重:数据库层通过唯一索引(如 order_no + user_id)防止重复插入。同时用 状态机校验 确保状态流转的幂等性(如只有待支付状态才能支付,重复支付请求直接返回当前状态)。最终实现了所有关键接口的严格幂等。
核心结论
- 三重保障:唯一 Token + Redis 锁 + 数据库唯一索引
- 状态机校验:确保状态流转幂等
- 覆盖场景:创建型、支付型、状态变更型接口
1. 幂等方案实现(虚构示例)
幂等 Token 机制
// 幂等注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
String keyPrefix() default "idempotent:";
int expireSeconds() default 300;
}
// 幂等切面
@Aspect
@Component
public class IdempotentAspect {
private final RedisTemplate redisTemplate;
@Around("@annotation(idempotent)")
public Object around(ProceedingJoinPoint pjp, Idempotent idempotent)
throws Throwable {
String token = extractToken(pjp); // 从请求头获取
String key = idempotent.keyPrefix() + token;
// 检查是否已处理
Boolean exists = redisTemplate.hasKey(key);
if (Boolean.TRUE.equals(exists)) {
// 已处理,直接返回缓存的结果
return redisTemplate.opsForValue().get(key + ":result");
}
// 加锁并执行
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(key, "processing", 10, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(locked)) {
throw new BusinessException("请求正在处理中,请稍后重试");
}
try {
Object result = pjp.proceed();
// 缓存结果
redisTemplate.opsForValue().set(key + ":result", result,
idempotent.expireSeconds(), TimeUnit.SECONDS);
return result;
} finally {
redisTemplate.delete(key); // 释放锁
}
}
}
数据库唯一索引
-- 创建型接口的唯一索引
ALTER TABLE orders
ADD UNIQUE KEY uk_order_user (order_no, user_id);
-- 状态机校验
@Service
public class OrderService {
@Idempotent(keyPrefix = "order:create:")
public OrderResult createOrder(OrderCreateDTO dto) {
// 数据库唯一索引防止重复创建
try {
Order order = orderRepository.save(convertToEntity(dto));
return OrderResult.success(order.getId());
} catch (DuplicateKeyException e) {
// 唯一索引冲突,返回已存在的订单
Order existing = orderRepository.findByOrderNo(dto.getOrderNo());
return OrderResult.success(existing.getId());
}
}
}
状态机校验
// 支付接口的状态机幂等
@Service
public class PaymentService {
private final Map<OrderStatus, Set<OrderStatus>> transitions = Map.of(
OrderStatus.PENDING_PAYMENT, Set.of(OrderStatus.PAID),
OrderStatus.PAID, Set.of(), // 已支付,不再流转
OrderStatus.CANCELLED, Set.of() // 已取消
);
@Idempotent(keyPrefix = "payment:")
public PaymentResult payOrder(String orderId, PaymentDTO dto) {
Order order = orderRepository.findById(orderId);
// 状态机校验
if (!transitions.getOrDefault(order.getStatus(), Set.of())
.contains(OrderStatus.PAID)) {
// 状态不允许变更,返回当前状态
return PaymentResult.currentStatus(order.getStatus());
}
// 执行支付...
order.setStatus(OrderStatus.PAID);
orderRepository.save(order);
return PaymentResult.success();
}
}
2. 面试回答要点
- 展示系统性思路:不是单一方案,而是多层保障
- 覆盖所有场景:创建、支付、状态变更等不同类型的幂等
- 说明 trade-off:幂等会增加一些复杂度,说明是否值得
- 提及边界情况:网络超时重试、并发请求等极端情况的处理
分享一个你印象最深的Bug?当时的现象、定位过程、解决方案、学到的东西?
原始问法:
- 分享一个你印象最深的Bug?当时的现象、定位过程、解决方案、学到的东西?
来源题目:
SRC-16-156-506
面试先答
**(虚构示例,需替换)**印象最深的是一个 Agent 并发会话上下文串扰 的 Bug。现象:客服 A 和用户对话时,偶尔会出现回复中混入客服 B 的对话内容。排查过程:首先怀疑是大模型的问题,排除后发现是会话存储的 Key 冲突。原来我们用 sessionId 作为 Redis Key,但并发场景下 sessionId 的生成逻辑有缺陷——用户快速连续发送消息时,两次请求用了相同的 sessionId 片段。根因:sessionId 用 userId + timestamp 拼接,毫秒级时间戳在高并发下会重复。解决方案:改用 UUID.randomUUID() 生成唯一 sessionId + Redis 的 SETNX 确保唯一性。学到的教训:分布式系统中任何 ID 生成逻辑都需要考虑并发场景,不能依赖时间戳的唯一性。
核心结论
- (虚构)Bug:并发场景下 sessionId 重复导致会话串扰
- 根因:时间戳拼接 ID 在高并发下不唯一
- 解决:改用 UUID + SETNX 唯一性保证
- 教训:分布式 ID 不能依赖时间戳
1. Bug 详情(虚构示例)
现象
客服 A 看到的对话:
用户:我的订单什么时候能发货?
Agent:您的订单【ORD12345】预计明天发货
用户:能帮我查一下物流吗?
Agent:(错误回复)您的订单【ORD67890】已签收 ← 这是客服 B 的订单!
排查过程
Step 1: 怀疑大模型问题
├── 检查是否大模型返回错误
├── 对比正确/错误请求的大模型输入
└── 发现:大模型输入中包含了错误的上下文 → 不是大模型的问题
Step 2: 检查上下文存储
├── 查看 Redis 中 sessionId 对应的数据
├── 发现:有时两个不同的会话指向同一个 Redis Key
└── 定位:sessionId 生成逻辑有问题
Step 3: 分析 sessionId 生成
├── 原代码:
└── String sessionId = userId + "_" + System.currentTimeMillis();
Step 4: 验证假设
├── 高并发下,同一毫秒内多个请求
├── System.currentTimeMillis() 返回相同值
├── 导致 sessionId 重复 → 上下文串扰
└── 确认根因
解决方案
// 修复前:
public String generateSessionId(Long userId) {
return userId + "_" + System.currentTimeMillis();
// 高并发下毫秒级重复!
}
// 修复后:
public String generateSessionId(Long userId) {
return userId + "_" + UUID.randomUUID().toString().replace("-", "");
// UUID 保证全局唯一
}
// 额外保障:Redis SETNX 确保唯一性
public void saveSession(String sessionId, SessionData data) {
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(sessionId, serialize(data), 24, TimeUnit.HOURS);
if (Boolean.FALSE.equals(success)) {
log.warn("Session ID冲突: {}", sessionId);
// 重新生成
sessionId = generateSessionId(userId);
}
}
2. 面试回答要点
- 按照 STAR + Bug 结构回答:现象 → 排查 → 根因 → 解决 → 教训
- 展示排查能力:如何一步步排除干扰、定位根因
- 体现反思能力:从 Bug 中总结的经验和改进措施
- 诚实面对:不要回避自己的错误,重点展示从中学到的东西
项目中使用消息队列有没有遇到什么问题?怎么解决的?
原始问法:
- 项目中使用消息队列有没有遇到什么问题?怎么解决的?
来源题目:
SRC-16-156-507
面试先答
**(虚构示例,需替换)**项目中使用 RocketMQ 遇到过 三个典型问题:消息重复消费、消息积压、消息顺序性。重复消费:消费者超时重试导致重复执行,通过 消费去重(Redis 记录消费 ID + 数据库唯一索引)解决。消息积压:双十一促销导致订单消息积压 10 万+,通过 扩容消费者 + 临时降级(非核心消息丢弃或批量处理)解决,2 小时内清空积压。消息顺序性:同一订单的状态变更消息需要严格有序,通过 MessageQueueSelector 按订单 ID 选择队列 保证同一订单的消息进入同一个队列,配合 单线程消费者 保证顺序。
核心结论
- 三大问题:重复消费、消息积压、消息顺序性
- 解决方案:消费去重、扩容+降级、队列路由
- 关键能力:MQ 问题预防 + 快速处理
1. 问题与解决方案(虚构示例)
问题一:消息重复消费
// 消费者去重
@RocketMQMessageListener(topic = "order_topic",
consumerGroup = "order_group")
public class OrderConsumer implements MessageListener {
private final RedisTemplate redisTemplate;
private final OrderRepository orderRepository;
@Override
public void onMessage(Message msg) {
String msgId = msg.getMsgId();
String dedupKey = "order:consumed:" + msgId;
// Redis 去重(SETNX)
Boolean isFirst = redisTemplate.opsForValue()
.setIfAbsent(dedupKey, "1", 24, TimeUnit.HOURS);
if (Boolean.FALSE.equals(isFirst)) {
log.warn("消息已消费,跳过: {}", msgId);
return;
}
// 数据库唯一索引二次保障
try {
processMessage(msg);
} catch (DuplicateKeyException e) {
log.warn("重复消息,跳过: {}", msgId);
}
}
}
问题二:消息积压
// 积压处理方案
@Service
public class MessageBacklogHandler {
// 1. 临时扩容消费者(通过 API 动态调整实例数)
public void scaleUpConsumers(int targetInstances) {
// 调用 K8s API 增加消费者 Pod 数量
k8sClient.scaleDeployment("order-consumer", targetInstances);
}
// 2. 非核心消息降级
public void processWithPriority(List<Message> messages) {
List<Message> critical = messages.stream()
.filter(m -> m.getPriority() == Priority.HIGH)
.collect(Collectors.toList());
List<Message> normal = messages.stream()
.filter(m -> m.getPriority() == Priority.NORMAL)
.collect(Collectors.toList());
// 优先处理高优先级消息
critical.forEach(this::processMessage);
// 普通消息批量处理
processBatch(normal);
}
// 3. 批量处理减少开销
private void processBatch(List<Message> messages) {
List<String> orderIds = messages.stream()
.map(Message::getOrderId)
.collect(Collectors.toList());
// 批量数据库操作
orderRepository.batchUpdateStatus(orderIds, Status.PROCESSING);
}
}
问题三:消息顺序性
// 生产者:按订单 ID 选择队列
public void sendOrderMessage(OrderEvent event) {
rocketMQTemplate.asyncSend(
"order_topic",
MessageBuilder.withPayload(event)
.setHeader("orderId", event.getOrderId())
.build(),
new SendCallback() { ... },
// 关键:按 orderId 选择 MessageQueue
new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs,
Message msg, Object arg) {
String orderId = (String) arg;
int index = Math.abs(orderId.hashCode()) % mqs.size();
return mqs.get(index);
}
},
event.getOrderId()
);
}
// 消费者:单线程消费保证顺序
@RocketMQMessageListener(
topic = "order_topic",
consumerGroup = "order_orderly_group",
// 消费模式:顺序消费
messageModel = MessageModel.CLUSTERING
)
public class OrderOrderlyConsumer implements MessageListenerOrderly {
@Override
public boolean consumeOrderly(MessageExt msg,
ConsumeOrderlyContext context) {
// 单线程处理,天然保证同一队列内消息顺序
processMessage(msg);
return true;
}
}
2. 面试回答要点
- 覆盖 MQ 核心问题:重复、积压、顺序、丢失
- 展示实战经验:不是理论,是真实遇到并解决的问题
- 体现预案意识:如何预防这些问题(监控、告警、预案)
- 量化效果:积压处理用了多久、恢复效果如何
项目中权限控制模块,你是如何设计数据权限和功能权限的隔离逻辑的?
原始问法:
- 项目中权限控制模块,你是如何设计数据权限和功能权限的隔离逻辑的?
来源题目:
SRC-16-156-508
面试先答
**(虚构示例,需替换)**权限控制模块采用 RBAC + 数据权限注解 的双层设计。功能权限:基于经典 RBAC 模型(用户→角色→权限),通过 Spring Security + JWT + 注解 实现,在 Controller 方法上标注 @PreAuthorize("hasAuthority('order:query')") 控制接口访问。数据权限:通过 自定义注解 @DataScope + AOP 切面 实现,在 Service 层根据用户角色自动拼接数据过滤条件(如部门经理只能看本部门数据,普通员工只能看自己的数据)。隔离逻辑:功能权限在入口层拦截,数据权限在数据层过滤,两者独立生效、互不干扰。核心是 认证和授权分离、功能和数据权限分离 的设计思想。
核心结论
- 功能权限:RBAC + Spring Security + JWT(入口层拦截)
- 数据权限:自定义注解 + AOP 切面(数据层过滤)
- 设计原则:认证/授权分离、功能/数据权限分离
1. 功能权限设计(虚构示例)
// RBAC 数据模型
@Entity
public class SysUser {
private Long id;
private String username;
private String password;
}
@Entity
public class SysRole {
private Long id;
private String name;
private String code;
}
@Entity
public class SysPermission {
private Long id;
private String code; // 如 "order:query"
private String type; // MENU / BUTTON / API
}
// Spring Security 配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.antMatchers("/api/order/**").hasAuthority("order:query")
.anyRequest().authenticated();
}
}
2. 数据权限设计(虚构示例)
// 数据权限注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataScope {
String deptAlias() default "d"; // 部门表别名
String userAlias() default "u"; // 用户表别名
}
// 数据权限切面
@Aspect
@Component
public class DataScopeAspect {
@Before("@annotation(dataScope)")
public void doBefore(JoinPoint point, DataScope dataScope) {
LoginUser user = SecurityUtils.getCurrentUser();
// 构建数据权限 SQL 条件
StringBuilder sql = new StringBuilder(" WHERE 1=1 ");
if (user.isAdmin()) {
sql.append(" "); // 管理员看全部
} else if (user.isDeptManager()) {
// 部门经理看本部门
sql.append(" AND dept_id = ").append(user.getDeptId());
} else {
// 普通员工看自己
sql.append(" AND create_by = ").append(user.getUserId());
}
// 将条件放入 ThreadLocal,供后续 SQL 使用
DataScopeHolder.set(sql.toString());
}
}
// Service 使用示例
@Service
public class OrderService {
@DataScope(deptAlias = "o", userAlias = "u")
public List<Order> queryOrders(OrderQueryDTO dto) {
MybatisPlusWrapper<Order> wrapper = new MybatisPlusWrapper<>();
// 自动拼接数据权限条件
String dataScopeSql = DataScopeHolder.get();
wrapper.apply(dataScopeSql);
return orderMapper.selectList(wrapper);
}
}
3. 权限隔离逻辑
请求到达
↓
┌─────────────────────────────┐
│ Layer 1: 认证(Authentication)│
│ JWT Token 验证 │
│ 获取用户身份信息 │
└─────────────┬───────────────┘
↓
┌─────────────────────────────┐
│ Layer 2: 功能权限(RBAC) │
│ @PreAuthorize 注解校验 │
│ 是否有接口访问权限 │
│ → Controller 层拦截 │
└─────────────┬───────────────┘
↓
┌─────────────────────────────┐
│ Layer 3: 数据权限(DataScope)│
│ @DataScope 注解 + AOP │
│ 拼接数据过滤条件 │
│ → Service/Mapper 层过滤 │
└─────────────────────────────┘
4. 面试回答要点
- 展示分层设计思想:认证 → 功能权限 → 数据权限,层层递进
- 体现实战经验:不是纸上谈兵,有具体的注解和切面实现
- 说明扩展性:如何支持更复杂的权限需求(如行级权限、字段级权限)
- 提及安全考虑:防止越权访问的措施
项目中如果抛出自定义异常,你是如何保证异常信息能准确定位问题的?
原始问法:
- 项目中如果抛出自定义异常,你是如何保证异常信息能准确定位问题的?
来源题目:
SRC-16-156-509
面试先答
**(虚构示例,需替换)**我们通过 结构化异常 + 全局异常处理 + 唯一追踪 ID + 分级日志 四件套保证异常的可定位性。结构化异常:自定义 BusinessException 携带错误码、错误信息、模块名、操作提示,每个错误码对应唯一问题类型。全局异常处理:@ControllerAdvice 统一捕获,记录完整堆栈 + 上下文信息(Trace ID、用户 ID、请求参数)。唯一追踪 ID:每次请求生成 Trace ID,贯穿日志和异常,排查时可以通过 Trace ID 串联整条调用链。分级日志:ERROR 级记录堆栈,WARN 级记录上下文,INFO 级记录关键节点。最终实现 "一错误码一定位" 的快速排查能力。
核心结论
- 四件套:结构化异常 → 全局处理 → Trace ID → 分级日志
- 核心思想:每个异常都有唯一标识和完整上下文
- 效果:快速定位问题,从分钟级到秒级
1. 结构化异常设计(虚构示例)
// 错误码体系
public enum ErrorCode {
// 订单相关(1000-1999)
ORDER_NOT_FOUND(1001, "订单不存在"),
ORDER_STATUS_INVALID(1002, "订单状态不正确"),
// 支付相关(2000-2999)
PAYMENT_FAILED(2001, "支付失败"),
PAYMENT_TIMEOUT(2002, "支付超时"),
// Agent 相关(3000-3999)
AGENT_TOOL_ERROR(3001, "工具调用失败"),
AGENT_RAG_ERROR(3002, "RAG 检索异常");
private final int code;
private final String message;
}
// 自定义业务异常
public class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
private final String traceId; // 追踪 ID
private final String module; // 模块名
private final Map<String, Object> context; // 上下文参数
public BusinessException(ErrorCode errorCode, String traceId,
String module, Map<String, Object> context) {
super(errorCode.getMessage());
this.errorCode = errorCode;
this.traceId = traceId;
this.module = module;
this.context = context;
}
}
2. 全局异常处理(虚构示例)
@RestControllerAdvice
public class GlobalExceptionHandler {
private final Logger log = LoggerFactory.getLogger(this.getClass());
@ExceptionHandler(BusinessException.class)
public ResponseEntity<?> handleBusinessException(
BusinessException ex, HttpServletRequest request) {
// 记录完整异常信息
log.error("业务异常 [TraceId:{}] [模块:{}] [错误码:{}] [用户:{}] " +
"[路径:{}] [参数:{}] [堆栈:{}]",
ex.getTraceId(),
ex.getModule(),
ex.getErrorCode().getCode(),
getCurrentUserId(),
request.getRequestURI(),
ex.getContext(),
ex.getStackTrace());
// 返回结构化响应
ErrorResponse response = ErrorResponse.builder()
.code(ex.getErrorCode().getCode())
.message(ex.getErrorCode().getMessage())
.traceId(ex.getTraceId())
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<?> handleUnknownException(
Exception ex, HttpServletRequest request) {
String traceId = request.getHeader("X-Trace-Id");
log.error("未知异常 [TraceId:{}] [路径:{}]", traceId,
request.getRequestURI(), ex);
ErrorResponse response = ErrorResponse.builder()
.code(500)
.message("系统内部错误")
.traceId(traceId)
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(response);
}
}
3. Trace ID 全链路追踪(虚构示例)
// Trace ID 过滤器
@Component
public class TraceIdFilter implements Filter {
private static final String TRACE_ID_HEADER = "X-Trace-Id";
private static final ThreadLocal<String> TRACE_ID_HOLDER = new ThreadLocal<>();
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 生成或获取 Trace ID
String traceId = httpRequest.getHeader(TRACE_ID_HEADER);
if (traceId == null || traceId.isEmpty()) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
TRACE_ID_HOLDER.set(traceId);
response.setHeader(TRACE_ID_HEADER, traceId);
try {
chain.doFilter(request, response);
} finally {
TRACE_ID_HOLDER.remove();
}
}
public static String getCurrentTraceId() {
return TRACE_ID_HOLDER.get();
}
}
4. 面试回答要点
- 展示体系化设计:不是简单的 try-catch,而是完整的异常管理体系
- 体现可观测性:Trace ID + 结构化日志,支持快速排查
- 关注用户体验:对用户返回友好的错误信息,不暴露技术细节
- 提及监控告警:异常监控大盘、告警规则
是否部署过项目?具体怎么部署的?
原始问法:
- 是否部署过项目?具体怎么部署的?
来源题目:
SRC-16-156-510
面试先答
**(虚构示例,需替换)**项目采用 Docker + Kubernetes + CI/CD 的容器化部署方案。部署流程:开发提交代码 → GitLab CI 触发 → 代码检查(SonarQube)→ 单元测试 → 构建 Docker 镜像 → 推送到 Harbor 镜像仓库 → 部署到 Kubernetes(Dev → Test → Staging → Production 四环境)。K8s 核心配置:Deployment(副本数、健康检查、滚动更新)、Service(负载均衡)、ConfigMap/Secret(配置和密钥管理)、HPA(自动扩缩容,CPU > 70% 自动扩容)、Ingress(域名路由)。发布策略:灰度发布(金丝雀部署,先部署 10% 流量,观察 30 分钟后全量)+ 快速回滚。
核心结论
- 部署方案:Docker + Kubernetes + CI/CD
- 部署流程:代码提交 → CI 检查 → 镜像构建 → K8s 部署
- 发布策略:灰度发布 + 快速回滚 + HPA 自动扩缩
1. CI/CD 流程(虚构示例)
开发者提交代码
↓
┌─────────────────────────────┐
│ GitLab CI Pipeline │
│ 1. 代码检查(SonarQube) │
│ 2. 单元测试(JUnit) │
│ 3. 构建打包(Maven) │
│ 4. 构建 Docker 镜像 │
│ 5. 推送到 Harbor 仓库 │
└─────────────┬───────────────┘
↓
┌─────────────────────────────┐
│ Kubernetes 部署 │
│ 1. Dev 环境自动部署 │
│ 2. Test 环境手动触发 │
│ 3. Staging 环境审批后部署 │
│ 4. Production 灰度发布 │
└─────────────────────────────┘
2. K8s 核心配置(虚构示例)
# Deployment 配置
apiVersion: apps/v1
kind: Deployment
metadata:
name: agent-service
namespace: production
spec:
replicas: 5
selector:
matchLabels:
app: agent-service
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 0 # 零宕机
template:
spec:
containers:
- name: agent-service
image: harbor.example.com/agent-service:latest
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
envFrom:
- configMapRef:
name: agent-config
- secretRef:
name: agent-secrets
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2000m"
memory: "4Gi"
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 30"] # 优雅关闭
---
# HPA 自动扩缩容
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: agent-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: agent-service
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
3. 灰度发布策略(虚构示例)
# Ingress 灰度配置
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: agent-ingress
annotations:
nginx.ingress.kubernetes.io/canary-by-header: "canary"
nginx.ingress.kubernetes.io/canary-weight: "10" # 10% 流量
spec:
rules:
- host: agent.example.com
http:
paths:
- path: /
backend:
service:
name: agent-service-canary
port:
number: 8080
---
apiVersion: v1
kind: Service
metadata:
name: agent-service-canary
spec:
selector:
app: agent-service
version: v2 # 新版本
ports:
- port: 8080
4. 面试回答要点
- 展示完整流程:从代码提交到生产部署的完整链路
- 体现工程化能力:CI/CD、Dockerfile、K8s YAML 等
- 关注高可用:健康检查、滚动更新、HPA、优雅关闭
- 提及发布策略:灰度、回滚、监控
项目中如何保证数据库和ES数据同步?
原始问法:
- 项目中如何保证数据库和ES数据同步?
来源题目:
SRC-16-156-511
面试先答
**(虚构示例,需替换)**数据库和 ES 的数据同步采用 "增量实时 + 全量定期 + 最终一致性校验" 的三重保障方案。增量实时同步:通过 Canal 监听 MySQL Binlog,捕获数据变更事件(INSERT/UPDATE/DELETE),发送到 RocketMQ,消费者消费后同步到 ES。全量定期同步:每天凌晨定时任务全量扫描 MySQL 数据,重建 ES 索引,保证数据最终一致性。一致性校验:每小时对比 MySQL 和 ES 的数据量,不一致时触发增量补偿。同时通过 版本号机制(每次更新版本号,ES 只保留最新版本)解决并发更新冲突。
核心结论
- 三重保障:Binlog 增量 + 定时全量 + 周期校验
- 核心机制:Canal + MQ + 版本号控制
- 一致性:最终一致性,秒级延迟
1. 同步架构(虚构示例)
┌──────────┐ Binlog ┌──────────┐ MQ ┌──────────┐
│ MySQL │──────────────→│ Canal │────────→│ RocketMQ │
│ (主库) │ │ (监听) │ │ (消息) │
└──────────┘ └──────────┘ └────┬─────┘
↓
┌──────────┐ ┌──────────┐
│ 业务服务 │ │ ES Consumer
│ (写DB) │ │ (消费MQ) │
└────┬─────┘ └────┬─────┘
│ ↓
│ 定期全量扫描 ┌──────────┐
└──────────────────────────────────────────→│ ElasticSearch
│ (索引) │
└──────────┘
↑
│ 每小时校验
┌──────────┐
│ 数据校验 │
│ 定时任务 │
└──────────┘
2. 核心实现(虚构示例)
Canal 配置
# canal 配置实例.properties
canal.instance.master.address=mysql-master:3306
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal_pwd
canal.instance.filter.regex=order_db\\..*
canal.mq.exchange=canal.exchange
canal.mq.routingKey=canal.routing.key
MQ 消费者
@RocketMQMessageListener(
topic = "canal_order_topic",
consumerGroup = "es_sync_group"
)
public class ESSyncConsumer implements MessageListener {
private final RestHighLevelClient esClient;
@Override
public void onMessage(Message msg) {
CanalMessage canalMsg = JSON.parseObject(
new String(msg.getBody()), CanalMessage.class
);
String tableName = canalMsg.getTable();
String eventType = canalMsg.getType(); // INSERT/UPDATE/DELETE
switch (eventType) {
case "INSERT":
case "UPDATE":
// 同步到 ES(upsert 语义)
esClient.index(IndexRequest.of(
i -> i.index(tableName)
.id(canalMsg.getId())
.document(canalMsg.getData())
));
break;
case "DELETE":
// 删除 ES 文档
esClient.delete(DeleteRequest.of(
tableName, canalMsg.getId()
));
break;
}
}
}
版本号控制
// 解决并发更新冲突
@Entity
public class Order {
@Version // JPA 乐观锁
private Long version;
private Long id;
private String status;
private LocalDateTime updatedAt;
}
// ES 索引中存储版本号
public void syncToES(Order order) {
esClient.index(IndexRequest.of(i -> {
try {
return i.index("orders")
.id(String.valueOf(order.getId()))
.document(Map.of(
"order_id", order.getId(),
"status", order.getStatus(),
"version", order.getVersion(), // 版本号
"updated_at", order.getUpdatedAt()
))
.ifSeqNoIsPresent() // 仅当版本号更大时才更新
.set("seq_no_primary_term",
getCurrentESVersion(order.getId()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}));
}
全量同步
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨 2 点
public void fullSync() {
List<Order> allOrders = orderRepository.findAll();
// 分批处理,每批 500 条
Lists.partition(allOrders, 500).forEach(batch -> {
esClient.bulk(BulkRequest.of(b -> {
for (Order order : batch) {
b.index(IndexRequest.of(i ->
i.index("orders")
.id(String.valueOf(order.getId()))
.document(convertToDocument(order))
));
}
return b;
}));
});
log.info("全量同步完成,共 {} 条", allOrders.size());
}
3. 面试回答要点
- 展示完整性:增量 + 全量 + 校验的三重保障
- 体现技术深度:Canal、Binlog、版本号控制
- 说明权衡:为什么选 Canal 而不是同步双写、优缺点
- 提及一致性:说明是最终一致性,允许短暂不一致
项目中Token过期机制是怎么设计的?
原始问法:
- 项目中Token过期机制是怎么设计的?
来源题目:
SRC-16-156-512
面试先答
**(虚构示例,需替换)**Token 过期机制采用 Access Token + Refresh Token 双令牌 设计。Access Token:短期有效(30 分钟),用于日常接口访问,存放在 Redis 中,Key 为 token:{userId},过期自动清除。Refresh Token:长期有效(7 天),用于刷新 Access Token,存放在 Redis 中,Key 为 refresh:{userId}。刷新流程:Access Token 过期后,前端用 Refresh Token 调用 /api/auth/refresh → 后端校验 Refresh Token → 生成新的 Access Token 和 Refresh Token(旧 Refresh Token 立即失效,防止 token 重放)。退出登录:删除 Redis 中的两个 Token。安全增强:Token 绑定设备指纹 + 异常登录告警 + 强制下线功能。
核心结论
- 双令牌设计:Access Token(30分钟)+ Refresh Token(7天)
- 安全机制:Token 旋转、设备绑定、异常告警、强制下线
- 存储方案:Redis 存储,自动过期
1. Token 生命周期(虚构示例)
用户登录
↓
┌─────────────────────────────┐
│ 生成 Access Token (30min) │
│ 生成 Refresh Token (7day) │
│ 存入 Redis │
│ 返回两个 Token 给前端 │
└─────────────┬───────────────┘
↓
┌─────────────────────────────┐
│ 正常使用阶段 │
│ 前端带 Access Token 请求 │
│ 后端校验 → 返回数据 │
│ Access Token 即将过期时 │
│ 前端自动用 Refresh Token 刷新│
└─────────────┬───────────────┘
↓
┌─────────────────────────────┐
│ 刷新阶段 │
│ 校验 Refresh Token │
│ 生成新的 Access + Refresh │
│ 旧 Refresh Token 立即失效 │
│ 返回新的两个 Token │
└─────────────┬───────────────┘
↓
┌─────────────────────────────┐
│ 安全机制 │
│ 设备指纹绑定校验 │
│ 异常登录告警 │
│ 管理员可强制下线 │
│ Refresh Token 过期 → 需重新登录│
└─────────────────────────────┘
2. 核心实现(虚构示例)
// Token 服务
@Service
public class TokenService {
private static final long ACCESS_TOKEN_EXPIRE = 30 * 60; // 30 分钟
private static final long REFRESH_TOKEN_EXPIRE = 7 * 24 * 3600; // 7 天
private final JwtUtil jwtUtil;
private final RedisTemplate redisTemplate;
// 登录时生成双 Token
public TokenPair generateTokenPair(Long userId, String deviceFingerprint) {
String accessToken = jwtUtil.generate(userId, ACCESS_TOKEN_EXPIRE);
String refreshToken = jwtUtil.generate(
userId + ":refresh", REFRESH_TOKEN_EXPIRE
);
// 存入 Redis
redisTemplate.opsForValue().set(
"token:" + userId,
accessToken,
ACCESS_TOKEN_EXPIRE, TimeUnit.SECONDS
);
redisTemplate.opsForValue().set(
"refresh:" + userId,
refreshToken + ":" + deviceFingerprint,
REFRESH_TOKEN_EXPIRE, TimeUnit.SECONDS
);
return new TokenPair(accessToken, refreshToken);
}
// 刷新 Token(Token 旋转)
public TokenPair refresh(String refreshToken, String deviceFingerprint) {
// 1. 校验 Refresh Token
Long userId = jwtUtil.parseUserId(refreshToken);
String stored = redisTemplate.opsForValue()
.get("refresh:" + userId);
if (stored == null) {
throw new AuthException("Refresh Token 已失效");
}
// 2. 设备指纹校验
String storedFingerprint = stored.split(":")[1];
if (!storedFingerprint.equals(deviceFingerprint)) {
// 设备不一致,告警
alertService.alertAbnormalLogin(userId);
throw new AuthException("设备不一致");
}
// 3. 旧 Refresh Token 立即失效(防止重放)
redisTemplate.delete("refresh:" + userId);
// 4. 生成新 Token 对
return generateTokenPair(userId, deviceFingerprint);
}
// 强制下线
public void forceLogout(Long userId) {
redisTemplate.delete("token:" + userId);
redisTemplate.delete("refresh:" + userId);
}
}
3. 面试回答要点
- 展示完整的 Token 生命周期管理
- 体现安全性:Token 旋转、设备绑定、异常告警
- 说明权衡:为什么用双令牌而不是单令牌
- 提及扩展:如何支持多设备登录、单点登录(SSO)
JWT鉴权流程是怎样的?
原始问法:
- JWT鉴权流程是怎样的?
来源题目:
SRC-16-156-513
面试先答
**(虚构示例,需替换)**JWT(JSON Web Token)鉴权流程分为 五步:第一步:用户登录 → 客户端发送用户名密码到 /api/auth/login;第二步:服务端验证 → 校验密码,生成 JWT(包含用户 ID、角色、过期时间等声明);第三步:返回 Token → 服务端返回 JWT(Header.Payload.Signature 三段式),客户端存储在 localStorage 或 Cookie;第四步:携带 Token 请求 → 客户端每次请求在 HTTP Header 中携带 Authorization: Bearer <JWT>;第五步:服务端校验 → 解析 JWT、验证签名、检查过期时间、提取用户信息进行权限判断。我们项目中使用 JJWT 库 实现,配合 Redis 存储 Token 版本号 支持强制下线,Access Token 有效期 30 分钟,Refresh Token 7 天。
核心结论
- JWT 五步流程:登录→验证→返回→携带→校验
- JWT 结构:Header(算法类型).Payload(声明数据).Signature(签名)
- 安全增强:Token 版本号、黑名单、Refresh Token 旋转
1. JWT 结构详解
JWT Token 结构:
┌─────────────────────────────────────────────────┐
│ Header.Payload.Signature │
├─────────────────────────────────────────────────┤
│ │
│ Header(Header): │
│ { │
│ "alg": "HS256", // 签名算法 │
│ "typ": "JWT" // Token 类型 │
│ } │
│ │
│ Payload(载荷): │
│ { │
│ "sub": "1234567890", // 主题(用户ID) │
│ "name": "John Doe", // 用户名 │
│ "iat": 1516239022, // 签发时间 │
│ "exp": 1516240000 // 过期时间 │
│ } │
│ │
│ Signature(签名): │
│ HMACSHA256( │
│ base64(Header) + "." + base64(Payload), │
│ secretKey │
│ ) │
│ │
└─────────────────────────────────────────────────┘
2. 完整鉴权流程(虚构示例)
客户端 服务端
│ │
│ 1. POST /api/auth/login │
│ {username, password} │
│─────────────────────────────→│
│ │
│ │ 2. 验证用户名密码
│ │ - 查询用户
│ │ - BCrypt 校验密码
│ │ - 检查账号状态
│ │
│ │ 3. 生成 JWT
│ │ - Header: {"alg":"HS256","typ":"JWT"}
│ │ - Payload: {sub, userId, role, exp}
│ │ - Signature: HMACSHA256(header+payload)
│ │
│ 4. 返回 Token │
│ {accessToken, refreshToken}│
│←─────────────────────────────│
│ │
│ 5. GET /api/orders │
│ Authorization: Bearer <JWT>│
│─────────────────────────────→│
│ │
│ │ 6. 校验 JWT
│ │ - 解析 Header.Payload.Signature
│ │ - 验证签名(确保未篡改)
│ │ - 检查 exp 是否过期
│ │ - 检查 Redis 黑名单
│ │ - 提取 userId、role
│ │
│ │ 7. 权限校验
│ │ - 检查角色是否有权访问
│ │ - 数据权限过滤
│ │
│ 8. 返回数据 │
│←─────────────────────────────│
3. 核心代码实现(虚构示例)
// JWT 工具类
@Component
public class JwtUtil {
private static final String SECRET_KEY = "your-256-bit-secret-key-here";
private static final long ACCESS_TOKEN_EXPIRE = 30 * 60; // 30 分钟
private static final long REFRESH_TOKEN_EXPIRE = 7 * 24 * 3600; // 7 天
// 生成 Token
public String generateAccessToken(Long userId, String role) {
return Jwts.builder()
.setHeaderParam("alg", "HS256")
.setHeaderParam("typ", "JWT")
.setSubject(String.valueOf(userId))
.claim("role", role)
.claim("type", "access")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis()
+ ACCESS_TOKEN_EXPIRE * 1000))
.signWith(Keys.hmacShaKeyFor(SECRET_KEY.getBytes()),
SignatureAlgorithm.HS256)
.compact();
}
// 解析和验证 Token
public Claims parseToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(SECRET_KEY.getBytes()))
.build()
.parseClaimsJws(token)
.getBody();
}
// 验证 Token 并提取用户信息
public UserInfo validateAndGetUser(String token) {
Claims claims = parseToken(token);
// 检查过期
if (claims.getExpiration().before(new Date())) {
throw new JwtException("Token 已过期");
}
return new UserInfo(
Long.parseLong(claims.getSubject()),
claims.get("role", String.class),
claims.get("type", String.class)
);
}
}
JWT 过滤器
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = authHeader.substring(7);
try {
UserInfo userInfo = jwtUtil.validateAndGetUser(token);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
userInfo, null,
getAuthorities(userInfo.getRole())
);
SecurityContextHolder.getContext().setAuthentication(auth);
} catch (JwtException e) {
// Token 无效,返回 401
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json");
response.getWriter().write(
"{\"error\":\"Invalid or expired token\"}"
);
return;
}
filterChain.doFilter(request, response);
}
}
4. 安全增强(虚构示例)
// JWT 黑名单 + Token 版本号
@Service
public class JwtSecurityService {
private final RedisTemplate redisTemplate;
// 强制下线:将 Token 加入黑名单
public void blacklistToken(String token, long remainingTime) {
redisTemplate.opsForValue().set(
"jwt:blacklist:" + token,
"1",
remainingTime, TimeUnit.SECONDS
);
}
// 检查 Token 是否在黑名单
public boolean isBlacklisted(String token) {
return Boolean.TRUE.equals(
redisTemplate.hasKey("jwt:blacklist:" + token)
);
}
// Token 版本号:支持修改密码后所有 Token 失效
public void incrementTokenVersion(Long userId) {
String key = "jwt:version:" + userId;
redisTemplate.opsForValue().increment(key);
}
// 验证 Token 版本号
public boolean verifyTokenVersion(Long userId, long tokenVersion) {
String key = "jwt:version:" + userId;
String currentVersion = redisTemplate.opsForValue().get(key);
if (currentVersion == null) return true; // 无版本号,允许访问
return Long.parseLong(currentVersion) == tokenVersion;
}
}
5. 常见追问
- 追问 1:JWT 和 Session 的区别?
- JWT 是无状态的,服务端不需要存储;Session 是有状态的,服务端需要存储。JWT 适合分布式,Session 适合传统单体
- 追问 2:JWT 的缺点是什么?
- 无法主动失效(需配合黑名单)、Payload 信息不宜过大、需要 HTTPS 传输
- 追问 3:如何防止 JWT 被窃取?
- HTTPS 加密传输、HttpOnly Cookie 存储、短有效期、绑定设备指纹
6. 面试回答要点
- 展示完整流程:从登录到鉴权的每一步
- 体现技术细节:JWT 三段结构、签名算法、代码实现
- 关注安全性:黑名单、版本号、设备绑定等增强措施
- 说明项目实践:结合项目说明具体的配置和策略
一句话总结
JWT 鉴权通过登录验证→生成 Token→携带 Token 请求→服务端校验的流程实现无状态认证,配合黑名单和版本号等增强机制保证安全性,适用于分布式系统。